Coverage for /usr/lib/python3/dist-packages/sympy/printing/printer.py: 72%

105 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1"""Printing subsystem driver 

2 

3SymPy's printing system works the following way: Any expression can be 

4passed to a designated Printer who then is responsible to return an 

5adequate representation of that expression. 

6 

7**The basic concept is the following:** 

8 

91. Let the object print itself if it knows how. 

102. Take the best fitting method defined in the printer. 

113. As fall-back use the emptyPrinter method for the printer. 

12 

13Which Method is Responsible for Printing? 

14^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

15 

16The whole printing process is started by calling ``.doprint(expr)`` on the printer 

17which you want to use. This method looks for an appropriate method which can 

18print the given expression in the given style that the printer defines. 

19While looking for the method, it follows these steps: 

20 

211. **Let the object print itself if it knows how.** 

22 

23 The printer looks for a specific method in every object. The name of that method 

24 depends on the specific printer and is defined under ``Printer.printmethod``. 

25 For example, StrPrinter calls ``_sympystr`` and LatexPrinter calls ``_latex``. 

26 Look at the documentation of the printer that you want to use. 

27 The name of the method is specified there. 

28 

29 This was the original way of doing printing in sympy. Every class had 

30 its own latex, mathml, str and repr methods, but it turned out that it 

31 is hard to produce a high quality printer, if all the methods are spread 

32 out that far. Therefore all printing code was combined into the different 

33 printers, which works great for built-in SymPy objects, but not that 

34 good for user defined classes where it is inconvenient to patch the 

35 printers. 

36 

372. **Take the best fitting method defined in the printer.** 

38 

39 The printer loops through expr classes (class + its bases), and tries 

40 to dispatch the work to ``_print_<EXPR_CLASS>`` 

41 

42 e.g., suppose we have the following class hierarchy:: 

43 

44 Basic 

45 | 

46 Atom 

47 | 

48 Number 

49 | 

50 Rational 

51 

52 then, for ``expr=Rational(...)``, the Printer will try 

53 to call printer methods in the order as shown in the figure below:: 

54 

55 p._print(expr) 

56 | 

57 |-- p._print_Rational(expr) 

58 | 

59 |-- p._print_Number(expr) 

60 | 

61 |-- p._print_Atom(expr) 

62 | 

63 `-- p._print_Basic(expr) 

64 

65 if ``._print_Rational`` method exists in the printer, then it is called, 

66 and the result is returned back. Otherwise, the printer tries to call 

67 ``._print_Number`` and so on. 

68 

693. **As a fall-back use the emptyPrinter method for the printer.** 

70 

71 As fall-back ``self.emptyPrinter`` will be called with the expression. If 

72 not defined in the Printer subclass this will be the same as ``str(expr)``. 

73 

74.. _printer_example: 

75 

76Example of Custom Printer 

77^^^^^^^^^^^^^^^^^^^^^^^^^ 

78 

79In the example below, we have a printer which prints the derivative of a function 

80in a shorter form. 

81 

82.. code-block:: python 

83 

84 from sympy.core.symbol import Symbol 

85 from sympy.printing.latex import LatexPrinter, print_latex 

86 from sympy.core.function import UndefinedFunction, Function 

87 

88 

89 class MyLatexPrinter(LatexPrinter): 

90 \"\"\"Print derivative of a function of symbols in a shorter form. 

91 \"\"\" 

92 def _print_Derivative(self, expr): 

93 function, *vars = expr.args 

94 if not isinstance(type(function), UndefinedFunction) or \\ 

95 not all(isinstance(i, Symbol) for i in vars): 

96 return super()._print_Derivative(expr) 

97 

98 # If you want the printer to work correctly for nested 

99 # expressions then use self._print() instead of str() or latex(). 

100 # See the example of nested modulo below in the custom printing 

101 # method section. 

102 return "{}_{{{}}}".format( 

103 self._print(Symbol(function.func.__name__)), 

104 ''.join(self._print(i) for i in vars)) 

105 

106 

107 def print_my_latex(expr): 

108 \"\"\" Most of the printers define their own wrappers for print(). 

109 These wrappers usually take printer settings. Our printer does not have 

110 any settings. 

111 \"\"\" 

112 print(MyLatexPrinter().doprint(expr)) 

113 

114 

115 y = Symbol("y") 

116 x = Symbol("x") 

117 f = Function("f") 

118 expr = f(x, y).diff(x, y) 

119 

120 # Print the expression using the normal latex printer and our custom 

121 # printer. 

122 print_latex(expr) 

123 print_my_latex(expr) 

124 

125The output of the code above is:: 

126 

127 \\frac{\\partial^{2}}{\\partial x\\partial y} f{\\left(x,y \\right)} 

128 f_{xy} 

129 

130.. _printer_method_example: 

131 

132Example of Custom Printing Method 

133^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

134 

135In the example below, the latex printing of the modulo operator is modified. 

136This is done by overriding the method ``_latex`` of ``Mod``. 

137 

138>>> from sympy import Symbol, Mod, Integer, print_latex 

139 

140>>> # Always use printer._print() 

141>>> class ModOp(Mod): 

142... def _latex(self, printer): 

143... a, b = [printer._print(i) for i in self.args] 

144... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) 

145 

146Comparing the output of our custom operator to the builtin one: 

147 

148>>> x = Symbol('x') 

149>>> m = Symbol('m') 

150>>> print_latex(Mod(x, m)) 

151x \\bmod m 

152>>> print_latex(ModOp(x, m)) 

153\\operatorname{Mod}{\\left(x, m\\right)} 

154 

155Common mistakes 

156~~~~~~~~~~~~~~~ 

157It's important to always use ``self._print(obj)`` to print subcomponents of 

158an expression when customizing a printer. Mistakes include: 

159 

1601. Using ``self.doprint(obj)`` instead: 

161 

162 >>> # This example does not work properly, as only the outermost call may use 

163 >>> # doprint. 

164 >>> class ModOpModeWrong(Mod): 

165 ... def _latex(self, printer): 

166 ... a, b = [printer.doprint(i) for i in self.args] 

167 ... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) 

168 

169 This fails when the ``mode`` argument is passed to the printer: 

170 

171 >>> print_latex(ModOp(x, m), mode='inline') # ok 

172 $\\operatorname{Mod}{\\left(x, m\\right)}$ 

173 >>> print_latex(ModOpModeWrong(x, m), mode='inline') # bad 

174 $\\operatorname{Mod}{\\left($x$, $m$\\right)}$ 

175 

1762. Using ``str(obj)`` instead: 

177 

178 >>> class ModOpNestedWrong(Mod): 

179 ... def _latex(self, printer): 

180 ... a, b = [str(i) for i in self.args] 

181 ... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) 

182 

183 This fails on nested objects: 

184 

185 >>> # Nested modulo. 

186 >>> print_latex(ModOp(ModOp(x, m), Integer(7))) # ok 

187 \\operatorname{Mod}{\\left(\\operatorname{Mod}{\\left(x, m\\right)}, 7\\right)} 

188 >>> print_latex(ModOpNestedWrong(ModOpNestedWrong(x, m), Integer(7))) # bad 

189 \\operatorname{Mod}{\\left(ModOpNestedWrong(x, m), 7\\right)} 

190 

1913. Using ``LatexPrinter()._print(obj)`` instead. 

192 

193 >>> from sympy.printing.latex import LatexPrinter 

194 >>> class ModOpSettingsWrong(Mod): 

195 ... def _latex(self, printer): 

196 ... a, b = [LatexPrinter()._print(i) for i in self.args] 

197 ... return r"\\operatorname{Mod}{\\left(%s, %s\\right)}" % (a, b) 

198 

199 This causes all the settings to be discarded in the subobjects. As an 

200 example, the ``full_prec`` setting which shows floats to full precision is 

201 ignored: 

202 

203 >>> from sympy import Float 

204 >>> print_latex(ModOp(Float(1) * x, m), full_prec=True) # ok 

205 \\operatorname{Mod}{\\left(1.00000000000000 x, m\\right)} 

206 >>> print_latex(ModOpSettingsWrong(Float(1) * x, m), full_prec=True) # bad 

207 \\operatorname{Mod}{\\left(1.0 x, m\\right)} 

208 

209""" 

