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

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. 

5 

6Updates by Jason Gedge (email <my last name> at cs mun ca) 

7 - terminal_string() method 

8 - minor fixes and changes (mostly to prettyForm) 

9 

10TODO: 

11 - Allow left/center/right alignment options for above/below and 

12 top/center/bottom alignment options for left/right 

13""" 

14 

15from .pretty_symbology import hobj, vobj, xsym, xobj, pretty_use_unicode, line_width 

16from sympy.utilities.exceptions import sympy_deprecation_warning 

17 

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' 

24 

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 

35 

36 @staticmethod 

37 def equalLengths(lines): 

38 # empty lines 

39 if not lines: 

40 return [''] 

41 

42 width = max(line_width(line) for line in lines) 

43 return [line.center(width) for line in lines] 

44 

45 def height(self): 

46 """The height of the picture in characters.""" 

47 return len(self.picture) 

48 

49 def width(self): 

50 """The width of the picture in characters.""" 

51 return line_width(self.picture[0]) 

52 

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) 

64 

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 

71 

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)) 

81 

82 result = [''.join(lines) for lines in zip(*pictures)] 

83 return '\n'.join(result), newBaseline 

84 

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. 

89 

90 Examples 

91 ======== 

92 

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 

98 

99 """ 

100 return stringPict.next(self, *args) 

101 

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,))) 

107 

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) 

125 

126 #compute new width 

127 newWidth = max( 

128 obj.width() 

129 for obj in objects 

130 if obj is not stringPict.LINE) 

131 

132 lineObj = stringPict(hobj('-', newWidth)) 

133 

134 #replace LINE with proper lines 

135 for i, obj in enumerate(objects): 

136 if obj is stringPict.LINE: 

137 objects[i] = lineObj 

138 

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 

146 

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 

151 

152 Examples 

153 ======== 

154 

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 

161 

162 """ 

163 s, baseline = stringPict.stack(self, *args) 

164 return s, self.baseline 

165 

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 

174 

175 def parens(self, left='(', right=')', ifascii_nougly=False): 

176 """Put parentheses around self. 

177 Returns string, baseline arguments for stringPict. 

178 

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 

184 

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 

189 

190 res = self 

191 

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)) 

198 

199 return ('\n'.join(res.picture), res.baseline) 

200 

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)) 

213 

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) 

247 

248 def render(self, * args, **kwargs): 

249 """Return the string form of self. 

250 

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) 

257 

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() 

264 

265 ncols -= 2 

266 if ncols <= 0: 

267 ncols = 78 

268 

269 # If smaller than the terminal width, no need to correct 

270 if self.width() <= ncols: 

271 return type(self.picture[0])(self) 

272 

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 

283 

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 

292 

293 if svals[-1] == '': 

294 del svals[-1] # Get rid of the last spacer 

295 

296 return "\n".join(svals) 

297 

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 

331 

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 

338 

339 def __hash__(self): 

340 return super().__hash__() 

341 

342 def __str__(self): 

343 return '\n'.join(self.picture) 

344 

345 def __repr__(self): 

346 return "stringPict(%r,%d)" % ('\n'.join(self.picture), self.baseline) 

347 

348 def __getitem__(self, index): 

349 return self.picture[index] 

350 

351 def __len__(self): 

352 return len(self.s) 

353 

354 

355class prettyForm(stringPict): 

356 """ 

357 Extension of the stringPict class that knows about basic math applications, 

358 optimizing double minus signs. 

359 

360 "Binding" is interpreted as follows:: 

361 

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) 

374 

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 

388 

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 

399 

400 # Note: code to handle subtraction is in _print_Add 

401 

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)) 

419 

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()) 

430 

431 if num.binding==prettyForm.NEG: 

432 num = num.right(" ")[0] 

433 

434 return prettyForm(binding=prettyForm.DIV, *stringPict.stack( 

435 num, 

436 stringPict.LINE, 

437 den)) 

438 

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 } 

446 

447 if len(others) == 0: 

448 return self # We aren't actually multiplying... So nothing to do here. 

449 

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) 

462 

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)) 

481 

482 def __repr__(self): 

483 return "prettyForm(%r,%d,%d)" % ( 

484 '\n'.join(self.picture), 

485 self.baseline, 

486 self.binding) 

487 

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 

503 

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())) 

515 

516 return prettyForm(binding=prettyForm.POW, *bot.above(top)) 

517 

518 simpleFunctions = ["sin", "cos", "tan"] 

519 

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))