|
"""Prettyprinter by Jurjen Bos. |
|
(I hate spammers: mail me at pietjepuk314 at the reverse of ku.oc.oohay). |
|
All objects have a method that create a "stringPict", |
|
that can be used in the str method for pretty printing. |
|
|
|
Updates by Jason Gedge (email <my last name> at cs mun ca) |
|
- terminal_string() method |
|
- minor fixes and changes (mostly to prettyForm) |
|
|
|
TODO: |
|
- Allow left/center/right alignment options for above/below and |
|
top/center/bottom alignment options for left/right |
|
""" |
|
|
|
import shutil |
|
|
|
from .pretty_symbology import hobj, vobj, xsym, xobj, pretty_use_unicode, line_width, center |
|
from sympy.utilities.exceptions import sympy_deprecation_warning |
|
|
|
_GLOBAL_WRAP_LINE = None |
|
|
|
class stringPict: |
|
"""An ASCII picture. |
|
The pictures are represented as a list of equal length strings. |
|
""" |
|
|
|
LINE = 'line' |
|
|
|
def __init__(self, s, baseline=0): |
|
"""Initialize from string. |
|
Multiline strings are centered. |
|
""" |
|
self.s = s |
|
|
|
self.picture = stringPict.equalLengths(s.splitlines()) |
|
|
|
self.baseline = baseline |
|
self.binding = None |
|
|
|
@staticmethod |
|
def equalLengths(lines): |
|
|
|
if not lines: |
|
return [''] |
|
|
|
width = max(line_width(line) for line in lines) |
|
return [center(line, width) for line in lines] |
|
|
|
def height(self): |
|
"""The height of the picture in characters.""" |
|
return len(self.picture) |
|
|
|
def width(self): |
|
"""The width of the picture in characters.""" |
|
return line_width(self.picture[0]) |
|
|
|
@staticmethod |
|
def next(*args): |
|
"""Put a string of stringPicts next to each other. |
|
Returns string, baseline arguments for stringPict. |
|
""" |
|
|
|
objects = [] |
|
for arg in args: |
|
if isinstance(arg, str): |
|
arg = stringPict(arg) |
|
objects.append(arg) |
|
|
|
|
|
newBaseline = max(obj.baseline for obj in objects) |
|
newHeightBelowBaseline = max( |
|
obj.height() - obj.baseline |
|
for obj in objects) |
|
newHeight = newBaseline + newHeightBelowBaseline |
|
|
|
pictures = [] |
|
for obj in objects: |
|
oneEmptyLine = [' '*obj.width()] |
|
basePadding = newBaseline - obj.baseline |
|
totalPadding = newHeight - obj.height() |
|
pictures.append( |
|
oneEmptyLine * basePadding + |
|
obj.picture + |
|
oneEmptyLine * (totalPadding - basePadding)) |
|
|
|
result = [''.join(lines) for lines in zip(*pictures)] |
|
return '\n'.join(result), newBaseline |
|
|
|
def right(self, *args): |
|
r"""Put pictures next to this one. |
|
Returns string, baseline arguments for stringPict. |
|
(Multiline) strings are allowed, and are given a baseline of 0. |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.printing.pretty.stringpict import stringPict |
|
>>> print(stringPict("10").right(" + ",stringPict("1\r-\r2",1))[0]) |
|
1 |
|
10 + - |
|
2 |
|
|
|
""" |
|
return stringPict.next(self, *args) |
|
|
|
def left(self, *args): |
|
"""Put pictures (left to right) at left. |
|
Returns string, baseline arguments for stringPict. |
|
""" |
|
return stringPict.next(*(args + (self,))) |
|
|
|
@staticmethod |
|
def stack(*args): |
|
"""Put pictures on top of each other, |
|
from top to bottom. |
|
Returns string, baseline arguments for stringPict. |
|
The baseline is the baseline of the second picture. |
|
Everything is centered. |
|
Baseline is the baseline of the second picture. |
|
Strings are allowed. |
|
The special value stringPict.LINE is a row of '-' extended to the width. |
|
""" |
|
|
|
objects = [] |
|
for arg in args: |
|
if arg is not stringPict.LINE and isinstance(arg, str): |
|
arg = stringPict(arg) |
|
objects.append(arg) |
|
|
|
|
|
newWidth = max( |
|
obj.width() |
|
for obj in objects |
|
if obj is not stringPict.LINE) |
|
|
|
lineObj = stringPict(hobj('-', newWidth)) |
|
|
|
|
|
for i, obj in enumerate(objects): |
|
if obj is stringPict.LINE: |
|
objects[i] = lineObj |
|
|
|
|
|
newPicture = [center(line, newWidth) for obj in objects for line in obj.picture] |
|
newBaseline = objects[0].height() + objects[1].baseline |
|
return '\n'.join(newPicture), newBaseline |
|
|
|
def below(self, *args): |
|
"""Put pictures under this picture. |
|
Returns string, baseline arguments for stringPict. |
|
Baseline is baseline of top picture |
|
|
|
Examples |
|
======== |
|
|
|
>>> from sympy.printing.pretty.stringpict import stringPict |
|
>>> print(stringPict("x+3").below( |
|
... stringPict.LINE, '3')[0]) #doctest: +NORMALIZE_WHITESPACE |
|
x+3 |
|
--- |
|
3 |
|
|
|
""" |
|
s, baseline = stringPict.stack(self, *args) |
|
return s, self.baseline |
|
|
|
def above(self, *args): |
|
"""Put pictures above this picture. |
|
Returns string, baseline arguments for stringPict. |
|
Baseline is baseline of bottom picture. |
|
""" |
|
string, baseline = stringPict.stack(*(args + (self,))) |
|
baseline = len(string.splitlines()) - self.height() + self.baseline |
|
return string, baseline |
|
|
|
def parens(self, left='(', right=')', ifascii_nougly=False): |
|
"""Put parentheses around self. |
|
Returns string, baseline arguments for stringPict. |
|
|
|
left or right can be None or empty string which means 'no paren from |
|
that side' |
|
""" |
|
h = self.height() |
|
b = self.baseline |
|
|
|
|
|
if ifascii_nougly and not pretty_use_unicode(): |
|
h = 1 |
|
b = 0 |
|
|
|
res = self |
|
|
|
if left: |
|
lparen = stringPict(vobj(left, h), baseline=b) |
|
res = stringPict(*lparen.right(self)) |
|
if right: |
|
rparen = stringPict(vobj(right, h), baseline=b) |
|
res = stringPict(*res.right(rparen)) |
|
|
|
return ('\n'.join(res.picture), res.baseline) |
|
|
|
def leftslash(self): |
|
"""Precede object by a slash of the proper size. |
|
""" |
|
|
|
height = max( |
|
self.baseline, |
|
self.height() - 1 - self.baseline)*2 + 1 |
|
slash = '\n'.join( |
|
' '*(height - i - 1) + xobj('/', 1) + ' '*i |
|
for i in range(height) |
|
) |
|
return self.left(stringPict(slash, height//2)) |
|
|
|
def root(self, n=None): |
|
"""Produce a nice root symbol. |
|
Produces ugly results for big n inserts. |
|
""" |
|
|
|
|
|
|
|
result = self.above('_'*self.width()) |
|
|
|
height = self.height() |
|
slash = '\n'.join( |
|
' ' * (height - i - 1) + '/' + ' ' * i |
|
for i in range(height) |
|
) |
|
slash = stringPict(slash, height - 1) |
|
|
|
if height > 2: |
|
downline = stringPict('\\ \n \\', 1) |
|
else: |
|
downline = stringPict('\\') |
|
|
|
if n is not None and n.width() > downline.width(): |
|
downline = downline.left(' '*(n.width() - downline.width())) |
|
downline = downline.above(n) |
|
|
|
root = downline.right(slash) |
|
|
|
|
|
|
|
|
|
|
|
root.baseline = result.baseline - result.height() + root.height() |
|
return result.left(root) |
|
|
|
def render(self, * args, **kwargs): |
|
"""Return the string form of self. |
|
|
|
Unless the argument line_break is set to False, it will |
|
break the expression in a form that can be printed |
|
on the terminal without being broken up. |
|
""" |
|
if _GLOBAL_WRAP_LINE is not None: |
|
kwargs["wrap_line"] = _GLOBAL_WRAP_LINE |
|
|
|
if kwargs["wrap_line"] is False: |
|
return "\n".join(self.picture) |
|
|
|
if kwargs["num_columns"] is not None: |
|
|
|
ncols = kwargs["num_columns"] |
|
else: |
|
|
|
ncols = self.terminal_width() |
|
|
|
if ncols <= 0: |
|
ncols = 80 |
|
|
|
|
|
if self.width() <= ncols: |
|
return type(self.picture[0])(self) |
|
|
|
""" |
|
Break long-lines in a visually pleasing format. |
|
without overflow indicators | with overflow indicators |
|
| 2 2 3 | | 2 2 3 βͺ| |
|
|6*x *y + 4*x*y + | |6*x *y + 4*x*y + βͺ| |
|
| | | | |
|
| 3 4 4 | |βͺ 3 4 4 | |
|
|4*y*x + x + y | |βͺ 4*y*x + x + y | |
|
|a*c*e + a*c*f + a*d | |a*c*e + a*c*f + a*d βͺ| |
|
|*e + a*d*f + b*c*e | | | |
|
|+ b*c*f + b*d*e + b | |βͺ *e + a*d*f + b*c* βͺ| |
|
|*d*f | | | |
|
| | |βͺ e + b*c*f + b*d*e βͺ| |
|
| | | | |
|
| | |βͺ + b*d*f | |
|
""" |
|
|
|
overflow_first = "" |
|
if kwargs["use_unicode"] or pretty_use_unicode(): |
|
overflow_start = "\N{RIGHTWARDS ARROW WITH HOOK} " |
|
overflow_end = " \N{RIGHTWARDS ARROW WITH HOOK}" |
|
else: |
|
overflow_start = "> " |
|
overflow_end = " >" |
|
|
|
def chunks(line): |
|
"""Yields consecutive chunks of line_width ncols""" |
|
prefix = overflow_first |
|
width, start = line_width(prefix + overflow_end), 0 |
|
for i, x in enumerate(line): |
|
wx = line_width(x) |
|
|
|
|
|
if width + wx > ncols: |
|
yield prefix + line[start:i] + overflow_end |
|
prefix = overflow_start |
|
width, start = line_width(prefix + overflow_end), i |
|
width += wx |
|
yield prefix + line[start:] |
|
|
|
|
|
pictures = zip(*map(chunks, self.picture)) |
|
|
|
|
|
pictures = ["\n".join(picture) for picture in pictures] |
|
|
|
|
|
return "\n\n".join(pictures) |
|
|
|
def terminal_width(self): |
|
"""Return the terminal width if possible, otherwise return 0. |
|
""" |
|
size = shutil.get_terminal_size(fallback=(0, 0)) |
|
return size.columns |
|
|
|
def __eq__(self, o): |
|
if isinstance(o, str): |
|
return '\n'.join(self.picture) == o |
|
elif isinstance(o, stringPict): |
|
return o.picture == self.picture |
|
return False |
|
|
|
def __hash__(self): |
|
return super().__hash__() |
|
|
|
def __str__(self): |
|
return '\n'.join(self.picture) |
|
|
|
def __repr__(self): |
|
return "stringPict(%r,%d)" % ('\n'.join(self.picture), self.baseline) |
|
|
|
def __getitem__(self, index): |
|
return self.picture[index] |
|
|
|
def __len__(self): |
|
return len(self.s) |
|
|
|
|
|
class prettyForm(stringPict): |
|
""" |
|
Extension of the stringPict class that knows about basic math applications, |
|
optimizing double minus signs. |
|
|
|
"Binding" is interpreted as follows:: |
|
|
|
ATOM this is an atom: never needs to be parenthesized |
|
FUNC this is a function application: parenthesize if added (?) |
|
DIV this is a division: make wider division if divided |
|
POW this is a power: only parenthesize if exponent |
|
MUL this is a multiplication: parenthesize if powered |
|
ADD this is an addition: parenthesize if multiplied or powered |
|
NEG this is a negative number: optimize if added, parenthesize if |
|
multiplied or powered |
|
OPEN this is an open object: parenthesize if added, multiplied, or |
|
powered (example: Piecewise) |
|
""" |
|
ATOM, FUNC, DIV, POW, MUL, ADD, NEG, OPEN = range(8) |
|
|
|
def __init__(self, s, baseline=0, binding=0, unicode=None): |
|
"""Initialize from stringPict and binding power.""" |
|
stringPict.__init__(self, s, baseline) |
|
self.binding = binding |
|
if unicode is not None: |
|
sympy_deprecation_warning( |
|
""" |
|
The unicode argument to prettyForm is deprecated. Only the s |
|
argument (the first positional argument) should be passed. |
|
""", |
|
deprecated_since_version="1.7", |
|
active_deprecations_target="deprecated-pretty-printing-functions") |
|
self._unicode = unicode or s |
|
|
|
@property |
|
def unicode(self): |
|
sympy_deprecation_warning( |
|
""" |
|
The prettyForm.unicode attribute is deprecated. Use the |
|
prettyForm.s attribute instead. |
|
""", |
|
deprecated_since_version="1.7", |
|
active_deprecations_target="deprecated-pretty-printing-functions") |
|
return self._unicode |
|
|
|
|
|
|
|
def __add__(self, *others): |
|
"""Make a pretty addition. |
|
Addition of negative numbers is simplified. |
|
""" |
|
arg = self |
|
if arg.binding > prettyForm.NEG: |
|
arg = stringPict(*arg.parens()) |
|
result = [arg] |
|
for arg in others: |
|
|
|
if arg.binding > prettyForm.NEG: |
|
arg = stringPict(*arg.parens()) |
|
|
|
if arg.binding != prettyForm.NEG: |
|
result.append(' + ') |
|
result.append(arg) |
|
return prettyForm(binding=prettyForm.ADD, *stringPict.next(*result)) |
|
|
|
def __truediv__(self, den, slashed=False): |
|
"""Make a pretty division; stacked or slashed. |
|
""" |
|
if slashed: |
|
raise NotImplementedError("Can't do slashed fraction yet") |
|
num = self |
|
if num.binding == prettyForm.DIV: |
|
num = stringPict(*num.parens()) |
|
if den.binding == prettyForm.DIV: |
|
den = stringPict(*den.parens()) |
|
|
|
if num.binding==prettyForm.NEG: |
|
num = num.right(" ")[0] |
|
|
|
return prettyForm(binding=prettyForm.DIV, *stringPict.stack( |
|
num, |
|
stringPict.LINE, |
|
den)) |
|
|
|
def __mul__(self, *others): |
|
"""Make a pretty multiplication. |
|
Parentheses are needed around +, - and neg. |
|
""" |
|
quantity = { |
|
'degree': "\N{DEGREE SIGN}" |
|
} |
|
|
|
if len(others) == 0: |
|
return self |
|
|
|
|
|
arg = self |
|
if arg.binding > prettyForm.MUL and arg.binding != prettyForm.NEG: |
|
arg = stringPict(*arg.parens()) |
|
result = [arg] |
|
for arg in others: |
|
if arg.picture[0] not in quantity.values(): |
|
result.append(xsym('*')) |
|
|
|
if arg.binding > prettyForm.MUL and arg.binding != prettyForm.NEG: |
|
arg = stringPict(*arg.parens()) |
|
result.append(arg) |
|
|
|
len_res = len(result) |
|
for i in range(len_res): |
|
if i < len_res - 1 and result[i] == '-1' and result[i + 1] == xsym('*'): |
|
|
|
result.pop(i) |
|
result.pop(i) |
|
result.insert(i, '-') |
|
if result[0][0] == '-': |
|
|
|
|
|
bin = prettyForm.NEG |
|
if result[0] == '-': |
|
right = result[1] |
|
if right.picture[right.baseline][0] == '-': |
|
result[0] = '- ' |
|
else: |
|
bin = prettyForm.MUL |
|
return prettyForm(binding=bin, *stringPict.next(*result)) |
|
|
|
def __repr__(self): |
|
return "prettyForm(%r,%d,%d)" % ( |
|
'\n'.join(self.picture), |
|
self.baseline, |
|
self.binding) |
|
|
|
def __pow__(self, b): |
|
"""Make a pretty power. |
|
""" |
|
a = self |
|
use_inline_func_form = False |
|
if b.binding == prettyForm.POW: |
|
b = stringPict(*b.parens()) |
|
if a.binding > prettyForm.FUNC: |
|
a = stringPict(*a.parens()) |
|
elif a.binding == prettyForm.FUNC: |
|
|
|
if b.height() > 1: |
|
a = stringPict(*a.parens()) |
|
else: |
|
use_inline_func_form = True |
|
|
|
if use_inline_func_form: |
|
|
|
|
|
b.baseline = a.prettyFunc.baseline + b.height() |
|
func = stringPict(*a.prettyFunc.right(b)) |
|
return prettyForm(*func.right(a.prettyArgs)) |
|
else: |
|
|
|
|
|
top = stringPict(*b.left(' '*a.width())) |
|
bot = stringPict(*a.right(' '*b.width())) |
|
|
|
return prettyForm(binding=prettyForm.POW, *bot.above(top)) |
|
|
|
simpleFunctions = ["sin", "cos", "tan"] |
|
|
|
@staticmethod |
|
def apply(function, *args): |
|
"""Functions of one or more variables. |
|
""" |
|
if function in prettyForm.simpleFunctions: |
|
|
|
assert len( |
|
args) == 1, "Simple function %s must have 1 argument" % function |
|
arg = args[0].__pretty__() |
|
if arg.binding <= prettyForm.DIV: |
|
|
|
return prettyForm(binding=prettyForm.FUNC, *arg.left(function + ' ')) |
|
argumentList = [] |
|
for arg in args: |
|
argumentList.append(',') |
|
argumentList.append(arg.__pretty__()) |
|
argumentList = stringPict(*stringPict.next(*argumentList[1:])) |
|
argumentList = stringPict(*argumentList.parens()) |
|
return prettyForm(binding=prettyForm.ATOM, *argumentList.left(function)) |
|
|