##// END OF EJS Templates
some more typechekcing
M Bussonnier -
Show More
@@ -1,133 +1,133
1 1 # encoding: utf-8
2 2 """
3 3 Tests for testing.tools
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import os
18 18 import unittest
19 19
20 20 from IPython.testing import decorators as dec
21 21 from IPython.testing import tools as tt
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Tests
25 25 #-----------------------------------------------------------------------------
26 26
27 27 @dec.skip_win32
28 28 def test_full_path_posix():
29 29 spath = "/foo/bar.py"
30 30 result = tt.full_path(spath, ["a.txt", "b.txt"])
31 31 assert result, ["/foo/a.txt" == "/foo/b.txt"]
32 32 spath = "/foo"
33 33 result = tt.full_path(spath, ["a.txt", "b.txt"])
34 34 assert result, ["/a.txt" == "/b.txt"]
35 result = tt.full_path(spath, "a.txt")
35 result = tt.full_path(spath, ["a.txt"])
36 36 assert result == ["/a.txt"]
37 37
38 38
39 39 @dec.skip_if_not_win32
40 40 def test_full_path_win32():
41 41 spath = "c:\\foo\\bar.py"
42 42 result = tt.full_path(spath, ["a.txt", "b.txt"])
43 43 assert result, ["c:\\foo\\a.txt" == "c:\\foo\\b.txt"]
44 44 spath = "c:\\foo"
45 45 result = tt.full_path(spath, ["a.txt", "b.txt"])
46 46 assert result, ["c:\\a.txt" == "c:\\b.txt"]
47 result = tt.full_path(spath, "a.txt")
47 result = tt.full_path(spath, ["a.txt"])
48 48 assert result == ["c:\\a.txt"]
49 49
50 50
51 51 def test_parser():
52 52 err = ("FAILED (errors=1)", 1, 0)
53 53 fail = ("FAILED (failures=1)", 0, 1)
54 54 both = ("FAILED (errors=1, failures=1)", 1, 1)
55 55 for txt, nerr, nfail in [err, fail, both]:
56 56 nerr1, nfail1 = tt.parse_test_output(txt)
57 57 assert nerr == nerr1
58 58 assert nfail == nfail1
59 59
60 60
61 61 def test_temp_pyfile():
62 62 src = 'pass\n'
63 63 fname = tt.temp_pyfile(src)
64 64 assert os.path.isfile(fname)
65 65 with open(fname, encoding="utf-8") as fh2:
66 66 src2 = fh2.read()
67 67 assert src2 == src
68 68
69 69 class TestAssertPrints(unittest.TestCase):
70 70 def test_passing(self):
71 71 with tt.AssertPrints("abc"):
72 72 print("abcd")
73 73 print("def")
74 74 print(b"ghi")
75 75
76 76 def test_failing(self):
77 77 def func():
78 78 with tt.AssertPrints("abc"):
79 79 print("acd")
80 80 print("def")
81 81 print(b"ghi")
82 82
83 83 self.assertRaises(AssertionError, func)
84 84
85 85
86 86 class Test_ipexec_validate(tt.TempFileMixin):
87 87 def test_main_path(self):
88 88 """Test with only stdout results.
89 89 """
90 90 self.mktmp("print('A')\n"
91 91 "print('B')\n"
92 92 )
93 93 out = "A\nB"
94 94 tt.ipexec_validate(self.fname, out)
95 95
96 96 def test_main_path2(self):
97 97 """Test with only stdout results, expecting windows line endings.
98 98 """
99 99 self.mktmp("print('A')\n"
100 100 "print('B')\n"
101 101 )
102 102 out = "A\r\nB"
103 103 tt.ipexec_validate(self.fname, out)
104 104
105 105 def test_exception_path(self):
106 106 """Test exception path in exception_validate.
107 107 """
108 108 self.mktmp("import sys\n"
109 109 "print('A')\n"
110 110 "print('B')\n"
111 111 "print('C', file=sys.stderr)\n"
112 112 "print('D', file=sys.stderr)\n"
113 113 )
114 114 out = "A\nB"
115 115 tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\nD")
116 116
117 117 def test_exception_path2(self):
118 118 """Test exception path in exception_validate, expecting windows line endings.
119 119 """
120 120 self.mktmp("import sys\n"
121 121 "print('A')\n"
122 122 "print('B')\n"
123 123 "print('C', file=sys.stderr)\n"
124 124 "print('D', file=sys.stderr)\n"
125 125 )
126 126 out = "A\r\nB"
127 127 tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\r\nD")
128 128
129 129
130 130 def tearDown(self):
131 131 # tear down correctly the mixin,
132 132 # unittest.TestCase.tearDown does nothing
133 133 tt.TempFileMixin.tearDown(self)
@@ -1,476 +1,471
1 1 """Generic testing tools.
2 2
3 3 Authors
4 4 -------
5 5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 6 """
7 7
8 8
9 9 # Copyright (c) IPython Development Team.
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 import os
13 13 from pathlib import Path
14 14 import re
15 15 import sys
16 16 import tempfile
17 17 import unittest
18 18
19 19 from contextlib import contextmanager
20 20 from io import StringIO
21 21 from subprocess import Popen, PIPE
22 22 from unittest.mock import patch
23 23
24 24 from traitlets.config.loader import Config
25 25 from IPython.utils.process import get_output_error_code
26 26 from IPython.utils.text import list_strings
27 27 from IPython.utils.io import temp_pyfile, Tee
28 28 from IPython.utils import py3compat
29 29
30 30 from . import decorators as dec
31 31 from . import skipdoctest
32 32
33 33
34 34 # The docstring for full_path doctests differently on win32 (different path
35 35 # separator) so just skip the doctest there. The example remains informative.
36 36 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
37 37
38 38 @doctest_deco
39 def full_path(startPath,files):
39 def full_path(startPath: str, files: list[str]) -> list[str]:
40 40 """Make full paths for all the listed files, based on startPath.
41 41
42 42 Only the base part of startPath is kept, since this routine is typically
43 43 used with a script's ``__file__`` variable as startPath. The base of startPath
44 44 is then prepended to all the listed files, forming the output list.
45 45
46 46 Parameters
47 47 ----------
48 48 startPath : string
49 49 Initial path to use as the base for the results. This path is split
50 50 using os.path.split() and only its first component is kept.
51 51
52 files : string or list
52 files : list
53 53 One or more files.
54 54
55 55 Examples
56 56 --------
57 57
58 58 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
59 59 ['/foo/a.txt', '/foo/b.txt']
60 60
61 61 >>> full_path('/foo',['a.txt','b.txt'])
62 62 ['/a.txt', '/b.txt']
63 63
64 If a single file is given, the output is still a list::
65
66 >>> full_path('/foo','a.txt')
67 ['/a.txt']
68 64 """
69
70 files = list_strings(files)
65 assert isinstance(files, list)
71 66 base = os.path.split(startPath)[0]
72 67 return [ os.path.join(base,f) for f in files ]
73 68
74 69
75 70 def parse_test_output(txt):
76 71 """Parse the output of a test run and return errors, failures.
77 72
78 73 Parameters
79 74 ----------
80 75 txt : str
81 76 Text output of a test run, assumed to contain a line of one of the
82 77 following forms::
83 78
84 79 'FAILED (errors=1)'
85 80 'FAILED (failures=1)'
86 81 'FAILED (errors=1, failures=1)'
87 82
88 83 Returns
89 84 -------
90 85 nerr, nfail
91 86 number of errors and failures.
92 87 """
93 88
94 89 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
95 90 if err_m:
96 91 nerr = int(err_m.group(1))
97 92 nfail = 0
98 93 return nerr, nfail
99 94
100 95 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
101 96 if fail_m:
102 97 nerr = 0
103 98 nfail = int(fail_m.group(1))
104 99 return nerr, nfail
105 100
106 101 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
107 102 re.MULTILINE)
108 103 if both_m:
109 104 nerr = int(both_m.group(1))
110 105 nfail = int(both_m.group(2))
111 106 return nerr, nfail
112 107
113 108 # If the input didn't match any of these forms, assume no error/failures
114 109 return 0, 0
115 110
116 111
117 112 # So nose doesn't think this is a test
118 113 parse_test_output.__test__ = False
119 114
120 115
121 116 def default_argv():
122 117 """Return a valid default argv for creating testing instances of ipython"""
123 118
124 119 return ['--quick', # so no config file is loaded
125 120 # Other defaults to minimize side effects on stdout
126 121 '--colors=NoColor', '--no-term-title','--no-banner',
127 122 '--autocall=0']
128 123
129 124
130 125 def default_config():
131 126 """Return a config object with good defaults for testing."""
132 127 config = Config()
133 128 config.TerminalInteractiveShell.colors = 'NoColor'
134 129 config.TerminalTerminalInteractiveShell.term_title = False,
135 130 config.TerminalInteractiveShell.autocall = 0
136 131 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
137 132 config.HistoryManager.hist_file = Path(f.name)
138 133 f.close()
139 134 config.HistoryManager.db_cache_size = 10000
140 135 return config
141 136
142 137
143 138 def get_ipython_cmd(as_string=False):
144 139 """
145 140 Return appropriate IPython command line name. By default, this will return
146 141 a list that can be used with subprocess.Popen, for example, but passing
147 142 `as_string=True` allows for returning the IPython command as a string.
148 143
149 144 Parameters
150 145 ----------
151 146 as_string: bool
152 147 Flag to allow to return the command as a string.
153 148 """
154 149 ipython_cmd = [sys.executable, "-m", "IPython"]
155 150
156 151 if as_string:
157 152 ipython_cmd = " ".join(ipython_cmd)
158 153
159 154 return ipython_cmd
160 155
161 156 def ipexec(fname, options=None, commands=()):
162 157 """Utility to call 'ipython filename'.
163 158
164 159 Starts IPython with a minimal and safe configuration to make startup as fast
165 160 as possible.
166 161
167 162 Note that this starts IPython in a subprocess!
168 163
169 164 Parameters
170 165 ----------
171 166 fname : str, Path
172 167 Name of file to be executed (should have .py or .ipy extension).
173 168
174 169 options : optional, list
175 170 Extra command-line flags to be passed to IPython.
176 171
177 172 commands : optional, list
178 173 Commands to send in on stdin
179 174
180 175 Returns
181 176 -------
182 177 ``(stdout, stderr)`` of ipython subprocess.
183 178 """
184 179 __tracebackhide__ = True
185 180
186 181 if options is None:
187 182 options = []
188 183
189 184 cmdargs = default_argv() + options
190 185
191 186 test_dir = os.path.dirname(__file__)
192 187
193 188 ipython_cmd = get_ipython_cmd()
194 189 # Absolute path for filename
195 190 full_fname = os.path.join(test_dir, fname)
196 191 full_cmd = ipython_cmd + cmdargs + ['--', full_fname]
197 192 env = os.environ.copy()
198 193 # FIXME: ignore all warnings in ipexec while we have shims
199 194 # should we keep suppressing warnings here, even after removing shims?
200 195 env['PYTHONWARNINGS'] = 'ignore'
201 196 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
202 197 # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
203 198 env.pop("PYCHARM_HOSTED", None)
204 199 for k, v in env.items():
205 200 # Debug a bizarre failure we've seen on Windows:
206 201 # TypeError: environment can only contain strings
207 202 if not isinstance(v, str):
208 203 print(k, v)
209 204 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
210 205 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
211 206 out, err = py3compat.decode(out), py3compat.decode(err)
212 207 # `import readline` causes 'ESC[?1034h' to be output sometimes,
213 208 # so strip that out before doing comparisons
214 209 if out:
215 210 out = re.sub(r'\x1b\[[^h]+h', '', out)
216 211 return out, err
217 212
218 213
219 214 def ipexec_validate(fname, expected_out, expected_err='',
220 215 options=None, commands=()):
221 216 """Utility to call 'ipython filename' and validate output/error.
222 217
223 218 This function raises an AssertionError if the validation fails.
224 219
225 220 Note that this starts IPython in a subprocess!
226 221
227 222 Parameters
228 223 ----------
229 224 fname : str, Path
230 225 Name of the file to be executed (should have .py or .ipy extension).
231 226
232 227 expected_out : str
233 228 Expected stdout of the process.
234 229
235 230 expected_err : optional, str
236 231 Expected stderr of the process.
237 232
238 233 options : optional, list
239 234 Extra command-line flags to be passed to IPython.
240 235
241 236 Returns
242 237 -------
243 238 None
244 239 """
245 240 __tracebackhide__ = True
246 241
247 242 out, err = ipexec(fname, options, commands)
248 243 # print('OUT', out) # dbg
249 244 # print('ERR', err) # dbg
250 245 # If there are any errors, we must check those before stdout, as they may be
251 246 # more informative than simply having an empty stdout.
252 247 if err:
253 248 if expected_err:
254 249 assert "\n".join(err.strip().splitlines()) == "\n".join(
255 250 expected_err.strip().splitlines()
256 251 )
257 252 else:
258 253 raise ValueError('Running file %r produced error: %r' %
259 254 (fname, err))
260 255 # If no errors or output on stderr was expected, match stdout
261 256 assert "\n".join(out.strip().splitlines()) == "\n".join(
262 257 expected_out.strip().splitlines()
263 258 )
264 259
265 260
266 261 class TempFileMixin(unittest.TestCase):
267 262 """Utility class to create temporary Python/IPython files.
268 263
269 264 Meant as a mixin class for test cases."""
270 265
271 266 def mktmp(self, src, ext='.py'):
272 267 """Make a valid python temp file."""
273 268 fname = temp_pyfile(src, ext)
274 269 if not hasattr(self, 'tmps'):
275 270 self.tmps=[]
276 271 self.tmps.append(fname)
277 272 self.fname = fname
278 273
279 274 def tearDown(self):
280 275 # If the tmpfile wasn't made because of skipped tests, like in
281 276 # win32, there's nothing to cleanup.
282 277 if hasattr(self, 'tmps'):
283 278 for fname in self.tmps:
284 279 # If the tmpfile wasn't made because of skipped tests, like in
285 280 # win32, there's nothing to cleanup.
286 281 try:
287 282 os.unlink(fname)
288 283 except:
289 284 # On Windows, even though we close the file, we still can't
290 285 # delete it. I have no clue why
291 286 pass
292 287
293 288 def __enter__(self):
294 289 return self
295 290
296 291 def __exit__(self, exc_type, exc_value, traceback):
297 292 self.tearDown()
298 293
299 294
300 295 pair_fail_msg = ("Testing {0}\n\n"
301 296 "In:\n"
302 297 " {1!r}\n"
303 298 "Expected:\n"
304 299 " {2!r}\n"
305 300 "Got:\n"
306 301 " {3!r}\n")
307 302 def check_pairs(func, pairs):
308 303 """Utility function for the common case of checking a function with a
309 304 sequence of input/output pairs.
310 305
311 306 Parameters
312 307 ----------
313 308 func : callable
314 309 The function to be tested. Should accept a single argument.
315 310 pairs : iterable
316 311 A list of (input, expected_output) tuples.
317 312
318 313 Returns
319 314 -------
320 315 None. Raises an AssertionError if any output does not match the expected
321 316 value.
322 317 """
323 318 __tracebackhide__ = True
324 319
325 320 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
326 321 for inp, expected in pairs:
327 322 out = func(inp)
328 323 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
329 324
330 325
331 326 MyStringIO = StringIO
332 327
333 328 _re_type = type(re.compile(r''))
334 329
335 330 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
336 331 -------
337 332 {2!s}
338 333 -------
339 334 """
340 335
341 336 class AssertPrints(object):
342 337 """Context manager for testing that code prints certain text.
343 338
344 339 Examples
345 340 --------
346 341 >>> with AssertPrints("abc", suppress=False):
347 342 ... print("abcd")
348 343 ... print("def")
349 344 ...
350 345 abcd
351 346 def
352 347 """
353 348 def __init__(self, s, channel='stdout', suppress=True):
354 349 self.s = s
355 350 if isinstance(self.s, (str, _re_type)):
356 351 self.s = [self.s]
357 352 self.channel = channel
358 353 self.suppress = suppress
359 354
360 355 def __enter__(self):
361 356 self.orig_stream = getattr(sys, self.channel)
362 357 self.buffer = MyStringIO()
363 358 self.tee = Tee(self.buffer, channel=self.channel)
364 359 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
365 360
366 361 def __exit__(self, etype, value, traceback):
367 362 __tracebackhide__ = True
368 363
369 364 try:
370 365 if value is not None:
371 366 # If an error was raised, don't check anything else
372 367 return False
373 368 self.tee.flush()
374 369 setattr(sys, self.channel, self.orig_stream)
375 370 printed = self.buffer.getvalue()
376 371 for s in self.s:
377 372 if isinstance(s, _re_type):
378 373 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
379 374 else:
380 375 assert s in printed, notprinted_msg.format(s, self.channel, printed)
381 376 return False
382 377 finally:
383 378 self.tee.close()
384 379
385 380 printed_msg = """Found {0!r} in printed output (on {1}):
386 381 -------
387 382 {2!s}
388 383 -------
389 384 """
390 385
391 386 class AssertNotPrints(AssertPrints):
392 387 """Context manager for checking that certain output *isn't* produced.
393 388
394 389 Counterpart of AssertPrints"""
395 390 def __exit__(self, etype, value, traceback):
396 391 __tracebackhide__ = True
397 392
398 393 try:
399 394 if value is not None:
400 395 # If an error was raised, don't check anything else
401 396 self.tee.close()
402 397 return False
403 398 self.tee.flush()
404 399 setattr(sys, self.channel, self.orig_stream)
405 400 printed = self.buffer.getvalue()
406 401 for s in self.s:
407 402 if isinstance(s, _re_type):
408 403 assert not s.search(printed),printed_msg.format(
409 404 s.pattern, self.channel, printed)
410 405 else:
411 406 assert s not in printed, printed_msg.format(
412 407 s, self.channel, printed)
413 408 return False
414 409 finally:
415 410 self.tee.close()
416 411
417 412 @contextmanager
418 413 def mute_warn():
419 414 from IPython.utils import warn
420 415 save_warn = warn.warn
421 416 warn.warn = lambda *a, **kw: None
422 417 try:
423 418 yield
424 419 finally:
425 420 warn.warn = save_warn
426 421
427 422 @contextmanager
428 423 def make_tempfile(name):
429 424 """Create an empty, named, temporary file for the duration of the context."""
430 425 open(name, "w", encoding="utf-8").close()
431 426 try:
432 427 yield
433 428 finally:
434 429 os.unlink(name)
435 430
436 431 def fake_input(inputs):
437 432 """Temporarily replace the input() function to return the given values
438 433
439 434 Use as a context manager:
440 435
441 436 with fake_input(['result1', 'result2']):
442 437 ...
443 438
444 439 Values are returned in order. If input() is called again after the last value
445 440 was used, EOFError is raised.
446 441 """
447 442 it = iter(inputs)
448 443 def mock_input(prompt=''):
449 444 try:
450 445 return next(it)
451 446 except StopIteration as e:
452 447 raise EOFError('No more inputs given') from e
453 448
454 449 return patch('builtins.input', mock_input)
455 450
456 451 def help_output_test(subcommand=''):
457 452 """test that `ipython [subcommand] -h` works"""
458 453 cmd = get_ipython_cmd() + [subcommand, '-h']
459 454 out, err, rc = get_output_error_code(cmd)
460 455 assert rc == 0, err
461 456 assert "Traceback" not in err
462 457 assert "Options" in out
463 458 assert "--help-all" in out
464 459 return out, err
465 460
466 461
467 462 def help_all_output_test(subcommand=''):
468 463 """test that `ipython [subcommand] --help-all` works"""
469 464 cmd = get_ipython_cmd() + [subcommand, '--help-all']
470 465 out, err, rc = get_output_error_code(cmd)
471 466 assert rc == 0, err
472 467 assert "Traceback" not in err
473 468 assert "Options" in out
474 469 assert "Class" in out
475 470 return out, err
476 471
@@ -1,828 +1,886
1 1 """
2 2 Utilities for working with strings and text.
3 3
4 4 Inheritance diagram:
5 5
6 6 .. inheritance-diagram:: IPython.utils.text
7 7 :parts: 3
8 8 """
9 9
10 10 import os
11 11 import re
12 12 import string
13 13 import sys
14 14 import textwrap
15 15 import warnings
16 16 from string import Formatter
17 17 from pathlib import Path
18 18
19 from typing import List, Dict, Tuple, Optional, cast, Sequence, Mapping, Any
19 from typing import (
20 List,
21 Dict,
22 Tuple,
23 Optional,
24 cast,
25 Sequence,
26 Mapping,
27 Any,
28 Union,
29 Callable,
30 Iterator,
31 TypeVar,
32 )
20 33
21 34 if sys.version_info < (3, 12):
22 35 from typing_extensions import Self
23 36 else:
24 37 from typing import Self
25 38
26 39
27 40 class LSString(str):
28 41 """String derivative with a special access attributes.
29 42
30 43 These are normal strings, but with the special attributes:
31 44
32 45 .l (or .list) : value as list (split on newlines).
33 46 .n (or .nlstr): original value (the string itself).
34 47 .s (or .spstr): value as whitespace-separated string.
35 48 .p (or .paths): list of path objects (requires path.py package)
36 49
37 50 Any values which require transformations are computed only once and
38 51 cached.
39 52
40 53 Such strings are very useful to efficiently interact with the shell, which
41 54 typically only understands whitespace-separated options for commands."""
42 55
43 56 __list: List[str]
44 57 __spstr: str
45 58 __paths: List[Path]
46 59
47 60 def get_list(self) -> List[str]:
48 61 try:
49 62 return self.__list
50 63 except AttributeError:
51 64 self.__list = self.split('\n')
52 65 return self.__list
53 66
54 67 l = list = property(get_list)
55 68
56 69 def get_spstr(self) -> str:
57 70 try:
58 71 return self.__spstr
59 72 except AttributeError:
60 73 self.__spstr = self.replace('\n',' ')
61 74 return self.__spstr
62 75
63 76 s = spstr = property(get_spstr)
64 77
65 78 def get_nlstr(self) -> Self:
66 79 return self
67 80
68 81 n = nlstr = property(get_nlstr)
69 82
70 83 def get_paths(self) -> List[Path]:
71 84 try:
72 85 return self.__paths
73 86 except AttributeError:
74 87 self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)]
75 88 return self.__paths
76 89
77 90 p = paths = property(get_paths)
78 91
79 92 # FIXME: We need to reimplement type specific displayhook and then add this
80 93 # back as a custom printer. This should also be moved outside utils into the
81 94 # core.
82 95
83 96 # def print_lsstring(arg):
84 97 # """ Prettier (non-repr-like) and more informative printer for LSString """
85 98 # print("LSString (.p, .n, .l, .s available). Value:")
86 99 # print(arg)
87 100 #
88 101 #
89 102 # print_lsstring = result_display.register(LSString)(print_lsstring)
90 103
91 104
92 105 class SList(list):
93 106 """List derivative with a special access attributes.
94 107
95 108 These are normal lists, but with the special attributes:
96 109
97 110 * .l (or .list) : value as list (the list itself).
98 111 * .n (or .nlstr): value as a string, joined on newlines.
99 112 * .s (or .spstr): value as a string, joined on spaces.
100 113 * .p (or .paths): list of path objects (requires path.py package)
101 114
102 115 Any values which require transformations are computed only once and
103 116 cached."""
104 117
105 118 __spstr: str
106 119 __nlstr: str
107 120 __paths: List[Path]
108 121
109 122 def get_list(self) -> Self:
110 123 return self
111 124
112 125 l = list = property(get_list)
113 126
114 127 def get_spstr(self) -> str:
115 128 try:
116 129 return self.__spstr
117 130 except AttributeError:
118 131 self.__spstr = ' '.join(self)
119 132 return self.__spstr
120 133
121 134 s = spstr = property(get_spstr)
122 135
123 136 def get_nlstr(self) -> str:
124 137 try:
125 138 return self.__nlstr
126 139 except AttributeError:
127 140 self.__nlstr = '\n'.join(self)
128 141 return self.__nlstr
129 142
130 143 n = nlstr = property(get_nlstr)
131 144
132 145 def get_paths(self) -> List[Path]:
133 146 try:
134 147 return self.__paths
135 148 except AttributeError:
136 149 self.__paths = [Path(p) for p in self if os.path.exists(p)]
137 150 return self.__paths
138 151
139 152 p = paths = property(get_paths)
140 153
141 def grep(self, pattern, prune = False, field = None):
142 """ Return all strings matching 'pattern' (a regex or callable)
154 def grep(
155 self,
156 pattern: Union[str, Callable[[Any], re.Match[str] | None]],
157 prune: bool = False,
158 field: Optional[int] = None,
159 ) -> Self:
160 """Return all strings matching 'pattern' (a regex or callable)
143 161
144 162 This is case-insensitive. If prune is true, return all items
145 163 NOT matching the pattern.
146 164
147 165 If field is specified, the match must occur in the specified
148 166 whitespace-separated field.
149 167
150 168 Examples::
151 169
152 170 a.grep( lambda x: x.startswith('C') )
153 171 a.grep('Cha.*log', prune=1)
154 172 a.grep('chm', field=-1)
155 173 """
156 174
157 def match_target(s):
175 def match_target(s: str) -> str:
158 176 if field is None:
159 177 return s
160 178 parts = s.split()
161 179 try:
162 180 tgt = parts[field]
163 181 return tgt
164 182 except IndexError:
165 183 return ""
166 184
167 185 if isinstance(pattern, str):
168 186 pred = lambda x : re.search(pattern, x, re.IGNORECASE)
169 187 else:
170 188 pred = pattern
171 189 if not prune:
172 return SList([el for el in self if pred(match_target(el))])
190 return type(self)([el for el in self if pred(match_target(el))])
173 191 else:
174 return SList([el for el in self if not pred(match_target(el))])
192 return type(self)([el for el in self if not pred(match_target(el))])
175 193
176 def fields(self, *fields):
177 """ Collect whitespace-separated fields from string list
194 def fields(self, *fields: List[str]) -> List[List[str]]:
195 """Collect whitespace-separated fields from string list
178 196
179 197 Allows quick awk-like usage of string lists.
180 198
181 199 Example data (in var a, created by 'a = !ls -l')::
182 200
183 201 -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog
184 202 drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython
185 203
186 204 * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']``
187 205 * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']``
188 206 (note the joining by space).
189 207 * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']``
190 208
191 209 IndexErrors are ignored.
192 210
193 211 Without args, fields() just split()'s the strings.
194 212 """
195 213 if len(fields) == 0:
196 214 return [el.split() for el in self]
197 215
198 216 res = SList()
199 217 for el in [f.split() for f in self]:
200 218 lineparts = []
201 219
202 220 for fd in fields:
203 221 try:
204 222 lineparts.append(el[fd])
205 223 except IndexError:
206 224 pass
207 225 if lineparts:
208 226 res.append(" ".join(lineparts))
209 227
210 228 return res
211 229
212 def sort(self,field= None, nums = False):
213 """ sort by specified fields (see fields())
230 def sort( # type:ignore[override]
231 self,
232 field: Optional[List[str]] = None,
233 nums: bool = False,
234 ) -> Self:
235 """sort by specified fields (see fields())
214 236
215 237 Example::
216 238
217 239 a.sort(1, nums = True)
218 240
219 241 Sorts a by second field, in numerical order (so that 21 > 3)
220 242
221 243 """
222 244
223 245 #decorate, sort, undecorate
224 246 if field is not None:
225 247 dsu = [[SList([line]).fields(field), line] for line in self]
226 248 else:
227 249 dsu = [[line, line] for line in self]
228 250 if nums:
229 251 for i in range(len(dsu)):
230 252 numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()])
231 253 try:
232 254 n = int(numstr)
233 255 except ValueError:
234 256 n = 0
235 257 dsu[i][0] = n
236 258
237 259
238 260 dsu.sort()
239 return SList([t[1] for t in dsu])
261 return type(self)([t[1] for t in dsu])
240 262
241 263
242 264 # FIXME: We need to reimplement type specific displayhook and then add this
243 265 # back as a custom printer. This should also be moved outside utils into the
244 266 # core.
245 267
246 268 # def print_slist(arg):
247 269 # """ Prettier (non-repr-like) and more informative printer for SList """
248 270 # print("SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):")
249 271 # if hasattr(arg, 'hideonce') and arg.hideonce:
250 272 # arg.hideonce = False
251 273 # return
252 274 #
253 275 # nlprint(arg) # This was a nested list printer, now removed.
254 276 #
255 277 # print_slist = result_display.register(SList)(print_slist)
256 278
257 279
258 def indent(instr,nspaces=4, ntabs=0, flatten=False):
280 def indent(instr: str, nspaces: int = 4, ntabs: int = 0, flatten: bool = False) -> str:
259 281 """Indent a string a given number of spaces or tabstops.
260 282
261 283 indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
262 284
263 285 Parameters
264 286 ----------
265 287 instr : basestring
266 288 The string to be indented.
267 289 nspaces : int (default: 4)
268 290 The number of spaces to be indented.
269 291 ntabs : int (default: 0)
270 292 The number of tabs to be indented.
271 293 flatten : bool (default: False)
272 294 Whether to scrub existing indentation. If True, all lines will be
273 295 aligned to the same indentation. If False, existing indentation will
274 296 be strictly increased.
275 297
276 298 Returns
277 299 -------
278 str|unicode : string indented by ntabs and nspaces.
300 str : string indented by ntabs and nspaces.
279 301
280 302 """
281 303 if instr is None:
282 304 return
283 305 ind = '\t'*ntabs+' '*nspaces
284 306 if flatten:
285 307 pat = re.compile(r'^\s*', re.MULTILINE)
286 308 else:
287 309 pat = re.compile(r'^', re.MULTILINE)
288 310 outstr = re.sub(pat, ind, instr)
289 311 if outstr.endswith(os.linesep+ind):
290 312 return outstr[:-len(ind)]
291 313 else:
292 314 return outstr
293 315
294 316
295 def list_strings(arg):
317 def list_strings(arg: Union[str, List[str]]) -> List[str]:
296 318 """Always return a list of strings, given a string or list of strings
297 319 as input.
298 320
299 321 Examples
300 322 --------
301 323 ::
302 324
303 325 In [7]: list_strings('A single string')
304 326 Out[7]: ['A single string']
305 327
306 328 In [8]: list_strings(['A single string in a list'])
307 329 Out[8]: ['A single string in a list']
308 330
309 331 In [9]: list_strings(['A','list','of','strings'])
310 332 Out[9]: ['A', 'list', 'of', 'strings']
311 333 """
312 334
313 335 if isinstance(arg, str):
314 336 return [arg]
315 337 else:
316 338 return arg
317 339
318 340
319 def marquee(txt='',width=78,mark='*'):
341 def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str:
320 342 """Return the input string centered in a 'marquee'.
321 343
322 344 Examples
323 345 --------
324 346 ::
325 347
326 348 In [16]: marquee('A test',40)
327 349 Out[16]: '**************** A test ****************'
328 350
329 351 In [17]: marquee('A test',40,'-')
330 352 Out[17]: '---------------- A test ----------------'
331 353
332 354 In [18]: marquee('A test',40,' ')
333 355 Out[18]: ' A test '
334 356
335 357 """
336 358 if not txt:
337 359 return (mark*width)[:width]
338 360 nmark = (width-len(txt)-2)//len(mark)//2
339 361 if nmark < 0: nmark =0
340 362 marks = mark*nmark
341 363 return '%s %s %s' % (marks,txt,marks)
342 364
343 365
344 366 ini_spaces_re = re.compile(r'^(\s+)')
345 367
346 def num_ini_spaces(strng):
368
369 def num_ini_spaces(strng: str) -> int:
347 370 """Return the number of initial spaces in a string"""
348 371 warnings.warn(
349 372 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
350 373 "It is considered fro removal in in future version. "
351 374 "Please open an issue if you believe it should be kept.",
352 375 stacklevel=2,
353 376 category=PendingDeprecationWarning,
354 377 )
355 378 ini_spaces = ini_spaces_re.match(strng)
356 379 if ini_spaces:
357 380 return ini_spaces.end()
358 381 else:
359 382 return 0
360 383
361 384
362 def format_screen(strng):
385 def format_screen(strng: str) -> str:
363 386 """Format a string for screen printing.
364 387
365 388 This removes some latex-type format codes."""
366 389 # Paragraph continue
367 390 par_re = re.compile(r'\\$',re.MULTILINE)
368 391 strng = par_re.sub('',strng)
369 392 return strng
370 393
371 394
372 395 def dedent(text: str) -> str:
373 396 """Equivalent of textwrap.dedent that ignores unindented first line.
374 397
375 398 This means it will still dedent strings like:
376 399 '''foo
377 400 is a bar
378 401 '''
379 402
380 403 For use in wrap_paragraphs.
381 404 """
382 405
383 406 if text.startswith('\n'):
384 407 # text starts with blank line, don't ignore the first line
385 408 return textwrap.dedent(text)
386 409
387 410 # split first line
388 411 splits = text.split('\n',1)
389 412 if len(splits) == 1:
390 413 # only one line
391 414 return textwrap.dedent(text)
392 415
393 416 first, rest = splits
394 417 # dedent everything but the first line
395 418 rest = textwrap.dedent(rest)
396 419 return '\n'.join([first, rest])
397 420
398 421
399 def wrap_paragraphs(text, ncols=80):
422 def wrap_paragraphs(text: str, ncols: int = 80) -> List[str]:
400 423 """Wrap multiple paragraphs to fit a specified width.
401 424
402 425 This is equivalent to textwrap.wrap, but with support for multiple
403 426 paragraphs, as separated by empty lines.
404 427
405 428 Returns
406 429 -------
407 430 list of complete paragraphs, wrapped to fill `ncols` columns.
408 431 """
409 432 warnings.warn(
410 433 "`wrap_paragraphs` is Pending Deprecation since IPython 8.17."
411 434 "It is considered fro removal in in future version. "
412 435 "Please open an issue if you believe it should be kept.",
413 436 stacklevel=2,
414 437 category=PendingDeprecationWarning,
415 438 )
416 439 paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE)
417 440 text = dedent(text).strip()
418 441 paragraphs = paragraph_re.split(text)[::2] # every other entry is space
419 442 out_ps = []
420 443 indent_re = re.compile(r'\n\s+', re.MULTILINE)
421 444 for p in paragraphs:
422 445 # presume indentation that survives dedent is meaningful formatting,
423 446 # so don't fill unless text is flush.
424 447 if indent_re.search(p) is None:
425 448 # wrap paragraph
426 449 p = textwrap.fill(p, ncols)
427 450 out_ps.append(p)
428 451 return out_ps
429 452
430 453
431 def strip_email_quotes(text):
454 def strip_email_quotes(text: str) -> str:
432 455 """Strip leading email quotation characters ('>').
433 456
434 457 Removes any combination of leading '>' interspersed with whitespace that
435 458 appears *identically* in all lines of the input text.
436 459
437 460 Parameters
438 461 ----------
439 462 text : str
440 463
441 464 Examples
442 465 --------
443 466
444 467 Simple uses::
445 468
446 469 In [2]: strip_email_quotes('> > text')
447 470 Out[2]: 'text'
448 471
449 472 In [3]: strip_email_quotes('> > text\\n> > more')
450 473 Out[3]: 'text\\nmore'
451 474
452 475 Note how only the common prefix that appears in all lines is stripped::
453 476
454 477 In [4]: strip_email_quotes('> > text\\n> > more\\n> more...')
455 478 Out[4]: '> text\\n> more\\nmore...'
456 479
457 480 So if any line has no quote marks ('>'), then none are stripped from any
458 481 of them ::
459 482
460 483 In [5]: strip_email_quotes('> > text\\n> > more\\nlast different')
461 484 Out[5]: '> > text\\n> > more\\nlast different'
462 485 """
463 486 lines = text.splitlines()
464 487 strip_len = 0
465 488
466 489 for characters in zip(*lines):
467 490 # Check if all characters in this position are the same
468 491 if len(set(characters)) > 1:
469 492 break
470 493 prefix_char = characters[0]
471 494
472 495 if prefix_char in string.whitespace or prefix_char == ">":
473 496 strip_len += 1
474 497 else:
475 498 break
476 499
477 500 text = "\n".join([ln[strip_len:] for ln in lines])
478 501 return text
479 502
480 503
481 def strip_ansi(source):
504 def strip_ansi(source: str) -> str:
482 505 """
483 506 Remove ansi escape codes from text.
484 507
485 508 Parameters
486 509 ----------
487 510 source : str
488 511 Source to remove the ansi from
489 512 """
490 513 warnings.warn(
491 514 "`strip_ansi` is Pending Deprecation since IPython 8.17."
492 515 "It is considered fro removal in in future version. "
493 516 "Please open an issue if you believe it should be kept.",
494 517 stacklevel=2,
495 518 category=PendingDeprecationWarning,
496 519 )
497 520
498 521 return re.sub(r'\033\[(\d|;)+?m', '', source)
499 522
500 523
501 524 class EvalFormatter(Formatter):
502 525 """A String Formatter that allows evaluation of simple expressions.
503 526
504 527 Note that this version interprets a `:` as specifying a format string (as per
505 528 standard string formatting), so if slicing is required, you must explicitly
506 529 create a slice.
507 530
508 531 This is to be used in templating cases, such as the parallel batch
509 532 script templates, where simple arithmetic on arguments is useful.
510 533
511 534 Examples
512 535 --------
513 536 ::
514 537
515 538 In [1]: f = EvalFormatter()
516 539 In [2]: f.format('{n//4}', n=8)
517 540 Out[2]: '2'
518 541
519 542 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
520 543 Out[3]: 'll'
521 544 """
522 def get_field(self, name, args, kwargs):
545
546 def get_field(self, name: str, args: Any, kwargs: Any) -> Tuple[Any, str]:
523 547 v = eval(name, kwargs)
524 548 return v, name
525 549
526 550 #XXX: As of Python 3.4, the format string parsing no longer splits on a colon
527 551 # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and
528 552 # above, it should be possible to remove FullEvalFormatter.
529 553
530 554 class FullEvalFormatter(Formatter):
531 555 """A String Formatter that allows evaluation of simple expressions.
532 556
533 557 Any time a format key is not found in the kwargs,
534 558 it will be tried as an expression in the kwargs namespace.
535 559
536 560 Note that this version allows slicing using [1:2], so you cannot specify
537 561 a format string. Use :class:`EvalFormatter` to permit format strings.
538 562
539 563 Examples
540 564 --------
541 565 ::
542 566
543 567 In [1]: f = FullEvalFormatter()
544 568 In [2]: f.format('{n//4}', n=8)
545 569 Out[2]: '2'
546 570
547 571 In [3]: f.format('{list(range(5))[2:4]}')
548 572 Out[3]: '[2, 3]'
549 573
550 574 In [4]: f.format('{3*2}')
551 575 Out[4]: '6'
552 576 """
553 577 # copied from Formatter._vformat with minor changes to allow eval
554 578 # and replace the format_spec code with slicing
555 579 def vformat(
556 580 self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
557 581 ) -> str:
558 582 result = []
559 583 conversion: Optional[str]
560 584 for literal_text, field_name, format_spec, conversion in self.parse(
561 585 format_string
562 586 ):
563 587 # output the literal text
564 588 if literal_text:
565 589 result.append(literal_text)
566 590
567 591 # if there's a field, output it
568 592 if field_name is not None:
569 593 # this is some markup, find the object and do
570 594 # the formatting
571 595
572 596 if format_spec:
573 597 # override format spec, to allow slicing:
574 598 field_name = ':'.join([field_name, format_spec])
575 599
576 600 # eval the contents of the field for the object
577 601 # to be formatted
578 602 obj = eval(field_name, dict(kwargs))
579 603
580 604 # do any conversion on the resulting object
581 605 # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377
582 606 obj = self.convert_field(obj, conversion) # type: ignore[arg-type]
583 607
584 608 # format the object and append to the result
585 609 result.append(self.format_field(obj, ''))
586 610
587 611 return ''.join(result)
588 612
589 613
590 614 class DollarFormatter(FullEvalFormatter):
591 615 """Formatter allowing Itpl style $foo replacement, for names and attribute
592 616 access only. Standard {foo} replacement also works, and allows full
593 617 evaluation of its arguments.
594 618
595 619 Examples
596 620 --------
597 621 ::
598 622
599 623 In [1]: f = DollarFormatter()
600 624 In [2]: f.format('{n//4}', n=8)
601 625 Out[2]: '2'
602 626
603 627 In [3]: f.format('23 * 76 is $result', result=23*76)
604 628 Out[3]: '23 * 76 is 1748'
605 629
606 630 In [4]: f.format('$a or {b}', a=1, b=2)
607 631 Out[4]: '1 or 2'
608 632 """
609 _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)")
610 def parse(self, fmt_string):
611 for literal_txt, field_name, format_spec, conversion \
612 in Formatter.parse(self, fmt_string):
613
633
634 _dollar_pattern_ignore_single_quote = re.compile(
635 r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)"
636 )
637
638 def parse(self, fmt_string: str) -> Iterator[Tuple[Any, Any, Any, Any]]: # type: ignore
639 for literal_txt, field_name, format_spec, conversion in Formatter.parse(
640 self, fmt_string
641 ):
614 642 # Find $foo patterns in the literal text.
615 643 continue_from = 0
616 644 txt = ""
617 645 for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt):
618 646 new_txt, new_field = m.group(1,2)
619 647 # $$foo --> $foo
620 648 if new_field.startswith("$"):
621 649 txt += new_txt + new_field
622 650 else:
623 651 yield (txt + new_txt, new_field, "", None)
624 652 txt = ""
625 653 continue_from = m.end()
626 654
627 655 # Re-yield the {foo} style pattern
628 656 yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
629 657
630 def __repr__(self):
658 def __repr__(self) -> str:
631 659 return "<DollarFormatter>"
632 660
633 661 #-----------------------------------------------------------------------------
634 662 # Utils to columnize a list of string
635 663 #-----------------------------------------------------------------------------
636 664
637 def _col_chunks(l, max_rows, row_first=False):
665
666 def _col_chunks(
667 l: List[int], max_rows: int, row_first: bool = False
668 ) -> Iterator[List[int]]:
638 669 """Yield successive max_rows-sized column chunks from l."""
639 670 if row_first:
640 671 ncols = (len(l) // max_rows) + (len(l) % max_rows > 0)
641 672 for i in range(ncols):
642 673 yield [l[j] for j in range(i, len(l), ncols)]
643 674 else:
644 675 for i in range(0, len(l), max_rows):
645 676 yield l[i:(i + max_rows)]
646 677
647 678
648 679 def _find_optimal(
649 rlist: List[str], row_first: bool, separator_size: int, displaywidth: int
680 rlist: List[int], row_first: bool, separator_size: int, displaywidth: int
650 681 ) -> Dict[str, Any]:
651 682 """Calculate optimal info to columnize a list of string"""
652 683 for max_rows in range(1, len(rlist) + 1):
653 684 col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first)))
654 685 sumlength = sum(col_widths)
655 686 ncols = len(col_widths)
656 687 if sumlength + separator_size * (ncols - 1) <= displaywidth:
657 688 break
658 689 return {'num_columns': ncols,
659 690 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0,
660 691 'max_rows': max_rows,
661 692 'column_widths': col_widths
662 693 }
663 694
664 695
665 def _get_or_default(mylist, i, default=None):
696 T = TypeVar("T")
697
698
699 def _get_or_default(mylist: List[T], i: int, default: T) -> T:
666 700 """return list item number, or default if don't exist"""
667 701 if i >= len(mylist):
668 702 return default
669 703 else :
670 704 return mylist[i]
671 705
672 706
673 707 def compute_item_matrix(
674 708 items: List[str],
675 709 row_first: bool = False,
676 710 empty: Optional[str] = None,
677 711 *,
678 712 separator_size: int = 2,
679 713 displaywidth: int = 80,
680 714 ) -> Tuple[List[List[int]], Dict[str, int]]:
681 715 """Returns a nested list, and info to columnize items
682 716
683 717 Parameters
684 718 ----------
685 719 items
686 720 list of strings to columize
687 721 row_first : (default False)
688 722 Whether to compute columns for a row-first matrix instead of
689 723 column-first (default).
690 724 empty : (default None)
691 725 default value to fill list if needed
692 726 separator_size : int (default=2)
693 727 How much characters will be used as a separation between each columns.
694 728 displaywidth : int (default=80)
695 729 The width of the area onto which the columns should enter
696 730
697 731 Returns
698 732 -------
699 733 strings_matrix
700 734 nested list of string, the outer most list contains as many list as
701 735 rows, the innermost lists have each as many element as columns. If the
702 736 total number of elements in `items` does not equal the product of
703 737 rows*columns, the last element of some lists are filled with `None`.
704 738 dict_info
705 739 some info to make columnize easier:
706 740
707 741 num_columns
708 742 number of columns
709 743 max_rows
710 744 maximum number of rows (final number may be less)
711 745 column_widths
712 746 list of with of each columns
713 747 optimal_separator_width
714 748 best separator width between columns
715 749
716 750 Examples
717 751 --------
718 752 ::
719 753
720 754 In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
721 755 In [2]: list, info = compute_item_matrix(l, displaywidth=12)
722 756 In [3]: list
723 757 Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]]
724 758 In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5}
725 759 In [5]: all((info[k] == ideal[k] for k in ideal.keys()))
726 760 Out[5]: True
727 761 """
728 762 warnings.warn(
729 763 "`compute_item_matrix` is Pending Deprecation since IPython 8.17."
730 764 "It is considered fro removal in in future version. "
731 765 "Please open an issue if you believe it should be kept.",
732 766 stacklevel=2,
733 767 category=PendingDeprecationWarning,
734 768 )
735 769 info = _find_optimal(
736 770 list(map(len, items)), # type: ignore[arg-type]
737 771 row_first,
738 772 separator_size=separator_size,
739 773 displaywidth=displaywidth,
740 774 )
741 775 nrow, ncol = info["max_rows"], info["num_columns"]
742 776 if row_first:
743 return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info)
777 return (
778 [
779 [
780 _get_or_default(
781 items, r * ncol + c, default=empty
782 ) # type:ignore[misc]
783 for c in range(ncol)
784 ]
785 for r in range(nrow)
786 ],
787 info,
788 )
744 789 else:
745 return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info)
790 return (
791 [
792 [
793 _get_or_default(
794 items, c * nrow + r, default=empty
795 ) # type:ignore[misc]
796 for c in range(ncol)
797 ]
798 for r in range(nrow)
799 ],
800 info,
801 )
746 802
747 803
748 804 def columnize(
749 805 items: List[str],
750 806 row_first: bool = False,
751 807 separator: str = " ",
752 808 displaywidth: int = 80,
753 809 spread: bool = False,
754 810 ) -> str:
755 811 """Transform a list of strings into a single string with columns.
756 812
757 813 Parameters
758 814 ----------
759 815 items : sequence of strings
760 816 The strings to process.
761 817 row_first : (default False)
762 818 Whether to compute columns for a row-first matrix instead of
763 819 column-first (default).
764 820 separator : str, optional [default is two spaces]
765 821 The string that separates columns.
766 822 displaywidth : int, optional [default is 80]
767 823 Width of the display in number of characters.
768 824
769 825 Returns
770 826 -------
771 827 The formatted string.
772 828 """
773 829 warnings.warn(
774 830 "`columnize` is Pending Deprecation since IPython 8.17."
775 831 "It is considered for removal in future versions. "
776 832 "Please open an issue if you believe it should be kept.",
777 833 stacklevel=2,
778 834 category=PendingDeprecationWarning,
779 835 )
780 836 if not items:
781 837 return "\n"
782 838 matrix: List[List[int]]
783 839 matrix, info = compute_item_matrix(
784 840 items,
785 841 row_first=row_first,
786 842 separator_size=len(separator),
787 843 displaywidth=displaywidth,
788 844 )
789 845 if spread:
790 846 separator = separator.ljust(int(info["optimal_separator_width"]))
791 847 fmatrix: List[filter[int]] = [filter(None, x) for x in matrix]
792 848 sjoin = lambda x: separator.join(
793 849 [y.ljust(w, " ") for y, w in zip(x, cast(List[int], info["column_widths"]))]
794 850 )
795 851 return "\n".join(map(sjoin, fmatrix)) + "\n"
796 852
797 853
798 def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""):
854 def get_text_list(
855 list_: List[str], last_sep: str = " and ", sep: str = ", ", wrap_item_with: str = ""
856 ) -> str:
799 857 """
800 858 Return a string with a natural enumeration of items
801 859
802 860 >>> get_text_list(['a', 'b', 'c', 'd'])
803 861 'a, b, c and d'
804 862 >>> get_text_list(['a', 'b', 'c'], ' or ')
805 863 'a, b or c'
806 864 >>> get_text_list(['a', 'b', 'c'], ', ')
807 865 'a, b, c'
808 866 >>> get_text_list(['a', 'b'], ' or ')
809 867 'a or b'
810 868 >>> get_text_list(['a'])
811 869 'a'
812 870 >>> get_text_list([])
813 871 ''
814 872 >>> get_text_list(['a', 'b'], wrap_item_with="`")
815 873 '`a` and `b`'
816 874 >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ")
817 875 'a + b + c = d'
818 876 """
819 877 if len(list_) == 0:
820 878 return ''
821 879 if wrap_item_with:
822 880 list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for
823 881 item in list_]
824 882 if len(list_) == 1:
825 883 return list_[0]
826 884 return '%s%s%s' % (
827 885 sep.join(i for i in list_[:-1]),
828 886 last_sep, list_[-1])
@@ -1,375 +1,385
1 1 [build-system]
2 2 requires = ["setuptools>=61.2"]
3 3 # We need access to the 'setupbase' module at build time.
4 4 # Hence we declare a custom build backend.
5 5 build-backend = "_build_meta" # just re-exports setuptools.build_meta definitions
6 6 backend-path = ["."]
7 7
8 8 [project]
9 9 name = "ipython"
10 10 description = "IPython: Productive Interactive Computing"
11 11 keywords = ["Interactive", "Interpreter", "Shell", "Embedding"]
12 12 classifiers = [
13 13 "Framework :: IPython",
14 14 "Framework :: Jupyter",
15 15 "Intended Audience :: Developers",
16 16 "Intended Audience :: Science/Research",
17 17 "License :: OSI Approved :: BSD License",
18 18 "Programming Language :: Python",
19 19 "Programming Language :: Python :: 3",
20 20 "Programming Language :: Python :: 3 :: Only",
21 21 "Topic :: System :: Shells",
22 22 ]
23 23 requires-python = ">=3.10"
24 24 dependencies = [
25 25 'colorama; sys_platform == "win32"',
26 26 "decorator",
27 27 "exceptiongroup; python_version<'3.11'",
28 28 "jedi>=0.16",
29 29 "matplotlib-inline",
30 30 'pexpect>4.3; sys_platform != "win32" and sys_platform != "emscripten"',
31 31 "prompt_toolkit>=3.0.41,<3.1.0",
32 32 "pygments>=2.4.0",
33 33 "stack_data",
34 34 "traitlets>=5.13.0",
35 35 "typing_extensions>=4.6; python_version<'3.12'",
36 36 ]
37 37 dynamic = ["authors", "license", "version"]
38 38
39 39 [project.entry-points."pygments.lexers"]
40 40 ipythonconsole = "IPython.lib.lexers:IPythonConsoleLexer"
41 41 ipython = "IPython.lib.lexers:IPythonLexer"
42 42 ipython3 = "IPython.lib.lexers:IPython3Lexer"
43 43
44 44 [project.scripts]
45 45 ipython = "IPython:start_ipython"
46 46 ipython3 = "IPython:start_ipython"
47 47
48 48 [project.readme]
49 49 file = "long_description.rst"
50 50 content-type = "text/x-rst"
51 51
52 52 [project.urls]
53 53 Homepage = "https://ipython.org"
54 54 Documentation = "https://ipython.readthedocs.io/"
55 55 Funding = "https://numfocus.org/"
56 56 Source = "https://github.com/ipython/ipython"
57 57 Tracker = "https://github.com/ipython/ipython/issues"
58 58
59 59 [project.optional-dependencies]
60 60 black = [
61 61 "black",
62 62 ]
63 63 doc = [
64 64 "docrepr",
65 65 "exceptiongroup",
66 66 "intersphinx_registry",
67 67 "ipykernel",
68 68 "ipython[test]",
69 69 "matplotlib",
70 70 "setuptools>=18.5",
71 71 "sphinx-rtd-theme",
72 72 "sphinx>=1.3",
73 73 "sphinxcontrib-jquery",
74 74 "tomli ; python_version<'3.11'",
75 75 "typing_extensions",
76 76 ]
77 77 kernel = [
78 78 "ipykernel",
79 79 ]
80 80 nbconvert = [
81 81 "nbconvert",
82 82 ]
83 83 nbformat = [
84 84 "nbformat",
85 85 ]
86 86 notebook = [
87 87 "ipywidgets",
88 88 "notebook",
89 89 ]
90 90 parallel = [
91 91 "ipyparallel",
92 92 ]
93 93 qtconsole = [
94 94 "qtconsole",
95 95 ]
96 96 terminal = []
97 97 test = [
98 98 "pytest",
99 99 "pytest-asyncio<0.22",
100 100 "testpath",
101 101 "pickleshare",
102 102 "packaging",
103 103 ]
104 104 test_extra = [
105 105 "ipython[test]",
106 106 "curio",
107 107 "matplotlib!=3.2.0",
108 108 "nbformat",
109 109 "numpy>=1.23",
110 110 "pandas",
111 111 "trio",
112 112 ]
113 113 matplotlib = [
114 114 "matplotlib"
115 115 ]
116 116 all = [
117 117 "ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,matplotlib]",
118 118 "ipython[test,test_extra]",
119 119 ]
120 120
121 121 [tool.mypy]
122 122 python_version = "3.10"
123 123 ignore_missing_imports = true
124 124 follow_imports = 'silent'
125 125 exclude = [
126 126 'test_\.+\.py',
127 127 'IPython.utils.tests.test_wildcard',
128 128 'testing',
129 129 'tests',
130 130 'PyColorize.py',
131 131 '_process_win32_controller.py',
132 132 'IPython/core/application.py',
133 133 'IPython/core/profileapp.py',
134 134 'IPython/lib/deepreload.py',
135 135 'IPython/sphinxext/ipython_directive.py',
136 136 'IPython/terminal/ipapp.py',
137 137 'IPython/utils/_process_win32.py',
138 138 'IPython/utils/path.py',
139 139 ]
140 140 disallow_untyped_defs = true
141 141 # ignore_errors = false
142 142 # ignore_missing_imports = false
143 143 # disallow_untyped_calls = true
144 144 disallow_incomplete_defs = true
145 145 # check_untyped_defs = true
146 146 # disallow_untyped_decorators = true
147 147 warn_redundant_casts = true
148 148
149 149 [[tool.mypy.overrides]]
150 150 module = [
151 151 "IPython.utils.text",
152 152 ]
153 disallow_untyped_defs = true
154 check_untyped_defs = false
155 disallow_untyped_decorators = true
156
157 [[tool.mypy.overrides]]
158 module = [
159 ]
153 160 disallow_untyped_defs = false
161 ignore_errors = true
162 ignore_missing_imports = true
163 disallow_untyped_calls = false
164 disallow_incomplete_defs = false
154 165 check_untyped_defs = false
155 166 disallow_untyped_decorators = false
156 167
157
158 168 # gloabl ignore error
159 169 [[tool.mypy.overrides]]
160 170 module = [
161 171 "IPython",
162 172 "IPython.conftest",
163 173 "IPython.core.alias",
164 174 "IPython.core.async_helpers",
165 175 "IPython.core.autocall",
166 176 "IPython.core.builtin_trap",
167 177 "IPython.core.compilerop",
168 178 "IPython.core.completer",
169 179 "IPython.core.completerlib",
170 180 "IPython.core.crashhandler",
171 181 "IPython.core.debugger",
172 182 "IPython.core.display",
173 183 "IPython.core.display_functions",
174 184 "IPython.core.display_trap",
175 185 "IPython.core.displayhook",
176 186 "IPython.core.displaypub",
177 187 "IPython.core.events",
178 188 "IPython.core.excolors",
179 189 "IPython.core.extensions",
180 190 "IPython.core.formatters",
181 191 "IPython.core.getipython",
182 192 "IPython.core.guarded_eval",
183 193 "IPython.core.history",
184 194 "IPython.core.historyapp",
185 195 "IPython.core.hooks",
186 196 "IPython.core.inputsplitter",
187 197 "IPython.core.inputtransformer",
188 198 "IPython.core.inputtransformer2",
189 199 "IPython.core.interactiveshell",
190 200 "IPython.core.logger",
191 201 "IPython.core.macro",
192 202 "IPython.core.magic",
193 203 "IPython.core.magic_arguments",
194 204 "IPython.core.magics.ast_mod",
195 205 "IPython.core.magics.auto",
196 206 "IPython.core.magics.basic",
197 207 "IPython.core.magics.code",
198 208 "IPython.core.magics.config",
199 209 "IPython.core.magics.display",
200 210 "IPython.core.magics.execution",
201 211 "IPython.core.magics.extension",
202 212 "IPython.core.magics.history",
203 213 "IPython.core.magics.logging",
204 214 "IPython.core.magics.namespace",
205 215 "IPython.core.magics.osm",
206 216 "IPython.core.magics.packaging",
207 217 "IPython.core.magics.pylab",
208 218 "IPython.core.magics.script",
209 219 "IPython.core.oinspect",
210 220 "IPython.core.page",
211 221 "IPython.core.payload",
212 222 "IPython.core.payloadpage",
213 223 "IPython.core.prefilter",
214 224 "IPython.core.profiledir",
215 225 "IPython.core.prompts",
216 226 "IPython.core.pylabtools",
217 227 "IPython.core.shellapp",
218 228 "IPython.core.splitinput",
219 229 "IPython.core.ultratb",
220 230 "IPython.extensions.autoreload",
221 231 "IPython.extensions.storemagic",
222 232 "IPython.external.qt_for_kernel",
223 233 "IPython.external.qt_loaders",
224 234 "IPython.lib.backgroundjobs",
225 235 "IPython.lib.clipboard",
226 236 "IPython.lib.demo",
227 237 "IPython.lib.display",
228 238 "IPython.lib.editorhooks",
229 239 "IPython.lib.guisupport",
230 240 "IPython.lib.latextools",
231 241 "IPython.lib.lexers",
232 242 "IPython.lib.pretty",
233 243 "IPython.paths",
234 244 "IPython.sphinxext.ipython_console_highlighting",
235 245 "IPython.terminal.debugger",
236 246 "IPython.terminal.embed",
237 247 "IPython.terminal.interactiveshell",
238 248 "IPython.terminal.magics",
239 249 "IPython.terminal.prompts",
240 250 "IPython.terminal.pt_inputhooks",
241 251 "IPython.terminal.pt_inputhooks.asyncio",
242 252 "IPython.terminal.pt_inputhooks.glut",
243 253 "IPython.terminal.pt_inputhooks.gtk",
244 254 "IPython.terminal.pt_inputhooks.gtk3",
245 255 "IPython.terminal.pt_inputhooks.gtk4",
246 256 "IPython.terminal.pt_inputhooks.osx",
247 257 "IPython.terminal.pt_inputhooks.pyglet",
248 258 "IPython.terminal.pt_inputhooks.qt",
249 259 "IPython.terminal.pt_inputhooks.tk",
250 260 "IPython.terminal.pt_inputhooks.wx",
251 261 "IPython.terminal.ptutils",
252 262 "IPython.terminal.shortcuts",
253 263 "IPython.terminal.shortcuts.auto_match",
254 264 "IPython.terminal.shortcuts.auto_suggest",
255 265 "IPython.terminal.shortcuts.filters",
256 266 "IPython.utils._process_cli",
257 267 "IPython.utils._process_common",
258 268 "IPython.utils._process_emscripten",
259 269 "IPython.utils._process_posix",
260 270 "IPython.utils.capture",
261 271 "IPython.utils.coloransi",
262 272 "IPython.utils.contexts",
263 273 "IPython.utils.data",
264 274 "IPython.utils.decorators",
265 275 "IPython.utils.dir2",
266 276 "IPython.utils.encoding",
267 277 "IPython.utils.frame",
268 278 "IPython.utils.generics",
269 279 "IPython.utils.importstring",
270 280 "IPython.utils.io",
271 281 "IPython.utils.ipstruct",
272 282 "IPython.utils.module_paths",
273 283 "IPython.utils.openpy",
274 284 "IPython.utils.process",
275 285 "IPython.utils.py3compat",
276 286 "IPython.utils.sentinel",
277 287 "IPython.utils.shimmodule",
278 288 "IPython.utils.strdispatch",
279 289 "IPython.utils.sysinfo",
280 290 "IPython.utils.syspathcontext",
281 291 "IPython.utils.tempdir",
282 292 "IPython.utils.terminal",
283 293 "IPython.utils.timing",
284 294 "IPython.utils.tokenutil",
285 295 "IPython.utils.tz",
286 296 "IPython.utils.ulinecache",
287 297 "IPython.utils.version",
288 298 "IPython.utils.wildcard",
289 299
290 300 ]
291 301 disallow_untyped_defs = false
292 302 ignore_errors = true
293 303 ignore_missing_imports = true
294 304 disallow_untyped_calls = false
295 305 disallow_incomplete_defs = false
296 306 check_untyped_defs = false
297 307 disallow_untyped_decorators = false
298 308
299 309 [tool.pytest.ini_options]
300 310 addopts = [
301 311 "--durations=10",
302 312 "-pIPython.testing.plugin.pytest_ipdoctest",
303 313 "--ipdoctest-modules",
304 314 "--ignore=docs",
305 315 "--ignore=examples",
306 316 "--ignore=htmlcov",
307 317 "--ignore=ipython_kernel",
308 318 "--ignore=ipython_parallel",
309 319 "--ignore=results",
310 320 "--ignore=tmp",
311 321 "--ignore=tools",
312 322 "--ignore=traitlets",
313 323 "--ignore=IPython/core/tests/daft_extension",
314 324 "--ignore=IPython/sphinxext",
315 325 "--ignore=IPython/terminal/pt_inputhooks",
316 326 "--ignore=IPython/__main__.py",
317 327 "--ignore=IPython/external/qt_for_kernel.py",
318 328 "--ignore=IPython/html/widgets/widget_link.py",
319 329 "--ignore=IPython/html/widgets/widget_output.py",
320 330 "--ignore=IPython/terminal/console.py",
321 331 "--ignore=IPython/utils/_process_cli.py",
322 332 "--ignore=IPython/utils/_process_posix.py",
323 333 "--ignore=IPython/utils/_process_win32.py",
324 334 "--ignore=IPython/utils/_process_win32_controller.py",
325 335 "--ignore=IPython/utils/daemonize.py",
326 336 "--ignore=IPython/utils/eventful.py",
327 337 "--ignore=IPython/kernel",
328 338 "--ignore=IPython/consoleapp.py",
329 339 "--ignore=IPython/core/inputsplitter.py",
330 340 "--ignore=IPython/lib/kernel.py",
331 341 "--ignore=IPython/utils/jsonutil.py",
332 342 "--ignore=IPython/utils/localinterfaces.py",
333 343 "--ignore=IPython/utils/log.py",
334 344 "--ignore=IPython/utils/signatures.py",
335 345 "--ignore=IPython/utils/traitlets.py",
336 346 "--ignore=IPython/utils/version.py"
337 347 ]
338 348 doctest_optionflags = [
339 349 "NORMALIZE_WHITESPACE",
340 350 "ELLIPSIS"
341 351 ]
342 352 ipdoctest_optionflags = [
343 353 "NORMALIZE_WHITESPACE",
344 354 "ELLIPSIS"
345 355 ]
346 356 asyncio_mode = "strict"
347 357
348 358 [tool.pyright]
349 359 pythonPlatform="All"
350 360
351 361 [tool.setuptools]
352 362 zip-safe = false
353 363 platforms = ["Linux", "Mac OSX", "Windows"]
354 364 license-files = ["LICENSE"]
355 365 include-package-data = false
356 366
357 367 [tool.setuptools.packages.find]
358 368 exclude = ["setupext"]
359 369 namespaces = false
360 370
361 371 [tool.setuptools.package-data]
362 372 "IPython" = ["py.typed"]
363 373 "IPython.core" = ["profile/README*"]
364 374 "IPython.core.tests" = ["*.png", "*.jpg", "daft_extension/*.py"]
365 375 "IPython.lib.tests" = ["*.wav"]
366 376 "IPython.testing.plugin" = ["*.txt"]
367 377
368 378 [tool.setuptools.dynamic]
369 379 version = {attr = "IPython.core.release.__version__"}
370 380
371 381 [tool.coverage.run]
372 382 omit = [
373 383 # omit everything in /tmp as we run tempfile
374 384 "/tmp/*",
375 385 ]
General Comments 0
You need to be logged in to leave comments. Login now