210 

211from __future__ import annotations 

212import sys 

213from typing import Any, Type 

214import inspect 

215from contextlib import contextmanager 

216from functools import cmp_to_key, update_wrapper 

217 

218from sympy.core.add import Add 

219from sympy.core.basic import Basic 

220 

221from sympy.core.function import AppliedUndef, UndefinedFunction, Function 

222 

223 

224 

225@contextmanager 

226def printer_context(printer, **kwargs): 

227 original = printer._context.copy() 

228 try: 

229 printer._context.update(kwargs) 

230 yield 

231 finally: 

232 printer._context = original 

233 

234 

235class Printer: 

236 """ Generic printer 

237 

238 Its job is to provide infrastructure for implementing new printers easily. 

239 

240 If you want to define your custom Printer or your custom printing method 

241 for your custom class then see the example above: printer_example_ . 

242 """ 

243 

244 _global_settings: dict[str, Any] = {} 

245 

246 _default_settings: dict[str, Any] = {} 

247 

248 printmethod = None # type: str 

249 

250 @classmethod 

251 def _get_initial_settings(cls): 

252 settings = cls._default_settings.copy() 

253 for key, val in cls._global_settings.items(): 

254 if key in cls._default_settings: 

255 settings[key] = val 

256 return settings 

257 

