From 2135c1042e65bcc0f1b9210e61d47546196067f6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 4 Nov 2026 10:44:09 -0700 Subject: [PATCH] Add a SymPy-style base Printer class Printer dispatches like sympy.printing.Printer: to the object's printmethod, then to the first _print_ method along the MRO, then to emptyPrinter. To match SymPy's dispatch for functions, rename FunctionSymbol to AppliedUndef and keep FunctionSymbol as an alias, so that _print_AppliedUndef is found for undefined functions. A PyFunction is dispatched on its name and on the "Base" parent classes of the wrapped SymPy class, and not as an AppliedUndef. --- symengine/__init__.py | 2 +- symengine/lib/symengine_wrapper.in.pyx | 5 +- symengine/printing.py | 54 +++++++++++- symengine/tests/test_printing.py | 110 +++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 3 deletions(-) diff --git a/symengine/__init__.py b/symengine/__init__.py index 07e2b31fa..4d26ade6f 100644 --- a/symengine/__init__.py +++ b/symengine/__init__.py @@ -27,6 +27,7 @@ Gt, Lt, And, Or, Not, Nand, Nor, Xor, Xnor, perfect_power, integer_nthroot, isprime, sqrt_mod, Expr, cse, count_ops, ccode, Piecewise, Contains, Interval, FiniteSet, linsolve, + AppliedUndef, FunctionSymbol, golden_ratio as GoldenRatio, catalan as Catalan, @@ -38,7 +39,6 @@ from .printing import init_printing -AppliedUndef = FunctionSymbol # an alias EmptySet = wrapper.S.EmptySet UniversalSet = wrapper.S.UniversalSet Reals = wrapper.S.Reals diff --git a/symengine/lib/symengine_wrapper.in.pyx b/symengine/lib/symengine_wrapper.in.pyx index b37892a5c..5548e9a41 100644 --- a/symengine/lib/symengine_wrapper.in.pyx +++ b/symengine/lib/symengine_wrapper.in.pyx @@ -2951,7 +2951,7 @@ class Abs(OneArgFunction): def func(self): return self.__class__ -class FunctionSymbol(Function): +class AppliedUndef(Function): def get_name(Basic self): cdef RCP[const symengine.FunctionSymbol] X = \ @@ -2978,6 +2978,9 @@ class FunctionSymbol(Function): return function_symbol(name, *values) +FunctionSymbol = AppliedUndef # an alias + + class UndefFunction(object): def __init__(self, name): self.name = name diff --git a/symengine/printing.py b/symengine/printing.py index 4d16d0f26..6dc49a5d5 100644 --- a/symengine/printing.py +++ b/symengine/printing.py @@ -1,4 +1,56 @@ -from .lib.symengine_wrapper import ccode, sympify, Basic, repr_latex as _repr_latex +from .lib.symengine_wrapper import (ccode, sympify, Basic, PyFunction, + repr_latex as _repr_latex) + + +def _dispatch_names(expr): + names = [cls.__name__ for cls in type(expr).__mro__] + if isinstance(expr, PyFunction): + # Like sympy, a wrapped sympy Function subclass is dispatched on its + # name, then on its parent classes ending in "Base", and is not an + # undefined function, even though PyFunction subclasses AppliedUndef + names.remove("AppliedUndef") + bases = [cls.__name__ for cls in type(expr.pyobject()).__mro__ + if cls.__name__.endswith("Base")] + return [expr.get_name()] + bases + names + return names + + +class Printer: + """ + Base class for printers, modelled after ``sympy.printing.Printer``. + + ``doprint(expr)`` returns the printed form of ``expr``. Printing a node + is dispatched, in order, to + + 1. ``expr.(printer)`` if ``expr`` defines it, + 2. the first ``_print_`` method found along ``type(expr).__mro__``, + 3. ``emptyPrinter(expr)``. + + As in SymPy, an undefined function like ``Function("f")(x)`` is printed + with ``_print_AppliedUndef`` or ``_print_Function``, never with a method + named after the function. + A function converted from a SymPy ``Function`` subclass ``f`` is printed + with ``_print_f`` if defined, before the methods for ``PyFunction``. + """ + + printmethod = None + + def doprint(self, expr): + return self._print(expr) + + def _print(self, expr, **kwargs): + if (self.printmethod and not isinstance(expr, type) + and hasattr(expr, self.printmethod)): + return getattr(expr, self.printmethod)(self, **kwargs) + for name in _dispatch_names(expr): + method = getattr(self, "_print_" + name, None) + if method is not None: + return method(expr, **kwargs) + return self.emptyPrinter(expr) + + def emptyPrinter(self, expr): + return str(expr) + class CCodePrinter: diff --git a/symengine/tests/test_printing.py b/symengine/tests/test_printing.py index e0c428169..aed5dd21a 100644 --- a/symengine/tests/test_printing.py +++ b/symengine/tests/test_printing.py @@ -36,3 +36,113 @@ def test_unicode(): x = Symbol("x") y = Integer(2) assert unicode(x / 2) == "x\n―\n2" + + +def test_printer_dispatch(): + from symengine import Rational + from symengine.printing import Printer + + class MyPrinter(Printer): + def _print_Basic(self, expr): + return "Basic" + + def _print_Integer(self, expr): + return "Integer" + + def _print_list(self, expr): + return "[" + ", ".join(self._print(e) for e in expr) + "]" + + p = MyPrinter() + assert p.doprint(Integer(2)) == "Integer" + assert p.doprint(Rational(1, 2)) == "Basic" + assert p.doprint([Integer(2), Symbol("x")]) == "[Integer, Basic]" + assert p.doprint("abc") == "abc" + + +def test_printmethod(): + from symengine.printing import Printer + + class MyPrinter(Printer): + printmethod = "_myprint" + + class Wrapped: + def _myprint(self, printer): + return "W(" + printer._print(Symbol("x")) + ")" + + assert MyPrinter().doprint(Wrapped()) == "W(x)" + + +def test_printer_function_symbol(): + from symengine import function_symbol + from symengine.printing import Printer + + x = Symbol("x") + + class MyPrinter(Printer): + def _print_gamma(self, expr): + return "gamma" + + def _print_Function(self, expr): + return "Function" + + # Like sympy, an undefined function is not dispatched on its name + assert MyPrinter().doprint(function_symbol("gamma", x)) == "Function" + + class MyPrinter2(MyPrinter): + def _print_AppliedUndef(self, expr): + return "AppliedUndef " + expr.get_name() + + assert MyPrinter2().doprint(function_symbol("f", x)) == "AppliedUndef f" + + +def test_applied_undef(): + from symengine import AppliedUndef, FunctionSymbol, function_symbol + + f = function_symbol("f", Symbol("x")) + assert FunctionSymbol is AppliedUndef + assert type(f) is AppliedUndef + assert type(f).__name__ == "AppliedUndef" + + +def test_printer_pyfunction(): + try: + import sympy + except ImportError: + return + from symengine import sympify + from symengine.printing import Printer + + class f(sympy.Function): + pass + + class g(f): + pass + + class fBase(sympy.Function): + pass + + class h(fBase): + pass + + x = sympy.Symbol("x") + + class MyPrinter(Printer): + def _print_f(self, expr): + return "f" + + def _print_fBase(self, expr): + return "fBase" + + def _print_PyFunction(self, expr): + return "PyFunction " + expr.get_name() + + p = MyPrinter() + assert p.doprint(sympify(f(x))) == "f" + # Like sympy, parent classes with a different name are skipped ... + assert p.doprint(sympify(g(x))) == "PyFunction g" + # ... unless their name ends in "Base" + assert p.doprint(sympify(h(x))) == "fBase" + # A wrapped sympy function is not an undefined function + MyPrinter._print_AppliedUndef = lambda self, expr: "AppliedUndef" + del MyPrinter._print_PyFunction + assert p.doprint(sympify(g(x))) == "g(x)"