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)
;