258 def __init__(self, settings=None): 

259 self._str = str 

260 

261 self._settings = self._get_initial_settings() 

262 self._context = {} # mutable during printing 

263 

264 if settings is not None: 

265 self._settings.update(settings) 

266 

267 if len(self._settings) > len(self._default_settings): 

268 for key in self._settings: 

269 if key not in self._default_settings: 

270 raise TypeError("Unknown setting '%s'." % key) 

271 

272 # _print_level is the number of times self._print() was recursively 

273 # called. See StrPrinter._print_Float() for an example of usage 

274 self._print_level = 0 

275 

276 @classmethod 

277 def set_global_settings(cls, **settings): 

278 """Set system-wide printing settings. """ 

279 for key, val in settings.items(): 

280 if val is not None: 

281 cls._global_settings[key] = val 

282 

283 @property 

284 def order(self): 

285 if 'order' in self._settings: 

286 return self._settings['order'] 

287 else: 

288 raise AttributeError("No order defined.") 

289 

290 def doprint(self, expr): 

291 """Returns printer's representation for expr (as a string)""" 

292 return self._str(self._print(expr)) 

293 

294 def _print(self, expr, **kwargs) -> str: 

295 """Internal dispatcher 

296 

297 Tries the following concepts to print an expression: 

298 1. Let the object print itself if it knows how. 

299 2. Take the best fitting method defined in the printer. 

300 3. As fall-back use the emptyPrinter method for the printer. 

301 """ 

302 self._print_level += 1 

303 try: 

304 # If the printer defines a name for a printing method 

305 # (Printer.printmethod) and the object knows for itself how it 

306 # should be printed, use that method. 

307 if self.printmethod and hasattr(expr, self.printmethod): 

308 if not (isinstance(expr, type) and issubclass(expr, Basic)): 

309 return getattr(expr, self.printmethod)(self, **kwargs) 

310 

311 # See if the class of expr is known, or if one of its super 

312 # classes is known, and use that print function 

