Spaces:
Sleeping
Sleeping
"""Printing subsystem driver | |
SymPy's printing system works the following way: Any expression can be | |
passed to a designated Printer who then is responsible to return an | |
adequate representation of that expression. | |
**The basic concept is the following:** | |
1. Let the object print itself if it knows how. | |
2. Take the best fitting method defined in the printer. | |
3. As fall-back use the emptyPrinter method for the printer. | |
Which Method is Responsible for Printing? | |
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |
The whole printing process is started by calling ``.doprint(expr)`` on the printer | |
which you want to use. This method looks for an appropriate method which can | |
print the given expression in the given style that the printer defines. | |
While looking for the method, it follows these steps: | |
1. **Let the object print itself if it knows how.** | |
The printer looks for a specific method in every object. The name of that method | |
depends on the specific printer and is defined under ``Printer.printmethod``. | |
For example, StrPrinter calls ``_sympystr`` and LatexPrinter calls ``_latex``. | |
Look at the documentation of the printer that you want to use. | |
The name of the method is specified there. | |
This was the original way of doing printing in sympy. Every class had | |
its own latex, mathml, str and repr methods, but it turned out that it | |
is hard to produce a high quality printer, if all the methods are spread | |
out that far. Therefore all printing code was combined into the different | |
printers, which works great for built-in SymPy objects, but not that | |
good for user defined classes where it is inconvenient to patch the | |
printers. | |
2. **Take the best fitting method defined in the printer.** | |
The printer loops through expr classes (class + its bases), and tries | |
to dispatch the work to ``_print_<EXPR_CLASS>`` | |
e.g., suppose we have the following class hierarchy:: | |
Basic | |
| | |
Atom | |
| | |
Number | |
| | |
Rational | |
then, for ``expr=Rational(...)``, the Printer will try | |
to call printer methods in the order as shown in the figure below:: | |
p._print(expr) | |
| | |
|-- p._print_Rational(expr) | |
| | |
|-- p._print_Number(expr) | |
| | |
|-- p._print_Atom(expr) | |
| | |
`-- p._print_Basic(expr) | |
if ``._print_Rational`` method exists in the printer, then it is called, | |
and the result is returned back. Otherwise, the printer tries to call | |
``._print_Number`` and so on. | |
3. **As a fall-back use the emptyPrinter method for the printer.** | |
As fall-back ``self.emptyPrinter`` will be called with the expression. If | |
not defined in the Printer subclass this will be the same as ``str(expr)``. | |
.. _printer_example: | |
Example of Custom Printer | |
^^^^^^^^^^^^^^^^^^^^^^^^^ | |
In the example below, we have a printer which prints the derivative of a function | |
in a shorter form. | |
.. code-block:: python | |
from sympy.core.symbol import Symbol | |
from sympy.printing.latex import LatexPrinter, print_latex | |
from sympy.core.function import UndefinedFunction, Function | |
class MyLatexPrinter(LatexPrinter): | |
\"\"\"Print derivative of a function of symbols in a shorter form. | |
\"\"\" | |
def _print_Derivative(self, expr): | |
function, *vars = expr.args | |
if not isinstance(type(function), UndefinedFunction) or \\ | |
not all(isinstance(i, Symbol) for i in vars): | |
return super()._print_Derivative(expr) | |
# If you want the printer to work correctly for nested | |
# expressions then use self._print() instead of str() or latex(). | |
# See the example of nested modulo below in the custom printing | |
# method section. | |
return "{}_{{{}}}".format( | |
self._print(Symbol(function.func.__name__)), | |
''.join(self._print(i) for i in vars)) | |
def print_my_latex(expr): | |
\"\"\" Most of the printers define their own wrappers for print(). | |
These wrappers usually take printer settings. Our printer does not have | |
any settings. | |
\"\"\" | |
print(MyLatexPrinter().doprint(expr)) | |
y = Symbol("y") | |
x = Symbol("x") | |
f = Function("f") | |
expr = f(x, y).diff(x, y) | |
# Print the expression using the normal latex printer and our custom | |
# printer. | |
print_latex(expr) | |
print_my_latex(expr) | |
The output of the code above is:: | |
\\frac{\\partial^{2}}{\\partial x\\partial y} f{\\left(x,y \\right)} | |
f_{xy} | |
.. _printer_method_example: | |
Example of Custom Printing Method | |
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |
In the example below, the latex printing of the modulo operator is modified. | |
This is done by overriding the method ``_latex`` of ``Mod``. | |
from sympy import Symbol, Mod, Integer, print_latex | |
# Always use printer._print() | |
class ModOp(Mod): | |
def _latex(self, printer): | |
a, b = [printer._print(i) for i in self.args] | |
return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) | |
Comparing the output of our custom operator to the builtin one: | |
x = Symbol('x') | |
m = Symbol('m') | |
print_latex(Mod(x, m)) | |
x \\bmod m | |
print_latex(ModOp(x, m)) | |
\\operatorname{Mod}{\\left(x, m\\right)} | |
Common mistakes | |
~~~~~~~~~~~~~~~ | |
It's important to always use ``self._print(obj)`` to print subcomponents of | |
an expression when customizing a printer. Mistakes include: | |
1. Using ``self.doprint(obj)`` instead: | |
>>> # This example does not work properly, as only the outermost call may use | |
>>> # doprint. | |
>>> class ModOpModeWrong(Mod): | |
... def _latex(self, printer): | |
... a, b = [printer.doprint(i) for i in self.args] | |
... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) | |
This fails when the ``mode`` argument is passed to the printer: | |
>>> print_latex(ModOp(x, m), mode='inline') # ok | |
$\\operatorname{Mod}{\\left(x, m\\right)}$ | |
>>> print_latex(ModOpModeWrong(x, m), mode='inline') # bad | |
$\\operatorname{Mod}{\\left($x$, $m$\\right)}$ | |
2. Using ``str(obj)`` instead: | |
>>> class ModOpNestedWrong(Mod): | |
... def _latex(self, printer): | |
... a, b = [str(i) for i in self.args] | |
... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) | |
This fails on nested objects: | |
>>> # Nested modulo. | |
>>> print_latex(ModOp(ModOp(x, m), Integer(7))) # ok | |
\\operatorname{Mod}{\\left(\\operatorname{Mod}{\\left(x, m\\right)}, 7\\right)} | |
>>> print_latex(ModOpNestedWrong(ModOpNestedWrong(x, m), Integer(7))) # bad | |
\\operatorname{Mod}{\\left(ModOpNestedWrong(x, m), 7\\right)} | |
3. Using ``LatexPrinter()._print(obj)`` instead. | |
>>> from sympy.printing.latex import LatexPrinter | |
>>> class ModOpSettingsWrong(Mod): | |
... def _latex(self, printer): | |
... a, b = [LatexPrinter()._print(i) for i in self.args] | |
... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) | |
This causes all the settings to be discarded in the subobjects. As an | |
example, the ``full_prec`` setting which shows floats to full precision is | |
ignored: | |
>>> from sympy import Float | |
>>> print_latex(ModOp(Float(1) * x, m), full_prec=True) # ok | |
\\operatorname{Mod}{\\left(1.00000000000000 x, m\\right)} | |
>>> print_latex(ModOpSettingsWrong(Float(1) * x, m), full_prec=True) # bad | |
\\operatorname{Mod}{\\left(1.0 x, m\\right)} | |
""" | |
from __future__ import annotations | |
import sys | |
from typing import Any, Type | |
import inspect | |
from contextlib import contextmanager | |
from functools import cmp_to_key, update_wrapper | |
from sympy.core.add import Add | |
from sympy.core.basic import Basic | |
from sympy.core.function import AppliedUndef, UndefinedFunction, Function | |
def printer_context(printer, **kwargs): | |
original = printer._context.copy() | |
try: | |
printer._context.update(kwargs) | |
yield | |
finally: | |
printer._context = original | |
class Printer: | |
""" Generic printer | |
Its job is to provide infrastructure for implementing new printers easily. | |
If you want to define your custom Printer or your custom printing method | |
for your custom class then see the example above: printer_example_ . | |
""" | |
_global_settings: dict[str, Any] = {} | |
_default_settings: dict[str, Any] = {} | |
printmethod = None # type: str | |
def _get_initial_settings(cls): | |
settings = cls._default_settings.copy() | |
for key, val in cls._global_settings.items(): | |
if key in cls._default_settings: | |
settings[key] = val | |
return settings | |
def __init__(self, settings=None): | |
self._str = str | |
self._settings = self._get_initial_settings() | |
self._context = {} # mutable during printing | |
if settings is not None: | |
self._settings.update(settings) | |
if len(self._settings) > len(self._default_settings): | |
for key in self._settings: | |
if key not in self._default_settings: | |
raise TypeError("Unknown setting '%s'." % key) | |
# _print_level is the number of times self._print() was recursively | |
# called. See StrPrinter._print_Float() for an example of usage | |
self._print_level = 0 | |
def set_global_settings(cls, **settings): | |
"""Set system-wide printing settings. """ | |
for key, val in settings.items(): | |
if val is not None: | |
cls._global_settings[key] = val | |
def order(self): | |
if 'order' in self._settings: | |
return self._settings['order'] | |
else: | |
raise AttributeError("No order defined.") | |
def doprint(self, expr): | |
"""Returns printer's representation for expr (as a string)""" | |
return self._str(self._print(expr)) | |
def _print(self, expr, **kwargs) -> str: | |
"""Internal dispatcher | |
Tries the following concepts to print an expression: | |
1. Let the object print itself if it knows how. | |
2. Take the best fitting method defined in the printer. | |
3. As fall-back use the emptyPrinter method for the printer. | |
""" | |
self._print_level += 1 | |
try: | |
# If the printer defines a name for a printing method | |
# (Printer.printmethod) and the object knows for itself how it | |
# should be printed, use that method. | |
if self.printmethod and hasattr(expr, self.printmethod): | |
if not (isinstance(expr, type) and issubclass(expr, Basic)): | |
return getattr(expr, self.printmethod)(self, **kwargs) | |
# See if the class of expr is known, or if one of its super | |
# classes is known, and use that print function | |
# Exception: ignore the subclasses of Undefined, so that, e.g., | |
# Function('gamma') does not get dispatched to _print_gamma | |
classes = type(expr).__mro__ | |
if AppliedUndef in classes: | |
classes = classes[classes.index(AppliedUndef):] | |
if UndefinedFunction in classes: | |
classes = classes[classes.index(UndefinedFunction):] | |
# Another exception: if someone subclasses a known function, e.g., | |
# gamma, and changes the name, then ignore _print_gamma | |
if Function in classes: | |
i = classes.index(Function) | |
classes = tuple(c for c in classes[:i] if \ | |
c.__name__ == classes[0].__name__ or \ | |
c.__name__.endswith("Base")) + classes[i:] | |
for cls in classes: | |
printmethodname = '_print_' + cls.__name__ | |
printmethod = getattr(self, printmethodname, None) | |
if printmethod is not None: | |
return printmethod(expr, **kwargs) | |
# Unknown object, fall back to the emptyPrinter. | |
return self.emptyPrinter(expr) | |
finally: | |
self._print_level -= 1 | |
def emptyPrinter(self, expr): | |
return str(expr) | |
def _as_ordered_terms(self, expr, order=None): | |
"""A compatibility function for ordering terms in Add. """ | |
order = order or self.order | |
if order == 'old': | |
return sorted(Add.make_args(expr), key=cmp_to_key(Basic._compare_pretty)) | |
elif order == 'none': | |
return list(expr.args) | |
else: | |
return expr.as_ordered_terms(order=order) | |
class _PrintFunction: | |
""" | |
Function wrapper to replace ``**settings`` in the signature with printer defaults | |
""" | |
def __init__(self, f, print_cls: Type[Printer]): | |
# find all the non-setting arguments | |
params = list(inspect.signature(f).parameters.values()) | |
assert params.pop(-1).kind == inspect.Parameter.VAR_KEYWORD | |
self.__other_params = params | |
self.__print_cls = print_cls | |
update_wrapper(self, f) | |
def __reduce__(self): | |
# Since this is used as a decorator, it replaces the original function. | |
# The default pickling will try to pickle self.__wrapped__ and fail | |
# because the wrapped function can't be retrieved by name. | |
return self.__wrapped__.__qualname__ | |
def __call__(self, *args, **kwargs): | |
return self.__wrapped__(*args, **kwargs) | |
def __signature__(self) -> inspect.Signature: | |
settings = self.__print_cls._get_initial_settings() | |
return inspect.Signature( | |
parameters=self.__other_params + [ | |
inspect.Parameter(k, inspect.Parameter.KEYWORD_ONLY, default=v) | |
for k, v in settings.items() | |
], | |
return_annotation=self.__wrapped__.__annotations__.get('return', inspect.Signature.empty) # type:ignore | |
) | |
def print_function(print_cls): | |
""" A decorator to replace kwargs with the printer settings in __signature__ """ | |
def decorator(f): | |
if sys.version_info < (3, 9): | |
# We have to create a subclass so that `help` actually shows the docstring in older Python versions. | |
# IPython and Sphinx do not need this, only a raw Python console. | |
cls = type(f'{f.__qualname__}_PrintFunction', (_PrintFunction,), {"__doc__": f.__doc__}) | |
else: | |
cls = _PrintFunction | |
return cls(f, print_cls) | |
return decorator | |