Coverage for /usr/lib/python3/dist-packages/sympy/printing/pretty/stringpict.py: 16%
258 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Prettyprinter by Jurjen Bos.
2(I hate spammers: mail me at pietjepuk314 at the reverse of ku.oc.oohay).
3All objects have a method that create a "stringPict",
4that can be used in the str method for pretty printing.
6Updates by Jason Gedge (email <my last name> at cs mun ca)
7 - terminal_string() method
8 - minor fixes and changes (mostly to prettyForm)
10TODO:
11 - Allow left/center/right alignment options for above/below and
12 top/center/bottom alignment options for left/right
13"""
15from .pretty_symbology import hobj, vobj, xsym, xobj, pretty_use_unicode, line_width
16from sympy.utilities.exceptions import sympy_deprecation_warning
18class stringPict:
19 """An ASCII picture.
20 The pictures are represented as a list of equal length strings.
21 """
22 #special value for stringPict.below
23 LINE = 'line'
25 def __init__(self, s, baseline=0):
26 """Initialize from string.
27 Multiline strings are centered.
28 """
29 self.s = s
30 #picture is a string that just can be printed
31 self.picture = stringPict.equalLengths(s.splitlines())
32 #baseline is the line number of the "base line"
33 self.baseline = baseline
34 self.binding = None
36 @staticmethod
37 def equalLengths(lines):
38 # empty lines
39 if not lines:
40 return ['']
42 width = max(line_width(line) for line in lines)
43 return [line.center(width) for line in lines]
45 def height(self):
46 """The height of the picture in characters."""
47 return len(self.picture)
49 def width(self):
50 """The width of the picture in characters."""
51 return line_width(self.picture[0])
53 @staticmethod
54 def next(*args):
55 """Put a string of stringPicts next to each other.
56 Returns string, baseline arguments for stringPict.
57 """
58 #convert everything to stringPicts
59 objects = []
60 for arg in args:
61 if isinstance(arg, str):
62 arg = stringPict(arg)
63 objects.append(arg)
65 #make a list of pictures, with equal height and baseline
66 newBaseline = max(obj.baseline for obj in objects)
67 newHeightBelowBaseline = max(
68 obj.height() - obj.baseline
69 for obj in objects)
70 newHeight = newBaseline + newHeightBelowBaseline
72 pictures = []
73 for obj in objects:
74 oneEmptyLine = [' '*obj.width()]
75 basePadding = newBaseline - obj.baseline
76 totalPadding = newHeight - obj.height()
77 pictures.append(
78 oneEmptyLine * basePadding +
79 obj.picture +
80 oneEmptyLine * (totalPadding - basePadding))
82 result = [''.join(lines) for lines in zip(*pictures)]
83 return '\n'.join(result), newBaseline
85 def right(self, *args):
86 r"""Put pictures next to this one.
87 Returns string, baseline arguments for stringPict.
88 (Multiline) strings are allowed, and are given a baseline of 0.
90 Examples
91 ========
93 >>> from sympy.printing.pretty.stringpict import stringPict
94 >>> print(stringPict("10").right(" + ",stringPict("1\r-\r2",1))[0])
95 1
96 10 + -
97 2
99 """
100 return stringPict.next(self, *args)
102 def left(self, *args):
103 """Put pictures (left to right) at left.
104 Returns string, baseline arguments for stringPict.
105 """
106 return stringPict.next(*(args + (self,)))
108 @staticmethod
109 def stack(*args):
110 """Put pictures on top of each other,
111 from top to bottom.
112 Returns string, baseline arguments for stringPict.
113 The baseline is the baseline of the second picture.
114 Everything is centered.
115 Baseline is the baseline of the second picture.
116 Strings are allowed.
117 The special value stringPict.LINE is a row of '-' extended to the width.
118 """
119 #convert everything to stringPicts; keep LINE
120 objects = []
121 for arg in args:
122 if arg is not stringPict.LINE and isinstance(arg, str):
123 arg = stringPict(arg)
124 objects.append(arg)
126 #compute new width
127 newWidth = max(
128 obj.width()
129 for obj in objects
130 if obj is not stringPict.LINE)
132 lineObj = stringPict(hobj('-', newWidth))
134 #replace LINE with proper lines
135 for i, obj in enumerate(objects):
136 if obj is stringPict.LINE:
137 objects[i] = lineObj
139 #stack the pictures, and center the result
140 newPicture = []
141 for obj in objects:
142 newPicture.extend(obj.picture)
143 newPicture = [line.center(newWidth) for line in newPicture]
144 newBaseline = objects[0].height() + objects[1].baseline
145 return '\n'.join(newPicture), newBaseline
147 def below(self, *args):
148 """Put pictures under this picture.
149 Returns string, baseline arguments for stringPict.
150 Baseline is baseline of top picture
152 Examples
153 ========
155 >>> from sympy.printing.pretty.stringpict import stringPict
156 >>> print(stringPict("x+3").below(
157 ... stringPict.LINE, '3')[0]) #doctest: +NORMALIZE_WHITESPACE
158 x+3
159 ---
160 3
162 """
163 s, baseline = stringPict.stack(self, *args)
164 return s, self.baseline
166 def above(self, *args):
167 """Put pictures above this picture.
168 Returns string, baseline arguments for stringPict.
169 Baseline is baseline of bottom picture.
170 """
171 string, baseline = stringPict.stack(*(args + (self,)))
172 baseline = len(string.splitlines()) - self.height() + self.baseline
173 return string, baseline
175 def parens(self, left='(', right=')', ifascii_nougly=False):
176 """Put parentheses around self.
177 Returns string, baseline arguments for stringPict.
179 left or right can be None or empty string which means 'no paren from
180 that side'
181 """
182 h = self.height()
183 b = self.baseline
185 # XXX this is a hack -- ascii parens are ugly!
186 if ifascii_nougly and not pretty_use_unicode():
187 h = 1
188 b = 0
190 res = self
192 if left:
193 lparen = stringPict(vobj(left, h), baseline=b)
194 res = stringPict(*lparen.right(self))
195 if right:
196 rparen = stringPict(vobj(right, h), baseline=b)
197 res = stringPict(*res.right(rparen))
199 return ('\n'.join(res.picture), res.baseline)
201 def leftslash(self):
202 """Precede object by a slash of the proper size.
203 """
204 # XXX not used anywhere ?
205 height = max(
206 self.baseline,
207 self.height() - 1 - self.baseline)*2 + 1
208 slash = '\n'.join(
209 ' '*(height - i - 1) + xobj('/', 1) + ' '*i
210 for i in range(height)
211 )
212 return self.left(stringPict(slash, height//2))
214 def root(self, n=None):
215 """Produce a nice root symbol.
216 Produces ugly results for big n inserts.
217 """
218 # XXX not used anywhere
219 # XXX duplicate of root drawing in pretty.py
220 #put line over expression
221 result = self.above('_'*self.width())
222 #construct right half of root symbol
223 height = self.height()
224 slash = '\n'.join(
225 ' ' * (height - i - 1) + '/' + ' ' * i
226 for i in range(height)
227 )
228 slash = stringPict(slash, height - 1)
229 #left half of root symbol
230 if height > 2:
231 downline = stringPict('\\ \n \\', 1)
232 else:
233 downline = stringPict('\\')
234 #put n on top, as low as possible
235 if n is not None and n.width() > downline.width():
236 downline = downline.left(' '*(n.width() - downline.width()))
237 downline = downline.above(n)
238 #build root symbol
239 root = downline.right(slash)
240 #glue it on at the proper height
241 #normally, the root symbel is as high as self
242 #which is one less than result
243 #this moves the root symbol one down
244 #if the root became higher, the baseline has to grow too
245 root.baseline = result.baseline - result.height() + root.height()
246 return result.left(root)
248 def render(self, * args, **kwargs):
249 """Return the string form of self.
251 Unless the argument line_break is set to False, it will
252 break the expression in a form that can be printed
253 on the terminal without being broken up.
254 """
255 if kwargs["wrap_line"] is False:
256 return "\n".join(self.picture)
258 if kwargs["num_columns"] is not None:
259 # Read the argument num_columns if it is not None
260 ncols = kwargs["num_columns"]
261 else:
262 # Attempt to get a terminal width
263 ncols = self.terminal_width()
265 ncols -= 2
266 if ncols <= 0:
267 ncols = 78
269 # If smaller than the terminal width, no need to correct
270 if self.width() <= ncols:
271 return type(self.picture[0])(self)
273 # for one-line pictures we don't need v-spacers. on the other hand, for
274 # multiline-pictures, we need v-spacers between blocks, compare:
275 #
276 # 2 2 3 | a*c*e + a*c*f + a*d | a*c*e + a*c*f + a*d | 3.14159265358979323
277 # 6*x *y + 4*x*y + | | *e + a*d*f + b*c*e | 84626433832795
278 # | *e + a*d*f + b*c*e | + b*c*f + b*d*e + b |
279 # 3 4 4 | | *d*f |
280 # 4*y*x + x + y | + b*c*f + b*d*e + b | |
281 # | | |
282 # | *d*f
284 i = 0
285 svals = []
286 do_vspacers = (self.height() > 1)
287 while i < self.width():
288 svals.extend([ sval[i:i + ncols] for sval in self.picture ])
289 if do_vspacers:
290 svals.append("") # a vertical spacer
291 i += ncols
293 if svals[-1] == '':
294 del svals[-1] # Get rid of the last spacer
296 return "\n".join(svals)
298 def terminal_width(self):
299 """Return the terminal width if possible, otherwise return 0.
300 """
301 ncols = 0
302 try:
303 import curses
304 import io
305 try:
306 curses.setupterm()
307 ncols = curses.tigetnum('cols')
308 except AttributeError:
309 # windows curses doesn't implement setupterm or tigetnum
310 # code below from
311 # https://code.activestate.com/recipes/440694/
312 from ctypes import windll, create_string_buffer
313 # stdin handle is -10
314 # stdout handle is -11
315 # stderr handle is -12
316 h = windll.kernel32.GetStdHandle(-12)
317 csbi = create_string_buffer(22)
318 res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
319 if res:
320 import struct
321 (bufx, bufy, curx, cury, wattr,
322 left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
323 ncols = right - left + 1
324 except curses.error:
325 pass
326 except io.UnsupportedOperation:
327 pass
328 except (ImportError, TypeError):
329 pass
330 return ncols
332 def __eq__(self, o):
333 if isinstance(o, str):
334 return '\n'.join(self.picture) == o
335 elif isinstance(o, stringPict):
336 return o.picture == self.picture
337 return False
339 def __hash__(self):
340 return super().__hash__()
342 def __str__(self):
343 return '\n'.join(self.picture)
345 def __repr__(self):
346 return "stringPict(%r,%d)" % ('\n'.join(self.picture), self.baseline)
348 def __getitem__(self, index):
349 return self.picture[index]
351 def __len__(self):
352 return len(self.s)
355class prettyForm(stringPict):
356 """
357 Extension of the stringPict class that knows about basic math applications,
358 optimizing double minus signs.
360 "Binding" is interpreted as follows::
362 ATOM this is an atom: never needs to be parenthesized
363 FUNC this is a function application: parenthesize if added (?)
364 DIV this is a division: make wider division if divided
365 POW this is a power: only parenthesize if exponent
366 MUL this is a multiplication: parenthesize if powered
367 ADD this is an addition: parenthesize if multiplied or powered
368 NEG this is a negative number: optimize if added, parenthesize if
369 multiplied or powered
370 OPEN this is an open object: parenthesize if added, multiplied, or
371 powered (example: Piecewise)
372 """
373 ATOM, FUNC, DIV, POW, MUL, ADD, NEG, OPEN = range(8)
375 def __init__(self, s, baseline=0, binding=0, unicode=None):
376 """Initialize from stringPict and binding power."""
377 stringPict.__init__(self, s, baseline)
378 self.binding = binding
379 if unicode is not None:
380 sympy_deprecation_warning(
381 """
382 The unicode argument to prettyForm is deprecated. Only the s
383 argument (the first positional argument) should be passed.
384 """,
385 deprecated_since_version="1.7",
386 active_deprecations_target="deprecated-pretty-printing-functions")
387 self._unicode = unicode or s
389 @property
390 def unicode(self):
391 sympy_deprecation_warning(
392 """
393 The prettyForm.unicode attribute is deprecated. Use the
394 prettyForm.s attribute instead.
395 """,
396 deprecated_since_version="1.7",
397 active_deprecations_target="deprecated-pretty-printing-functions")
398 return self._unicode
400 # Note: code to handle subtraction is in _print_Add
402 def __add__(self, *others):
403 """Make a pretty addition.
404 Addition of negative numbers is simplified.
405 """
406 arg = self
407 if arg.binding > prettyForm.NEG:
408 arg = stringPict(*arg.parens())
409 result = [arg]
410 for arg in others:
411 #add parentheses for weak binders
412 if arg.binding > prettyForm.NEG:
413 arg = stringPict(*arg.parens())
414 #use existing minus sign if available
415 if arg.binding != prettyForm.NEG:
416 result.append(' + ')
417 result.append(arg)
418 return prettyForm(binding=prettyForm.ADD, *stringPict.next(*result))
420 def __truediv__(self, den, slashed=False):
421 """Make a pretty division; stacked or slashed.
422 """
423 if slashed:
424 raise NotImplementedError("Can't do slashed fraction yet")
425 num = self
426 if num.binding == prettyForm.DIV:
427 num = stringPict(*num.parens())
428 if den.binding == prettyForm.DIV:
429 den = stringPict(*den.parens())
431 if num.binding==prettyForm.NEG:
432 num = num.right(" ")[0]
434 return prettyForm(binding=prettyForm.DIV, *stringPict.stack(
435 num,
436 stringPict.LINE,
437 den))
439 def __mul__(self, *others):
440 """Make a pretty multiplication.
441 Parentheses are needed around +, - and neg.
442 """
443 quantity = {
444 'degree': "\N{DEGREE SIGN}"
445 }
447 if len(others) == 0:
448 return self # We aren't actually multiplying... So nothing to do here.
450 # add parens on args that need them
451 arg = self
452 if arg.binding > prettyForm.MUL and arg.binding != prettyForm.NEG:
453 arg = stringPict(*arg.parens())
454 result = [arg]
455 for arg in others:
456 if arg.picture[0] not in quantity.values():
457 result.append(xsym('*'))
458 #add parentheses for weak binders
459 if arg.binding > prettyForm.MUL and arg.binding != prettyForm.NEG:
460 arg = stringPict(*arg.parens())
461 result.append(arg)
463 len_res = len(result)
464 for i in range(len_res):
465 if i < len_res - 1 and result[i] == '-1' and result[i + 1] == xsym('*'):
466 # substitute -1 by -, like in -1*x -> -x
467 result.pop(i)
468 result.pop(i)
469 result.insert(i, '-')
470 if result[0][0] == '-':
471 # if there is a - sign in front of all
472 # This test was failing to catch a prettyForm.__mul__(prettyForm("-1", 0, 6)) being negative
473 bin = prettyForm.NEG
474 if result[0] == '-':
475 right = result[1]
476 if right.picture[right.baseline][0] == '-':
477 result[0] = '- '
478 else:
479 bin = prettyForm.MUL
480 return prettyForm(binding=bin, *stringPict.next(*result))
482 def __repr__(self):
483 return "prettyForm(%r,%d,%d)" % (
484 '\n'.join(self.picture),
485 self.baseline,
486 self.binding)
488 def __pow__(self, b):
489 """Make a pretty power.
490 """
491 a = self
492 use_inline_func_form = False
493 if b.binding == prettyForm.POW:
494 b = stringPict(*b.parens())
495 if a.binding > prettyForm.FUNC:
496 a = stringPict(*a.parens())
497 elif a.binding == prettyForm.FUNC:
498 # heuristic for when to use inline power
499 if b.height() > 1:
500 a = stringPict(*a.parens())
501 else:
502 use_inline_func_form = True
504 if use_inline_func_form:
505 # 2
506 # sin + + (x)
507 b.baseline = a.prettyFunc.baseline + b.height()
508 func = stringPict(*a.prettyFunc.right(b))
509 return prettyForm(*func.right(a.prettyArgs))
510 else:
511 # 2 <-- top
512 # (x+y) <-- bot
513 top = stringPict(*b.left(' '*a.width()))
514 bot = stringPict(*a.right(' '*b.width()))
516 return prettyForm(binding=prettyForm.POW, *bot.above(top))
518 simpleFunctions = ["sin", "cos", "tan"]
520 @staticmethod
521 def apply(function, *args):
522 """Functions of one or more variables.
523 """
524 if function in prettyForm.simpleFunctions:
525 #simple function: use only space if possible
526 assert len(
527 args) == 1, "Simple function %s must have 1 argument" % function
528 arg = args[0].__pretty__()
529 if arg.binding <= prettyForm.DIV:
530 #optimization: no parentheses necessary
531 return prettyForm(binding=prettyForm.FUNC, *arg.left(function + ' '))
532 argumentList = []
533 for arg in args:
534 argumentList.append(',')
535 argumentList.append(arg.__pretty__())
536 argumentList = stringPict(*stringPict.next(*argumentList[1:]))
537 argumentList = stringPict(*argumentList.parens())
538 return prettyForm(binding=prettyForm.ATOM, *argumentList.left(function))