Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 62 additions & 20 deletions examples/cells/src/py/pycells/_syntax.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Syntax module."""
"""Syntax module.

C++ templates only exist once instantiated at concrete arguments, so cppwg wraps
each instantiation as its own class with a mangled name: Point<2> → Point_2,
Point<3> → Point_3. That's correct but we'd rather write Point[2] in Python,
mirroring C++'s Point<2>. This is a helper module to add that subscript syntax,
holding two helpers — TemplateClass (a stub base class using __class_getitem__,
like list[int]) and TemplateMethod (a descriptor) — plus the shared key
normalization that resolves a subscript to the concrete instantiation.
"""

from collections.abc import Iterable

Expand All @@ -7,7 +16,7 @@ def _normalize_key(key):
"""Normalize a template-argument subscript key to a tuple of strings.

A scalar key becomes a 1-tuple; each argument maps to its ``__name__`` (for a
class) or ``str`` otherwise - so ``Node[2]`` and ``MacroMesh[2, 2]`` and
class) or ``str`` otherwise - so ``Point[2]`` and ``MacroMesh[2, 2]`` and
``CellFactory[Cell, 2]`` all key the same way the wrapped names were built. A
string is treated as a single scalar (not iterated character by character).
"""
Expand All @@ -21,9 +30,13 @@ class TemplateClass:

Subclass it with an ``_instantiations`` map from template-argument tuples to
the concrete wrapped classes; ``Foo[args]`` then resolves the instantiation -
e.g. ``Node[2]`` -> ``Node_2`` - mirroring how ``list[int]`` works via
e.g. ``Point[2]`` -> ``Point_2`` - mirroring how ``list[int]`` works via
``__class_getitem__``. Subclassing (rather than an instance) makes ``Foo`` a
real class object. Keys are normalized once at subclass creation.

Usage:
>>> class Foo(TemplateClass):
... _instantiations = {2: Foo_2, 3: Foo_3}
"""

_instantiations: dict = {}
Expand All @@ -39,30 +52,59 @@ def __class_getitem__(cls, key):


class TemplateMethod:
"""Subscript syntax for a templated method (the method analogue of
TemplateClass).

Assign it as a class attribute so ``obj.<base>[Arg]()`` dispatches to the
per-instantiation binding ``obj.<base>_<Arg>()`` that cppwg generates for the
templated C++ method - e.g. ``pop.AddCellWriter[CellVolumesWriter]()`` calls
``pop.AddCellWriter_CellVolumesWriter()``. Each subscript argument maps to its
``__name__`` (for a class) or ``str`` (otherwise), joined with underscores to
match cppwg's ``Foo_2`` instantiation naming.
"""Subscript syntax for a templated method.

TemplateMethod is a descriptor: set it as a class attribute, then use the
``obj.<base>[Arg]()`` subscript form to reach the per-instantiation binding
``obj.<base>_<Arg>()`` that cppwg generates. When the name is also a plain
(non-templated) overload, pass it as ``fallback`` so ``obj.<base>(...)`` keeps
working alongside the subscript form.

Usage:
>>> Foo.Bar = TemplateMethod("Bar")
>>> foo_obj.Bar[T]()

