diff --git a/examples/cells/src/py/pycells/_syntax.py b/examples/cells/src/py/pycells/_syntax.py index a0c1f54..8173e9d 100644 --- a/examples/cells/src/py/pycells/_syntax.py +++ b/examples/cells/src/py/pycells/_syntax.py @@ -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 @@ -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). """ @@ -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 = {} @@ -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.[Arg]()`` dispatches to the - per-instantiation binding ``obj._()`` 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.[Arg]()`` subscript form to reach the per-instantiation binding + ``obj._()`` that cppwg generates. When the name is also a plain + (non-templated) overload, pass it as ``fallback`` so ``obj.(...)`` 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 __..., 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) diff --git a/examples/shapes/src/cpp/primitives/UnitSquare.hpp b/examples/shapes/src/cpp/primitives/UnitSquare.hpp index 36ef41c..cf44258 100644 --- a/examples/shapes/src/cpp/primitives/UnitSquare.hpp +++ b/examples/shapes/src/cpp/primitives/UnitSquare.hpp @@ -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 { @@ -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_ diff --git a/examples/shapes/src/py/pyshapes/_syntax.py b/examples/shapes/src/py/pyshapes/_syntax.py index 40d2de6..8173e9d 100644 --- a/examples/shapes/src/py/pyshapes/_syntax.py +++ b/examples/shapes/src/py/pyshapes/_syntax.py @@ -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 @@ -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 = {} @@ -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.[Arg]()`` dispatches to the - per-instantiation binding ``obj._()`` 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.[Arg]()`` subscript form to reach the per-instantiation binding + ``obj._()`` that cppwg generates. When the name is also a plain + (non-templated) overload, pass it as ``fallback`` so ``obj.(...)`` 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 __..., 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) diff --git a/examples/shapes/src/py/pyshapes/primitives/__init__.py b/examples/shapes/src/py/pyshapes/primitives/__init__.py index 8ca996d..872ae91 100644 --- a/examples/shapes/src/py/pyshapes/primitives/__init__.py +++ b/examples/shapes/src/py/pyshapes/primitives/__init__.py @@ -11,5 +11,7 @@ class Shape(TemplateClass): # UnitSquare::GetAreaIn() 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) diff --git a/examples/shapes/src/py/tests/test_classes.py b/examples/shapes/src/py/tests/test_classes.py index 7ae79e8..1f0a3a2 100644 --- a/examples/shapes/src/py/tests/test_classes.py +++ b/examples/shapes/src/py/tests/test_classes.py @@ -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. diff --git a/examples/shapes/wrapper/primitives/UnitSquare.cppwg.cpp b/examples/shapes/wrapper/primitives/UnitSquare.cppwg.cpp index 4cc4045..8359807 100644 --- a/examples/shapes/wrapper/primitives/UnitSquare.cppwg.cpp +++ b/examples/shapes/wrapper/primitives/UnitSquare.cppwg.cpp @@ -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) .def("GetAreaIn_SquareFeet", &UnitSquare::GetAreaIn) ;