313 # Exception: ignore the subclasses of Undefined, so that, e.g., 

314 # Function('gamma') does not get dispatched to _print_gamma 

315 classes = type(expr).__mro__ 

316 if AppliedUndef in classes: 

317 classes = classes[classes.index(AppliedUndef):] 

318 if UndefinedFunction in classes: 

319 classes = classes[classes.index(UndefinedFunction):] 

320 # Another exception: if someone subclasses a known function, e.g., 

321 # gamma, and changes the name, then ignore _print_gamma 

322 if Function in classes: 

323 i = classes.index(Function) 

324 classes = tuple(c for c in classes[:i] if \ 

325 c.__name__ == classes[0].__name__ or \ 

326 c.__name__.endswith("Base")) + classes[i:] 

327 for cls in classes: 

328 printmethodname = '_print_' + cls.__name__ 

329 printmethod = getattr(self, printmethodname, None) 

330 if printmethod is not None: 

331 return printmethod(expr, **kwargs) 

332 # Unknown object, fall back to the emptyPrinter. 

333 return self.emptyPrinter(expr) 

334 finally: 

335 self._print_level -= 1 

336 

337 def emptyPrinter(self, expr): 

338 return str(expr) 

339 

340 def _as_ordered_terms(self, expr, order=None): 

341 """A compatibility function for ordering terms in Add. """ 

342 order = order or self.order 

343 

344 if order == 'old': 

345 return sorted(Add.make_args(expr), key=cmp_to_key(Basic._compare_pretty)) 

346 elif order == 'none': 

347 return list(expr.args) 

348 else: 

349 return expr.as_ordered_terms(order=order) 

350 

351 

352class _PrintFunction: 

353 """ 

354 Function wrapper to replace ``**settings`` in the signature with printer defaults 

355 """ 

356 def __init__(self, f, print_cls: Type[Printer]): 

357 # find all the non-setting arguments 

358 params = list(inspect.signature(f).parameters.values()) 

359 assert params.pop(-1).kind == inspect.Parameter.VAR_KEYWORD 

360 self.__other_params = params 

361 

362 # define the __globals__ attribute to fix Debian bug #980707 

363 # which touches the package octave-symbolic 

364 # 

365 if hasattr(f, "__globals__"): 

366 self.__globals__ = f.__globals__ 

367 else: 

368 self.__globals__ = [] 

369 

370 self.__print_cls = print_cls 

371 update_wrapper(self, f) 

372 

373 def __reduce__(self): 

374 # Since this is used as a decorator, it replaces the original function. 

375 # The default pickling will try to pickle self.__wrapped__ and fail 

376 # because the wrapped function can't be retrieved by name. 

377 return self.__wrapped__.__qualname__ 

378 

379 def __call__(self, *args, **kwargs): 

380 return self.__wrapped__(*args, **kwargs) 

381 

382 @property 

383 def __signature__(self) -> inspect.Signature: 

384 settings = self.__print_cls._get_initial_settings() 

385 return inspect.Signature( 

386 parameters=self.__other_params + [ 

387 inspect.Parameter(k, inspect.Parameter.KEYWORD_ONLY, default=v) 

388 for k, v in settings.items() 

389 ], 

390 return_annotation=self.__wrapped__.__annotations__.get('return', inspect.Signature.empty) # type:ignore 

391 ) 

392 

393 

394def print_function(print_cls): 

395 """ A decorator to replace kwargs with the printer settings in __signature__ """ 

396 def decorator(f): 

397 if sys.version_info < (3, 9): 

398 # We have to create a subclass so that `help` actually shows the docstring in older Python versions. 

399 # IPython and Sphinx do not need this, only a raw Python console. 

400 cls = type(f'{f.__qualname__}_PrintFunction', (_PrintFunction,), {"__doc__": f.__doc__}) 

401 else: 

402 cls = _PrintFunction 

403 return cls(f, print_cls) 

404 return decorator