If ``Bar`` also has a plain overload, keep it as the fallback:
>>> Foo.Bar = TemplateMethod("Bar", Foo.Bar)
>>> foo_obj.Bar(arg) # the plain overload, via the fallback
"""

def __init__(self, base_name):
self._base_name = base_name
def __init__(self, base_name, fallback=None):
self._base_name = base_name # e.g. "Bar" for foo_obj.Bar[T]()
self._fallback = fallback # a plain overload of the same name, or None

def __get__(self, obj, owner=None):
return _BoundTemplateMethod(obj if obj is not None else owner, self._base_name)
# Bar is a descriptor on the class, so accessing ``foo_obj.Bar`` triggers
# __get__, returning a _BoundTemplateMethod. obj is the instance, or None
# when accessed on the class itself (``Foo.Bar``); owner is the class.
return _BoundTemplateMethod(obj, owner, self._base_name, self._fallback)


class _BoundTemplateMethod:
def __init__(self, target, base_name):
self._target = target
self._base_name = base_name
def __init__(self, obj, owner, base_name, fallback):
self._obj = obj # the instance, or None when accessed on the class
# The mangled bindings live on the instance's class; look them up on the
# instance (instance access) or the class itself (class access).
self._target = obj if obj is not None else owner
self._base_name = base_name # e.g. "Bar" for foo_obj.Bar[T]()
self._fallback = fallback

def __getitem__(self, key):
# Mangled binding is <base>_<arg1>_<arg2>..., matching cppwg's Foo_2 style.
suffix = "_" + "_".join(_normalize_key(key))
# The [T] subscript on ``foo_obj.Bar[T]()`` triggers __getitem__,
# returning the target.Bar_T method, the binding generated by cppwg.
suffix = "_" + "_".join(_normalize_key(key)) # e.g. _T
return getattr(self._target, self._base_name + suffix)

def __call__(self, *args, **kwargs):
# ``foo_obj.Bar(...)`` with no subscript calls the plain overload kept as
# the fallback; with no fallback the name is purely templated, so point
# the caller at the subscript form.
if self._fallback is None:
raise TypeError(
f"{self._base_name} is templated; use {self._base_name}[Arg](...)"
)
# On instance access, bind the instance as the receiver. On class access
# (``Foo.Bar(inst, ...)``) the caller passes it, so don't inject it again.
if self._obj is None:
return self._fallback(*args, **kwargs)
return self._fallback(self._obj, *args, **kwargs)
16 changes: 16 additions & 0 deletions examples/shapes/src/cpp/primitives/UnitSquare.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
* GetAreaIn_SquareMetres, GetAreaIn_SquareFeet. The pyshapes package then exposes
* them through the TemplateMethod descriptor as GetAreaIn[SquareMetres]() etc.,
* mirroring pychaste's AddCellWriter[Writer]().
*
* GetAreaIn is also overloaded with a plain, non-templated form that takes an
* explicit units-per-square-metre factor. cppwg wraps that overload normally as
* GetAreaIn, which the TemplateMethod descriptor would otherwise shadow; the
* package keeps it reachable by passing it as the descriptor's fallback, so
* square.GetAreaIn(factor) works alongside square.GetAreaIn[SquareFeet]() -
* mirroring pychaste's AddCellWriter(writer) plain overload.
*/
class UnitSquare
{
Expand Down Expand Up @@ -42,6 +49,15 @@ class UnitSquare
{
return GetArea() * UNIT().PerSquareMetre();
}

/**
* Return the area expressed in a custom unit, given how many of that unit
* make up one square metre. A plain (non-templated) overload of GetAreaIn.
*/
double GetAreaIn(double perSquareMetre) const
{
return GetArea() * perSquareMetre;
}
};

#endif // UNIT_SQUARE_HPP_
78 changes: 61 additions & 17 deletions examples/shapes/src/py/pyshapes/_syntax.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
"""Syntax module.

C++ templates only exist once instantiated at concrete arguments, so cppwg wraps
each instantiation as its own class with a mangled name: Point<2> → Point_2,
Point<3> → Point_3. That's correct but we'd rather write Point[2] in Python,
mirroring C++'s Point<2>. This is a helper module to add that subscript syntax,
holding two helpers — TemplateClass (a stub base class using __class_getitem__,
like list[int]) and TemplateMethod (a descriptor) — plus the shared key
normalization that resolves a subscript to the concrete instantiation.
"""

from collections.abc import Iterable


Expand All @@ -22,6 +33,10 @@ class TemplateClass:
e.g. ``Point[2]`` -> ``Point_2`` - mirroring how ``list[int]`` works via
``__class_getitem__``. Subclassing (rather than an instance) makes ``Foo`` a
real class object. Keys are normalized once at subclass creation.

Usage:
>>> class Foo(TemplateClass):
... _instantiations = {2: Foo_2, 3: Foo_3}
"""

_instantiations: dict = {}
Expand All @@ -37,30 +52,59 @@ def __class_getitem__(cls, key):


class TemplateMethod:
"""Subscript syntax for a templated method (the method analogue of
TemplateClass).

Assign it as a class attribute so ``obj.<base>[Arg]()`` dispatches to the
per-instantiation binding ``obj.<base>_<Arg>()`` that cppwg generates for the
templated C++ method - e.g. ``pop.AddCellWriter[CellVolumesWriter]()`` calls
``pop.AddCellWriter_CellVolumesWriter()``. Each subscript argument maps to its
``__name__`` (for a class) or ``str`` (otherwise), joined with underscores to
match cppwg's ``Foo_2`` instantiation naming.
"""Subscript syntax for a templated method.

TemplateMethod is a descriptor: set it as a class attribute, then use the
``obj.<base>[Arg]()`` subscript form to reach the per-instantiation binding
``obj.<base>_<Arg>()`` that cppwg generates. When the name is also a plain
(non-templated) overload, pass it as ``fallback`` so ``obj.<base>(...)`` keeps
working alongside the subscript form.

Usage:
>>> Foo.Bar = TemplateMethod("Bar")
>>> foo_obj.Bar[T]()

