text.py
886 lines
| 25.7 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2498 | """ | ||
Utilities for working with strings and text. | ||||
Thomas Kluyver
|
r8795 | |||
Inheritance diagram: | ||||
.. inheritance-diagram:: IPython.utils.text | ||||
:parts: 3 | ||||
Brian Granger
|
r2498 | """ | ||
import os | ||||
import re | ||||
Blazej Michalik
|
r26615 | import string | ||
M Bussonnier
|
r28778 | import sys | ||
MinRK
|
r4020 | import textwrap | ||
Matthias Bussonnier
|
r28434 | import warnings | ||
MinRK
|
r4004 | from string import Formatter | ||
Thomas Kluyver
|
r24258 | from pathlib import Path | ||
Brian Granger
|
r2498 | |||
M Bussonnier
|
r28890 | from typing import ( | ||
List, | ||||
Dict, | ||||
Tuple, | ||||
Optional, | ||||
cast, | ||||
Sequence, | ||||
Mapping, | ||||
Any, | ||||
Union, | ||||
Callable, | ||||
Iterator, | ||||
TypeVar, | ||||
) | ||||
M Bussonnier
|
r28778 | |||
if sys.version_info < (3, 12): | ||||
from typing_extensions import Self | ||||
else: | ||||
from typing import Self | ||||
Matthias Bussonnier
|
r28475 | |||
Brian Granger
|
r2498 | |||
class LSString(str): | ||||
"""String derivative with a special access attributes. | ||||
These are normal strings, but with the special attributes: | ||||
.l (or .list) : value as list (split on newlines). | ||||
.n (or .nlstr): original value (the string itself). | ||||
.s (or .spstr): value as whitespace-separated string. | ||||
MinRK
|
r20811 | .p (or .paths): list of path objects (requires path.py package) | ||
Brian Granger
|
r2498 | |||
Any values which require transformations are computed only once and | ||||
cached. | ||||
Such strings are very useful to efficiently interact with the shell, which | ||||
typically only understands whitespace-separated options for commands.""" | ||||
M Bussonnier
|
r28778 | __list: List[str] | ||
__spstr: str | ||||
__paths: List[Path] | ||||
def get_list(self) -> List[str]: | ||||
Brian Granger
|
r2498 | try: | ||
return self.__list | ||||
except AttributeError: | ||||
self.__list = self.split('\n') | ||||
return self.__list | ||||
l = list = property(get_list) | ||||
M Bussonnier
|
r28778 | def get_spstr(self) -> str: | ||
Brian Granger
|
r2498 | try: | ||
return self.__spstr | ||||
except AttributeError: | ||||
self.__spstr = self.replace('\n',' ') | ||||
return self.__spstr | ||||
s = spstr = property(get_spstr) | ||||
M Bussonnier
|
r28778 | def get_nlstr(self) -> Self: | ||
Brian Granger
|
r2498 | return self | ||
n = nlstr = property(get_nlstr) | ||||
M Bussonnier
|
r28778 | def get_paths(self) -> List[Path]: | ||
Brian Granger
|
r2498 | try: | ||
return self.__paths | ||||
except AttributeError: | ||||
Min RK
|
r22249 | self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)] | ||
Brian Granger
|
r2498 | return self.__paths | ||
p = paths = property(get_paths) | ||||
Brian Granger
|
r2781 | # FIXME: We need to reimplement type specific displayhook and then add this | ||
# back as a custom printer. This should also be moved outside utils into the | ||||
# core. | ||||
Brian Granger
|
r2498 | |||
Brian Granger
|
r2781 | # def print_lsstring(arg): | ||
# """ Prettier (non-repr-like) and more informative printer for LSString """ | ||||
Antony Lee
|
r28756 | # print("LSString (.p, .n, .l, .s available). Value:") | ||
# print(arg) | ||||
Bernardo B. Marques
|
r4872 | # | ||
# | ||||
Hugo
|
r24687 | # print_lsstring = result_display.register(LSString)(print_lsstring) | ||
Brian Granger
|
r2498 | |||
class SList(list): | ||||
"""List derivative with a special access attributes. | ||||
These are normal lists, but with the special attributes: | ||||
Thomas Kluyver
|
r13592 | * .l (or .list) : value as list (the list itself). | ||
* .n (or .nlstr): value as a string, joined on newlines. | ||||
* .s (or .spstr): value as a string, joined on spaces. | ||||
MinRK
|
r20811 | * .p (or .paths): list of path objects (requires path.py package) | ||
Brian Granger
|
r2498 | |||
Any values which require transformations are computed only once and | ||||
cached.""" | ||||
M Bussonnier
|
r28778 | __spstr: str | ||
__nlstr: str | ||||
__paths: List[Path] | ||||
def get_list(self) -> Self: | ||||
Brian Granger
|
r2498 | return self | ||
l = list = property(get_list) | ||||
M Bussonnier
|
r28778 | def get_spstr(self) -> str: | ||
Brian Granger
|
r2498 | try: | ||
return self.__spstr | ||||
except AttributeError: | ||||
self.__spstr = ' '.join(self) | ||||
return self.__spstr | ||||
s = spstr = property(get_spstr) | ||||
M Bussonnier
|
r28778 | def get_nlstr(self) -> str: | ||
Brian Granger
|
r2498 | try: | ||
return self.__nlstr | ||||
except AttributeError: | ||||
self.__nlstr = '\n'.join(self) | ||||
return self.__nlstr | ||||
n = nlstr = property(get_nlstr) | ||||
M Bussonnier
|
r28778 | def get_paths(self) -> List[Path]: | ||
Brian Granger
|
r2498 | try: | ||
return self.__paths | ||||
except AttributeError: | ||||
Min RK
|
r22249 | self.__paths = [Path(p) for p in self if os.path.exists(p)] | ||
Brian Granger
|
r2498 | return self.__paths | ||
p = paths = property(get_paths) | ||||
M Bussonnier
|
r28890 | def grep( | ||
self, | ||||
pattern: Union[str, Callable[[Any], re.Match[str] | None]], | ||||
prune: bool = False, | ||||
field: Optional[int] = None, | ||||
) -> Self: | ||||
"""Return all strings matching 'pattern' (a regex or callable) | ||||
Brian Granger
|
r2498 | |||
This is case-insensitive. If prune is true, return all items | ||||
NOT matching the pattern. | ||||
If field is specified, the match must occur in the specified | ||||
whitespace-separated field. | ||||
Examples:: | ||||
a.grep( lambda x: x.startswith('C') ) | ||||
a.grep('Cha.*log', prune=1) | ||||
a.grep('chm', field=-1) | ||||
""" | ||||
M Bussonnier
|
r28890 | def match_target(s: str) -> str: | ||
Brian Granger
|
r2498 | if field is None: | ||
return s | ||||
parts = s.split() | ||||
try: | ||||
tgt = parts[field] | ||||
return tgt | ||||
except IndexError: | ||||
return "" | ||||
Srinivas Reddy Thatiparthy
|
r23037 | if isinstance(pattern, str): | ||
Brian Granger
|
r2498 | pred = lambda x : re.search(pattern, x, re.IGNORECASE) | ||
else: | ||||
pred = pattern | ||||
if not prune: | ||||
M Bussonnier
|
r28890 | return type(self)([el for el in self if pred(match_target(el))]) | ||
Brian Granger
|
r2498 | else: | ||
M Bussonnier
|
r28890 | return type(self)([el for el in self if not pred(match_target(el))]) | ||
Brian Granger
|
r2498 | |||
M Bussonnier
|
r28890 | def fields(self, *fields: List[str]) -> List[List[str]]: | ||
"""Collect whitespace-separated fields from string list | ||||
Brian Granger
|
r2498 | |||
Allows quick awk-like usage of string lists. | ||||
Example data (in var a, created by 'a = !ls -l'):: | ||||
Thomas Kluyver
|
r13592 | |||
Brian Granger
|
r2498 | -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog | ||
drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython | ||||
Thomas Kluyver
|
r13592 | * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']`` | ||
* ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']`` | ||||
(note the joining by space). | ||||
* ``a.fields(-1)`` is ``['ChangeLog', 'IPython']`` | ||||
Brian Granger
|
r2498 | |||
IndexErrors are ignored. | ||||
Without args, fields() just split()'s the strings. | ||||
""" | ||||
if len(fields) == 0: | ||||
return [el.split() for el in self] | ||||
res = SList() | ||||
for el in [f.split() for f in self]: | ||||
lineparts = [] | ||||
for fd in fields: | ||||
try: | ||||
lineparts.append(el[fd]) | ||||
except IndexError: | ||||
pass | ||||
if lineparts: | ||||
res.append(" ".join(lineparts)) | ||||
return res | ||||
M Bussonnier
|
r28890 | def sort( # type:ignore[override] | ||
self, | ||||
field: Optional[List[str]] = None, | ||||
nums: bool = False, | ||||
) -> Self: | ||||
"""sort by specified fields (see fields()) | ||||
Brian Granger
|
r2498 | |||
Example:: | ||||
Thomas Kluyver
|
r13807 | |||
Brian Granger
|
r2498 | a.sort(1, nums = True) | ||
Sorts a by second field, in numerical order (so that 21 > 3) | ||||
""" | ||||
#decorate, sort, undecorate | ||||
if field is not None: | ||||
dsu = [[SList([line]).fields(field), line] for line in self] | ||||
else: | ||||
dsu = [[line, line] for line in self] | ||||
if nums: | ||||
for i in range(len(dsu)): | ||||
numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()]) | ||||
try: | ||||
n = int(numstr) | ||||
except ValueError: | ||||
Rémy Léone
|
r21782 | n = 0 | ||
Brian Granger
|
r2498 | dsu[i][0] = n | ||
dsu.sort() | ||||
M Bussonnier
|
r28890 | return type(self)([t[1] for t in dsu]) | ||
Brian Granger
|
r2498 | |||
Brian Granger
|
r2781 | # FIXME: We need to reimplement type specific displayhook and then add this | ||
# back as a custom printer. This should also be moved outside utils into the | ||||
# core. | ||||
# def print_slist(arg): | ||||
# """ Prettier (non-repr-like) and more informative printer for SList """ | ||||
Antony Lee
|
r28756 | # print("SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):") | ||
Brian Granger
|
r2781 | # if hasattr(arg, 'hideonce') and arg.hideonce: | ||
# arg.hideonce = False | ||||
# return | ||||
Bernardo B. Marques
|
r4872 | # | ||
Thomas Kluyver
|
r9474 | # nlprint(arg) # This was a nested list printer, now removed. | ||
Bernardo B. Marques
|
r4872 | # | ||
Hugo
|
r24687 | # print_slist = result_display.register(SList)(print_slist) | ||
Brian Granger
|
r2498 | |||
M Bussonnier
|
r28890 | def indent(instr: str, nspaces: int = 4, ntabs: int = 0, flatten: bool = False) -> str: | ||
Brian Granger
|
r2498 | """Indent a string a given number of spaces or tabstops. | ||
indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | Parameters | ||
---------- | ||||
instr : basestring | ||||
The string to be indented. | ||||
nspaces : int (default: 4) | ||||
The number of spaces to be indented. | ||||
ntabs : int (default: 0) | ||||
The number of tabs to be indented. | ||||
flatten : bool (default: False) | ||||
Whether to scrub existing indentation. If True, all lines will be | ||||
aligned to the same indentation. If False, existing indentation will | ||||
be strictly increased. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3943 | Returns | ||
------- | ||||
M Bussonnier
|
r28890 | str : string indented by ntabs and nspaces. | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2498 | """ | ||
MinRK
|
r3943 | if instr is None: | ||
Brian Granger
|
r2498 | return | ||
ind = '\t'*ntabs+' '*nspaces | ||||
MinRK
|
r3943 | if flatten: | ||
pat = re.compile(r'^\s*', re.MULTILINE) | ||||
else: | ||||
pat = re.compile(r'^', re.MULTILINE) | ||||
outstr = re.sub(pat, ind, instr) | ||||
Brian Granger
|
r2498 | if outstr.endswith(os.linesep+ind): | ||
return outstr[:-len(ind)] | ||||
else: | ||||
return outstr | ||||
M Bussonnier
|
r28890 | def list_strings(arg: Union[str, List[str]]) -> List[str]: | ||
Brian Granger
|
r2498 | """Always return a list of strings, given a string or list of strings | ||
as input. | ||||
Thomas Kluyver
|
r13914 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
Brian Granger
|
r2498 | |||
In [7]: list_strings('A single string') | ||||
Out[7]: ['A single string'] | ||||
In [8]: list_strings(['A single string in a list']) | ||||
Out[8]: ['A single string in a list'] | ||||
In [9]: list_strings(['A','list','of','strings']) | ||||
Out[9]: ['A', 'list', 'of', 'strings'] | ||||
""" | ||||
Srinivas Reddy Thatiparthy
|
r23037 | if isinstance(arg, str): | ||
return [arg] | ||||
else: | ||||
return arg | ||||
Brian Granger
|
r2498 | |||
M Bussonnier
|
r28890 | def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str: | ||
Brian Granger
|
r2498 | """Return the input string centered in a 'marquee'. | ||
Thomas Kluyver
|
r13914 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
Brian Granger
|
r2498 | |||
In [16]: marquee('A test',40) | ||||
Out[16]: '**************** A test ****************' | ||||
In [17]: marquee('A test',40,'-') | ||||
Out[17]: '---------------- A test ----------------' | ||||
In [18]: marquee('A test',40,' ') | ||||
Out[18]: ' A test ' | ||||
""" | ||||
if not txt: | ||||
return (mark*width)[:width] | ||||
Thomas Kluyver
|
r4732 | nmark = (width-len(txt)-2)//len(mark)//2 | ||
Brian Granger
|
r2498 | if nmark < 0: nmark =0 | ||
marks = mark*nmark | ||||
return '%s %s %s' % (marks,txt,marks) | ||||
Brian Granger
|
r2761 | ini_spaces_re = re.compile(r'^(\s+)') | ||
M Bussonnier
|
r28890 | |||
def num_ini_spaces(strng: str) -> int: | ||||
Brian Granger
|
r2761 | """Return the number of initial spaces in a string""" | ||
Matthias Bussonnier
|
r28434 | warnings.warn( | ||
"`num_ini_spaces` is Pending Deprecation since IPython 8.17." | ||||
Andrew Kreimer
|
r28885 | "It is considered for removal in in future version. " | ||
Matthias Bussonnier
|
r28434 | "Please open an issue if you believe it should be kept.", | ||
stacklevel=2, | ||||
category=PendingDeprecationWarning, | ||||
) | ||||
Brian Granger
|
r2761 | ini_spaces = ini_spaces_re.match(strng) | ||
if ini_spaces: | ||||
return ini_spaces.end() | ||||
else: | ||||
return 0 | ||||
Fernando Perez
|
r2929 | |||
M Bussonnier
|
r28890 | def format_screen(strng: str) -> str: | ||
Fernando Perez
|
r2929 | """Format a string for screen printing. | ||
This removes some latex-type format codes.""" | ||||
# Paragraph continue | ||||
par_re = re.compile(r'\\$',re.MULTILINE) | ||||
strng = par_re.sub('',strng) | ||||
return strng | ||||
Fernando Perez
|
r7713 | |||
Matthias Bussonnier
|
r28563 | def dedent(text: str) -> str: | ||
MinRK
|
r4020 | """Equivalent of textwrap.dedent that ignores unindented first line. | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | This means it will still dedent strings like: | ||
'''foo | ||||
is a bar | ||||
''' | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | For use in wrap_paragraphs. | ||
""" | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | if text.startswith('\n'): | ||
# text starts with blank line, don't ignore the first line | ||||
return textwrap.dedent(text) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | # split first line | ||
splits = text.split('\n',1) | ||||
if len(splits) == 1: | ||||
# only one line | ||||
return textwrap.dedent(text) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | first, rest = splits | ||
# dedent everything but the first line | ||||
rest = textwrap.dedent(rest) | ||||
return '\n'.join([first, rest]) | ||||
Fernando Perez
|
r7713 | |||
M Bussonnier
|
r28890 | def wrap_paragraphs(text: str, ncols: int = 80) -> List[str]: | ||
MinRK
|
r4020 | """Wrap multiple paragraphs to fit a specified width. | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | This is equivalent to textwrap.wrap, but with support for multiple | ||
paragraphs, as separated by empty lines. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | Returns | ||
------- | ||||
list of complete paragraphs, wrapped to fill `ncols` columns. | ||||
""" | ||||
Matthias Bussonnier
|
r28434 | warnings.warn( | ||
"`wrap_paragraphs` is Pending Deprecation since IPython 8.17." | ||||
Andrew Kreimer
|
r28885 | "It is considered for removal in in future version. " | ||
Matthias Bussonnier
|
r28434 | "Please open an issue if you believe it should be kept.", | ||
stacklevel=2, | ||||
category=PendingDeprecationWarning, | ||||
) | ||||
MinRK
|
r4020 | paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE) | ||
text = dedent(text).strip() | ||||
paragraphs = paragraph_re.split(text)[::2] # every other entry is space | ||||
out_ps = [] | ||||
indent_re = re.compile(r'\n\s+', re.MULTILINE) | ||||
for p in paragraphs: | ||||
Bernardo B. Marques
|
r4872 | # presume indentation that survives dedent is meaningful formatting, | ||
MinRK
|
r4020 | # so don't fill unless text is flush. | ||
if indent_re.search(p) is None: | ||||
# wrap paragraph | ||||
p = textwrap.fill(p, ncols) | ||||
out_ps.append(p) | ||||
return out_ps | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4020 | |||
M Bussonnier
|
r28890 | def strip_email_quotes(text: str) -> str: | ||
Fernando Perez
|
r7713 | """Strip leading email quotation characters ('>'). | ||
Fernando Perez
|
r7719 | |||
Removes any combination of leading '>' interspersed with whitespace that | ||||
appears *identically* in all lines of the input text. | ||||
Parameters | ||||
---------- | ||||
text : str | ||||
Examples | ||||
-------- | ||||
Simple uses:: | ||||
In [2]: strip_email_quotes('> > text') | ||||
Out[2]: 'text' | ||||
Fernando Perez
|
r7720 | In [3]: strip_email_quotes('> > text\\n> > more') | ||
Out[3]: 'text\\nmore' | ||||
Fernando Perez
|
r7719 | |||
Note how only the common prefix that appears in all lines is stripped:: | ||||
Fernando Perez
|
r7720 | In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') | ||
Out[4]: '> text\\n> more\\nmore...' | ||||
Fernando Perez
|
r7719 | |||
Blazej Michalik
|
r26615 | So if any line has no quote marks ('>'), then none are stripped from any | ||
Fernando Perez
|
r7719 | of them :: | ||
Blazej Michalik
|
r26615 | |||
Fernando Perez
|
r7720 | In [5]: strip_email_quotes('> > text\\n> > more\\nlast different') | ||
Out[5]: '> > text\\n> > more\\nlast different' | ||||
Fernando Perez
|
r7713 | """ | ||
lines = text.splitlines() | ||||
Blazej Michalik
|
r26615 | strip_len = 0 | ||
for characters in zip(*lines): | ||||
# Check if all characters in this position are the same | ||||
if len(set(characters)) > 1: | ||||
break | ||||
Blazej Michalik
|
r26619 | prefix_char = characters[0] | ||
Blazej Michalik
|
r26615 | |||
if prefix_char in string.whitespace or prefix_char == ">": | ||||
strip_len += 1 | ||||
Fernando Perez
|
r7713 | else: | ||
break | ||||
Blazej Michalik
|
r26615 | |||
text = "\n".join([ln[strip_len:] for ln in lines]) | ||||
Fernando Perez
|
r7713 | return text | ||
Blazej Michalik
|
r26615 | |||
M Bussonnier
|
r28890 | def strip_ansi(source: str) -> str: | ||
Thomas Kluyver
|
r16980 | """ | ||
Remove ansi escape codes from text. | ||||
Matthias Bussonnier
|
r26419 | |||
Thomas Kluyver
|
r16980 | Parameters | ||
---------- | ||||
source : str | ||||
Source to remove the ansi from | ||||
""" | ||||
Matthias Bussonnier
|
r28434 | warnings.warn( | ||
"`strip_ansi` is Pending Deprecation since IPython 8.17." | ||||
Andrew Kreimer
|
r28885 | "It is considered for removal in in future version. " | ||
Matthias Bussonnier
|
r28434 | "Please open an issue if you believe it should be kept.", | ||
stacklevel=2, | ||||
category=PendingDeprecationWarning, | ||||
) | ||||
Thomas Kluyver
|
r16980 | return re.sub(r'\033\[(\d|;)+?m', '', source) | ||
Fernando Perez
|
r7713 | |||
MinRK
|
r4004 | class EvalFormatter(Formatter): | ||
"""A String Formatter that allows evaluation of simple expressions. | ||||
Matthias Bussonnier
|
r27639 | |||
Matthias Bussonnier
|
r27531 | Note that this version interprets a `:` as specifying a format string (as per | ||
Thomas Kluyver
|
r5353 | standard string formatting), so if slicing is required, you must explicitly | ||
create a slice. | ||||
Matthias Bussonnier
|
r27639 | |||
MinRK
|
r4004 | This is to be used in templating cases, such as the parallel batch | ||
script templates, where simple arithmetic on arguments is useful. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4004 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
In [1]: f = EvalFormatter() | ||||
In [2]: f.format('{n//4}', n=8) | ||||
Out[2]: '2' | ||||
In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello") | ||||
Out[3]: 'll' | ||||
Thomas Kluyver
|
r5353 | """ | ||
M Bussonnier
|
r28890 | |||
def get_field(self, name: str, args: Any, kwargs: Any) -> Tuple[Any, str]: | ||||
Thomas Kluyver
|
r5353 | v = eval(name, kwargs) | ||
return v, name | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r14002 | #XXX: As of Python 3.4, the format string parsing no longer splits on a colon | ||
# inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and | ||||
# above, it should be possible to remove FullEvalFormatter. | ||||
Fernando Perez
|
r7719 | |||
Thomas Kluyver
|
r5353 | class FullEvalFormatter(Formatter): | ||
"""A String Formatter that allows evaluation of simple expressions. | ||||
Marin Gilles
|
r21541 | |||
Thomas Kluyver
|
r5353 | Any time a format key is not found in the kwargs, | ||
it will be tried as an expression in the kwargs namespace. | ||||
Marin Gilles
|
r21541 | |||
Thomas Kluyver
|
r5353 | Note that this version allows slicing using [1:2], so you cannot specify | ||
a format string. Use :class:`EvalFormatter` to permit format strings. | ||||
Marin Gilles
|
r21541 | |||
Thomas Kluyver
|
r5353 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
In [1]: f = FullEvalFormatter() | ||||
In [2]: f.format('{n//4}', n=8) | ||||
Paul Ivanov
|
r22976 | Out[2]: '2' | ||
Thomas Kluyver
|
r13807 | |||
In [3]: f.format('{list(range(5))[2:4]}') | ||||
Paul Ivanov
|
r22976 | Out[3]: '[2, 3]' | ||
MinRK
|
r4004 | |||
Thomas Kluyver
|
r13807 | In [4]: f.format('{3*2}') | ||
Paul Ivanov
|
r22976 | Out[4]: '6' | ||
MinRK
|
r4004 | """ | ||
MinRK
|
r4654 | # copied from Formatter._vformat with minor changes to allow eval | ||
# and replace the format_spec code with slicing | ||||
M Bussonnier
|
r28778 | def vformat( | ||
self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any] | ||||
) -> str: | ||||
MinRK
|
r4654 | result = [] | ||
Matthias Bussonnier
|
r28626 | conversion: Optional[str] | ||
for literal_text, field_name, format_spec, conversion in self.parse( | ||||
format_string | ||||
): | ||||
MinRK
|
r4654 | # output the literal text | ||
if literal_text: | ||||
result.append(literal_text) | ||||
# if there's a field, output it | ||||
if field_name is not None: | ||||
# this is some markup, find the object and do | ||||
Thomas Kluyver
|
r5353 | # the formatting | ||
MinRK
|
r4654 | |||
Thomas Kluyver
|
r5353 | if format_spec: | ||
MinRK
|
r4654 | # override format spec, to allow slicing: | ||
field_name = ':'.join([field_name, format_spec]) | ||||
# eval the contents of the field for the object | ||||
# to be formatted | ||||
M Bussonnier
|
r28778 | obj = eval(field_name, dict(kwargs)) | ||
MinRK
|
r4654 | |||
# do any conversion on the resulting object | ||||
Matthias Bussonnier
|
r28626 | # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377 | ||
obj = self.convert_field(obj, conversion) # type: ignore[arg-type] | ||||
MinRK
|
r4654 | |||
# format the object and append to the result | ||||
Thomas Kluyver
|
r5353 | result.append(self.format_field(obj, '')) | ||
MinRK
|
r4654 | |||
Matthias Bussonnier
|
r25343 | return ''.join(result) | ||
MinRK
|
r4654 | |||
Fernando Perez
|
r7719 | |||
Thomas Kluyver
|
r5354 | class DollarFormatter(FullEvalFormatter): | ||
"""Formatter allowing Itpl style $foo replacement, for names and attribute | ||||
access only. Standard {foo} replacement also works, and allows full | ||||
evaluation of its arguments. | ||||
Examples | ||||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
In [1]: f = DollarFormatter() | ||||
In [2]: f.format('{n//4}', n=8) | ||||
Paul Ivanov
|
r22976 | Out[2]: '2' | ||
Thomas Kluyver
|
r13807 | |||
In [3]: f.format('23 * 76 is $result', result=23*76) | ||||
Paul Ivanov
|
r22976 | Out[3]: '23 * 76 is 1748' | ||
Thomas Kluyver
|
r13807 | |||
In [4]: f.format('$a or {b}', a=1, b=2) | ||||
Paul Ivanov
|
r22976 | Out[4]: '1 or 2' | ||
Thomas Kluyver
|
r5354 | """ | ||
M Bussonnier
|
r28890 | |||
_dollar_pattern_ignore_single_quote = re.compile( | ||||
r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)" | ||||
) | ||||
def parse(self, fmt_string: str) -> Iterator[Tuple[Any, Any, Any, Any]]: # type: ignore | ||||
for literal_txt, field_name, format_spec, conversion in Formatter.parse( | ||||
self, fmt_string | ||||
): | ||||
Thomas Kluyver
|
r5354 | # Find $foo patterns in the literal text. | ||
continue_from = 0 | ||||
Thomas Kluyver
|
r5365 | txt = "" | ||
ICanWaitAndFishAllDay
|
r23620 | for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt): | ||
Thomas Kluyver
|
r5354 | new_txt, new_field = m.group(1,2) | ||
Thomas Kluyver
|
r5365 | # $$foo --> $foo | ||
if new_field.startswith("$"): | ||||
txt += new_txt + new_field | ||||
else: | ||||
yield (txt + new_txt, new_field, "", None) | ||||
txt = "" | ||||
Thomas Kluyver
|
r5354 | continue_from = m.end() | ||
Marin Gilles
|
r21541 | |||
Thomas Kluyver
|
r5354 | # Re-yield the {foo} style pattern | ||
Thomas Kluyver
|
r5365 | yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) | ||
MinRK
|
r4004 | |||
M Bussonnier
|
r28890 | def __repr__(self) -> str: | ||
Matthias Bussonnier
|
r27136 | return "<DollarFormatter>" | ||
Matthias BUSSONNIER
|
r7387 | #----------------------------------------------------------------------------- | ||
# Utils to columnize a list of string | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r7719 | |||
M Bussonnier
|
r28890 | |||
def _col_chunks( | ||||
l: List[int], max_rows: int, row_first: bool = False | ||||
) -> Iterator[List[int]]: | ||||
naught101
|
r21618 | """Yield successive max_rows-sized column chunks from l.""" | ||
naught101
|
r21613 | if row_first: | ||
naught101
|
r21618 | ncols = (len(l) // max_rows) + (len(l) % max_rows > 0) | ||
Srinivas Reddy Thatiparthy
|
r23036 | for i in range(ncols): | ||
yield [l[j] for j in range(i, len(l), ncols)] | ||||
naught101
|
r21613 | else: | ||
Srinivas Reddy Thatiparthy
|
r23036 | for i in range(0, len(l), max_rows): | ||
naught101
|
r21618 | yield l[i:(i + max_rows)] | ||
Matthias BUSSONNIER
|
r7387 | |||
Fernando Perez
|
r7719 | |||
M Bussonnier
|
r28778 | def _find_optimal( | ||
M Bussonnier
|
r28890 | rlist: List[int], row_first: bool, separator_size: int, displaywidth: int | ||
M Bussonnier
|
r28778 | ) -> Dict[str, Any]: | ||
Matthias BUSSONNIER
|
r7387 | """Calculate optimal info to columnize a list of string""" | ||
naught101
|
r21618 | for max_rows in range(1, len(rlist) + 1): | ||
col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first))) | ||||
naught101
|
r21613 | sumlength = sum(col_widths) | ||
ncols = len(col_widths) | ||||
if sumlength + separator_size * (ncols - 1) <= displaywidth: | ||||
break | ||||
return {'num_columns': ncols, | ||||
Paul Ivanov
|
r22980 | 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0, | ||
naught101
|
r21618 | 'max_rows': max_rows, | ||
naught101
|
r21613 | 'column_widths': col_widths | ||
} | ||||
Matthias BUSSONNIER
|
r7387 | |||
Fernando Perez
|
r7719 | |||
M Bussonnier
|
r28890 | T = TypeVar("T") | ||
def _get_or_default(mylist: List[T], i: int, default: T) -> T: | ||||
Matthias BUSSONNIER
|
r7387 | """return list item number, or default if don't exist""" | ||
if i >= len(mylist): | ||||
return default | ||||
else : | ||||
return mylist[i] | ||||
Fernando Perez
|
r7719 | |||
Matthias Bussonnier
|
r28480 | def compute_item_matrix( | ||
M Bussonnier
|
r28778 | items: List[str], | ||
row_first: bool = False, | ||||
empty: Optional[str] = None, | ||||
*, | ||||
separator_size: int = 2, | ||||
displaywidth: int = 80, | ||||
Matthias Bussonnier
|
r28480 | ) -> Tuple[List[List[int]], Dict[str, int]]: | ||
Matthias BUSSONNIER
|
r7388 | """Returns a nested list, and info to columnize items | ||
Thomas Kluyver
|
r9244 | Parameters | ||
---------- | ||||
Thomas Kluyver
|
r13807 | items | ||
Matthias BUSSONNIER
|
r7388 | list of strings to columize | ||
naught101
|
r21613 | row_first : (default False) | ||
naught101
|
r21615 | Whether to compute columns for a row-first matrix instead of | ||
naught101
|
r21613 | column-first (default). | ||
Matthias BUSSONNIER
|
r7393 | empty : (default None) | ||
default value to fill list if needed | ||||
Matthias BUSSONNIER
|
r7388 | separator_size : int (default=2) | ||
luz.paz
|
r24322 | How much characters will be used as a separation between each columns. | ||
Matthias BUSSONNIER
|
r7388 | displaywidth : int (default=80) | ||
luzpaz
|
r24084 | The width of the area onto which the columns should enter | ||
Matthias BUSSONNIER
|
r7388 | |||
Thomas Kluyver
|
r9244 | Returns | ||
------- | ||||
Thomas Kluyver
|
r13807 | strings_matrix | ||
Matthias BUSSONNIER
|
r7388 | nested list of string, the outer most list contains as many list as | ||
luzpaz
|
r24084 | rows, the innermost lists have each as many element as columns. If the | ||
Matthias BUSSONNIER
|
r7388 | total number of elements in `items` does not equal the product of | ||
rows*columns, the last element of some lists are filled with `None`. | ||||
Thomas Kluyver
|
r13807 | dict_info | ||
Matthias BUSSONNIER
|
r7388 | some info to make columnize easier: | ||
naught101
|
r21613 | num_columns | ||
Thomas Kluyver
|
r13807 | number of columns | ||
naught101
|
r21618 | max_rows | ||
maximum number of rows (final number may be less) | ||||
naught101
|
r21613 | column_widths | ||
Thomas Kluyver
|
r13807 | list of with of each columns | ||
optimal_separator_width | ||||
best separator width between columns | ||||
Matthias BUSSONNIER
|
r7388 | |||
Thomas Kluyver
|
r9244 | Examples | ||
-------- | ||||
Thomas Kluyver
|
r13807 | :: | ||
In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] | ||||
Paul Ivanov
|
r22980 | In [2]: list, info = compute_item_matrix(l, displaywidth=12) | ||
In [3]: list | ||||
Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]] | ||||
In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5} | ||||
In [5]: all((info[k] == ideal[k] for k in ideal.keys())) | ||||
Out[5]: True | ||||
Matthias BUSSONNIER
|
r7387 | """ | ||
Matthias Bussonnier
|
r28434 | warnings.warn( | ||
"`compute_item_matrix` is Pending Deprecation since IPython 8.17." | ||||
Andrew Kreimer
|
r28885 | "It is considered for removal in in future version. " | ||
Matthias Bussonnier
|
r28434 | "Please open an issue if you believe it should be kept.", | ||
stacklevel=2, | ||||
category=PendingDeprecationWarning, | ||||
) | ||||
Matthias Bussonnier
|
r28480 | info = _find_optimal( | ||
M Bussonnier
|
r28778 | list(map(len, items)), # type: ignore[arg-type] | ||
Matthias Bussonnier
|
r28480 | row_first, | ||
separator_size=separator_size, | ||||
displaywidth=displaywidth, | ||||
) | ||||
nrow, ncol = info["max_rows"], info["num_columns"] | ||||
naught101
|
r21613 | if row_first: | ||
M Bussonnier
|
r28890 | return ( | ||
[ | ||||
[ | ||||
_get_or_default( | ||||
items, r * ncol + c, default=empty | ||||
) # type:ignore[misc] | ||||
for c in range(ncol) | ||||
] | ||||
for r in range(nrow) | ||||
], | ||||
info, | ||||
) | ||||
naught101
|
r21613 | else: | ||
M Bussonnier
|
r28890 | return ( | ||
[ | ||||
[ | ||||
_get_or_default( | ||||
items, c * nrow + r, default=empty | ||||
) # type:ignore[misc] | ||||
for c in range(ncol) | ||||
] | ||||
for r in range(nrow) | ||||
], | ||||
info, | ||||
) | ||||
MinRK
|
r4004 | |||
Fernando Perez
|
r7719 | |||
Matthias Bussonnier
|
r28626 | def columnize( | ||
items: List[str], | ||||
row_first: bool = False, | ||||
separator: str = " ", | ||||
displaywidth: int = 80, | ||||
spread: bool = False, | ||||
M Bussonnier
|
r28778 | ) -> str: | ||
Matthias Bussonnier
|
r27639 | """Transform a list of strings into a single string with columns. | ||
Fernando Perez
|
r4537 | |||
Parameters | ||||
---------- | ||||
items : sequence of strings | ||||
The strings to process. | ||||
naught101
|
r21613 | row_first : (default False) | ||
naught101
|
r21615 | Whether to compute columns for a row-first matrix instead of | ||
naught101
|
r21613 | column-first (default). | ||
Fernando Perez
|
r4537 | separator : str, optional [default is two spaces] | ||
The string that separates columns. | ||||
displaywidth : int, optional [default is 80] | ||||
Width of the display in number of characters. | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r4537 | Returns | ||
------- | ||||
The formatted string. | ||||
""" | ||||
Matthias Bussonnier
|
r28434 | warnings.warn( | ||
"`columnize` is Pending Deprecation since IPython 8.17." | ||||
Matthias Bussonnier
|
r28626 | "It is considered for removal in future versions. " | ||
Matthias Bussonnier
|
r28434 | "Please open an issue if you believe it should be kept.", | ||
stacklevel=2, | ||||
category=PendingDeprecationWarning, | ||||
) | ||||
naught101
|
r21613 | if not items: | ||
Matthias Bussonnier
|
r28480 | return "\n" | ||
matrix: List[List[int]] | ||||
matrix, info = compute_item_matrix( | ||||
items, | ||||
row_first=row_first, | ||||
separator_size=len(separator), | ||||
displaywidth=displaywidth, | ||||
) | ||||
naught101
|
r21621 | if spread: | ||
Matthias Bussonnier
|
r28480 | separator = separator.ljust(int(info["optimal_separator_width"])) | ||
fmatrix: List[filter[int]] = [filter(None, x) for x in matrix] | ||||
sjoin = lambda x: separator.join( | ||||
Matthias Bussonnier
|
r28626 | [y.ljust(w, " ") for y, w in zip(x, cast(List[int], info["column_widths"]))] | ||
Matthias Bussonnier
|
r28480 | ) | ||
return "\n".join(map(sjoin, fmatrix)) + "\n" | ||||
Martín Gaitán
|
r12919 | |||
M Bussonnier
|
r28890 | def get_text_list( | ||
list_: List[str], last_sep: str = " and ", sep: str = ", ", wrap_item_with: str = "" | ||||
) -> str: | ||||
Martín Gaitán
|
r12919 | """ | ||
Return a string with a natural enumeration of items | ||||
>>> get_text_list(['a', 'b', 'c', 'd']) | ||||
'a, b, c and d' | ||||
Martín Gaitán
|
r12925 | >>> get_text_list(['a', 'b', 'c'], ' or ') | ||
Martín Gaitán
|
r12919 | 'a, b or c' | ||
Martín Gaitán
|
r12925 | >>> get_text_list(['a', 'b', 'c'], ', ') | ||
'a, b, c' | ||||
>>> get_text_list(['a', 'b'], ' or ') | ||||
Martín Gaitán
|
r12919 | 'a or b' | ||
>>> get_text_list(['a']) | ||||
'a' | ||||
>>> get_text_list([]) | ||||
'' | ||||
>>> get_text_list(['a', 'b'], wrap_item_with="`") | ||||
'`a` and `b`' | ||||
Martín Gaitán
|
r12925 | >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ") | ||
'a + b + c = d' | ||||
Martín Gaitán
|
r12919 | """ | ||
if len(list_) == 0: | ||||
return '' | ||||
if wrap_item_with: | ||||
list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for | ||||
item in list_] | ||||
if len(list_) == 1: | ||||
return list_[0] | ||||
Martín Gaitán
|
r12925 | return '%s%s%s' % ( | ||
sep.join(i for i in list_[:-1]), | ||||
Marin Gilles
|
r21542 | last_sep, list_[-1]) | ||