If ``Bar`` also has a plain overload, keep it as the fallback:
>>> Foo.Bar = TemplateMethod("Bar", Foo.Bar)
>>> foo_obj.Bar(arg) # the plain overload, via the fallback
"""

def __init__(self, base_name):
self._base_name = base_name
def __init__(self, base_name, fallback=None):
self._base_name = base_name # e.g. "Bar" for foo_obj.Bar[T]()
self._fallback = fallback # a plain overload of the same name, or None

def __get__(self, obj, owner=None):
return _BoundTemplateMethod(obj if obj is not None else owner, self._base_name)
# Bar is a descriptor on the class, so accessing ``foo_obj.Bar`` triggers
# __get__, returning a _BoundTemplateMethod. obj is the instance, or None
# when accessed on the class itself (``Foo.Bar``); owner is the class.
return _BoundTemplateMethod(obj, owner, self._base_name, self._fallback)


class _BoundTemplateMethod:
def __init__(self, target, base_name):
self._target = target
self._base_name = base_name
def __init__(self, obj, owner, base_name, fallback):
self._obj = obj # the instance, or None when accessed on the class
# The mangled bindings live on the instance's class; look them up on the
# instance (instance access) or the class itself (class access).
self._target = obj if obj is not None else owner
self._base_name = base_name # e.g. "Bar" for foo_obj.Bar[T]()
self._fallback = fallback

def __getitem__(self, key):
# Mangled binding is <base>_<arg1>_<arg2>..., matching cppwg's Foo_2 style.
suffix = "_" + "_".join(_normalize_key(key))
# The [T] subscript on ``foo_obj.Bar[T]()`` triggers __getitem__,
# returning the target.Bar_T method, the binding generated by cppwg.
suffix = "_" + "_".join(_normalize_key(key)) # e.g. _T
return getattr(self._target, self._base_name + suffix)

def __call__(self, *args, **kwargs):
# ``foo_obj.Bar(...)`` with no subscript calls the plain overload kept as
# the fallback; with no fallback the name is purely templated, so point
# the caller at the subscript form.
if self._fallback is None:
raise TypeError(
f"{self._base_name} is templated; use {self._base_name}[Arg](...)"
)
# On instance access, bind the instance as the receiver. On class access
# (``Foo.Bar(inst, ...)``) the caller passes it, so don't inject it again.
if self._obj is None:
return self._fallback(*args, **kwargs)
return self._fallback(self._obj, *args, **kwargs)
6 changes: 4 additions & 2 deletions examples/shapes/src/py/pyshapes/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ class Shape(TemplateClass):


# UnitSquare::GetAreaIn<UNIT>() is a templated method (see GetAreaInCustomTemplate.py);
# expose its per-unit bindings as GetAreaIn[Unit]().
UnitSquare.GetAreaIn = TemplateMethod("GetAreaIn")
# expose its per-unit bindings as GetAreaIn[Unit](). GetAreaIn is also a plain
# overload (GetAreaIn(perSquareMetre)); pass it as the fallback so the descriptor
# does not shadow it and UnitSquare.GetAreaIn(factor) keeps working.
UnitSquare.GetAreaIn = TemplateMethod("GetAreaIn", UnitSquare.GetAreaIn)
18 changes: 18 additions & 0 deletions examples/shapes/src/py/tests/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ def testTemplateMethodSyntax(self):
square.GetAreaIn[prim.SquareFeet](), square.GetAreaIn_SquareFeet()
)

def testTemplateMethodFallback(self):
# GetAreaIn is also a plain (non-templated) overload,
# GetAreaIn(perSquareMetre). The TemplateMethod descriptor would shadow it,
# but it was passed as the fallback, so calling GetAreaIn without a
# subscript dispatches to the plain overload.
prim = pyshapes.primitives
square = prim.UnitSquare(3.0) # side 3 -> 9 square metres

# Plain call (no subscript) -> the C++ GetAreaIn(double) overload.
self.assertAlmostEqual(square.GetAreaIn(10.7639104), 96.8752, places=4)
# It agrees with the templated form when given that unit's factor.
self.assertEqual(square.GetAreaIn(1.0), square.GetAreaIn[prim.SquareMetres]())

# Class-level access supplies the receiver explicitly, so the descriptor
# must not inject it again: UnitSquare.GetAreaIn(square, factor) behaves
# like the unbound plain overload.
self.assertEqual(prim.UnitSquare.GetAreaIn(square, 1.0), square.GetAreaIn(1.0))

def testEnums(self):
# ShapeKind is a plain (unscoped) enum wrapped as a first-class entity.
# Being unscoped, .export_values() also exposes the enumerators directly.
Expand Down
3 changes: 3 additions & 0 deletions examples/shapes/wrapper/primitives/UnitSquare.cppwg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ void register_UnitSquare_class(py::module &m)
.def("GetArea",
(double(UnitSquare::*)() const) &UnitSquare::GetArea,
" ")
.def("GetAreaIn",
(double(UnitSquare::*)(double) const) &UnitSquare::GetAreaIn,
" ", py::arg("perSquareMetre"))
.def("GetAreaIn_SquareMetres", &UnitSquare::GetAreaIn<SquareMetres>)
.def("GetAreaIn_SquareFeet", &UnitSquare::GetAreaIn<SquareFeet>)
;
Expand Down
Loading