Show More
@@ -1,264 +1,267 b'' | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | """ |
|
3 | 3 | System command aliases. |
|
4 | 4 | |
|
5 | 5 | Authors: |
|
6 | 6 | |
|
7 | 7 | * Fernando Perez |
|
8 | 8 | * Brian Granger |
|
9 | 9 | """ |
|
10 | 10 | |
|
11 | 11 | #----------------------------------------------------------------------------- |
|
12 | 12 | # Copyright (C) 2008-2011 The IPython Development Team |
|
13 | 13 | # |
|
14 | 14 | # Distributed under the terms of the BSD License. |
|
15 | 15 | # |
|
16 | 16 | # The full license is in the file COPYING.txt, distributed with this software. |
|
17 | 17 | #----------------------------------------------------------------------------- |
|
18 | 18 | |
|
19 | 19 | #----------------------------------------------------------------------------- |
|
20 | 20 | # Imports |
|
21 | 21 | #----------------------------------------------------------------------------- |
|
22 | 22 | |
|
23 | 23 | import os |
|
24 | 24 | import re |
|
25 | 25 | import sys |
|
26 | 26 | |
|
27 | 27 | from traitlets.config.configurable import Configurable |
|
28 | 28 | from .error import UsageError |
|
29 | 29 | |
|
30 | 30 | from traitlets import List, Instance |
|
31 | 31 | from logging import error |
|
32 | 32 | |
|
33 | import typing as t | |
|
34 | ||
|
35 | ||
|
33 | 36 | #----------------------------------------------------------------------------- |
|
34 | 37 | # Utilities |
|
35 | 38 | #----------------------------------------------------------------------------- |
|
36 | 39 | |
|
37 | 40 | # This is used as the pattern for calls to split_user_input. |
|
38 | 41 | shell_line_split = re.compile(r'^(\s*)()(\S+)(.*$)') |
|
39 | 42 | |
|
40 | def default_aliases(): | |
|
43 | def default_aliases() -> t.List[t.Tuple[str, str]]: | |
|
41 | 44 | """Return list of shell aliases to auto-define. |
|
42 | 45 | """ |
|
43 | 46 | # Note: the aliases defined here should be safe to use on a kernel |
|
44 | 47 | # regardless of what frontend it is attached to. Frontends that use a |
|
45 | 48 | # kernel in-process can define additional aliases that will only work in |
|
46 | 49 | # their case. For example, things like 'less' or 'clear' that manipulate |
|
47 | 50 | # the terminal should NOT be declared here, as they will only work if the |
|
48 | 51 | # kernel is running inside a true terminal, and not over the network. |
|
49 | 52 | |
|
50 | 53 | if os.name == 'posix': |
|
51 | 54 | default_aliases = [('mkdir', 'mkdir'), ('rmdir', 'rmdir'), |
|
52 | 55 | ('mv', 'mv'), ('rm', 'rm'), ('cp', 'cp'), |
|
53 | 56 | ('cat', 'cat'), |
|
54 | 57 | ] |
|
55 | 58 | # Useful set of ls aliases. The GNU and BSD options are a little |
|
56 | 59 | # different, so we make aliases that provide as similar as possible |
|
57 | 60 | # behavior in ipython, by passing the right flags for each platform |
|
58 | 61 | if sys.platform.startswith('linux'): |
|
59 | 62 | ls_aliases = [('ls', 'ls -F --color'), |
|
60 | 63 | # long ls |
|
61 | 64 | ('ll', 'ls -F -o --color'), |
|
62 | 65 | # ls normal files only |
|
63 | 66 | ('lf', 'ls -F -o --color %l | grep ^-'), |
|
64 | 67 | # ls symbolic links |
|
65 | 68 | ('lk', 'ls -F -o --color %l | grep ^l'), |
|
66 | 69 | # directories or links to directories, |
|
67 | 70 | ('ldir', 'ls -F -o --color %l | grep /$'), |
|
68 | 71 | # things which are executable |
|
69 | 72 | ('lx', 'ls -F -o --color %l | grep ^-..x'), |
|
70 | 73 | ] |
|
71 | 74 | elif sys.platform.startswith('openbsd') or sys.platform.startswith('netbsd'): |
|
72 | 75 | # OpenBSD, NetBSD. The ls implementation on these platforms do not support |
|
73 | 76 | # the -G switch and lack the ability to use colorized output. |
|
74 | 77 | ls_aliases = [('ls', 'ls -F'), |
|
75 | 78 | # long ls |
|
76 | 79 | ('ll', 'ls -F -l'), |
|
77 | 80 | # ls normal files only |
|
78 | 81 | ('lf', 'ls -F -l %l | grep ^-'), |
|
79 | 82 | # ls symbolic links |
|
80 | 83 | ('lk', 'ls -F -l %l | grep ^l'), |
|
81 | 84 | # directories or links to directories, |
|
82 | 85 | ('ldir', 'ls -F -l %l | grep /$'), |
|
83 | 86 | # things which are executable |
|
84 | 87 | ('lx', 'ls -F -l %l | grep ^-..x'), |
|
85 | 88 | ] |
|
86 | 89 | else: |
|
87 | 90 | # BSD, OSX, etc. |
|
88 | 91 | ls_aliases = [('ls', 'ls -F -G'), |
|
89 | 92 | # long ls |
|
90 | 93 | ('ll', 'ls -F -l -G'), |
|
91 | 94 | # ls normal files only |
|
92 | 95 | ('lf', 'ls -F -l -G %l | grep ^-'), |
|
93 | 96 | # ls symbolic links |
|
94 | 97 | ('lk', 'ls -F -l -G %l | grep ^l'), |
|
95 | 98 | # directories or links to directories, |
|
96 | 99 | ('ldir', 'ls -F -G -l %l | grep /$'), |
|
97 | 100 | # things which are executable |
|
98 | 101 | ('lx', 'ls -F -l -G %l | grep ^-..x'), |
|
99 | 102 | ] |
|
100 | 103 | default_aliases = default_aliases + ls_aliases |
|
101 | 104 | elif os.name in ['nt', 'dos']: |
|
102 | 105 | default_aliases = [('ls', 'dir /on'), |
|
103 | 106 | ('ddir', 'dir /ad /on'), ('ldir', 'dir /ad /on'), |
|
104 | 107 | ('mkdir', 'mkdir'), ('rmdir', 'rmdir'), |
|
105 | 108 | ('echo', 'echo'), ('ren', 'ren'), ('copy', 'copy'), |
|
106 | 109 | ] |
|
107 | 110 | else: |
|
108 | 111 | default_aliases = [] |
|
109 | 112 | |
|
110 | 113 | return default_aliases |
|
111 | 114 | |
|
112 | 115 | |
|
113 | 116 | class AliasError(Exception): |
|
114 | 117 | pass |
|
115 | 118 | |
|
116 | 119 | |
|
117 | 120 | class InvalidAliasError(AliasError): |
|
118 | 121 | pass |
|
119 | 122 | |
|
120 | 123 | class Alias(object): |
|
121 | 124 | """Callable object storing the details of one alias. |
|
122 | 125 | |
|
123 | 126 | Instances are registered as magic functions to allow use of aliases. |
|
124 | 127 | """ |
|
125 | 128 | |
|
126 | 129 | # Prepare blacklist |
|
127 | 130 | blacklist = {'cd','popd','pushd','dhist','alias','unalias'} |
|
128 | 131 | |
|
129 | 132 | def __init__(self, shell, name, cmd): |
|
130 | 133 | self.shell = shell |
|
131 | 134 | self.name = name |
|
132 | 135 | self.cmd = cmd |
|
133 | 136 | self.__doc__ = "Alias for `!{}`".format(cmd) |
|
134 | 137 | self.nargs = self.validate() |
|
135 | 138 | |
|
136 | 139 | def validate(self): |
|
137 | 140 | """Validate the alias, and return the number of arguments.""" |
|
138 | 141 | if self.name in self.blacklist: |
|
139 | 142 | raise InvalidAliasError("The name %s can't be aliased " |
|
140 | 143 | "because it is a keyword or builtin." % self.name) |
|
141 | 144 | try: |
|
142 | 145 | caller = self.shell.magics_manager.magics['line'][self.name] |
|
143 | 146 | except KeyError: |
|
144 | 147 | pass |
|
145 | 148 | else: |
|
146 | 149 | if not isinstance(caller, Alias): |
|
147 | 150 | raise InvalidAliasError("The name %s can't be aliased " |
|
148 | 151 | "because it is another magic command." % self.name) |
|
149 | 152 | |
|
150 | 153 | if not (isinstance(self.cmd, str)): |
|
151 | 154 | raise InvalidAliasError("An alias command must be a string, " |
|
152 | 155 | "got: %r" % self.cmd) |
|
153 | 156 | |
|
154 | 157 | nargs = self.cmd.count('%s') - self.cmd.count('%%s') |
|
155 | 158 | |
|
156 | 159 | if (nargs > 0) and (self.cmd.find('%l') >= 0): |
|
157 | 160 | raise InvalidAliasError('The %s and %l specifiers are mutually ' |
|
158 | 161 | 'exclusive in alias definitions.') |
|
159 | 162 | |
|
160 | 163 | return nargs |
|
161 | 164 | |
|
162 | 165 | def __repr__(self): |
|
163 | 166 | return "<alias {} for {!r}>".format(self.name, self.cmd) |
|
164 | 167 | |
|
165 | 168 | def __call__(self, rest=''): |
|
166 | 169 | cmd = self.cmd |
|
167 | 170 | nargs = self.nargs |
|
168 | 171 | # Expand the %l special to be the user's input line |
|
169 | 172 | if cmd.find('%l') >= 0: |
|
170 | 173 | cmd = cmd.replace('%l', rest) |
|
171 | 174 | rest = '' |
|
172 | 175 | |
|
173 | 176 | if nargs==0: |
|
174 | 177 | if cmd.find('%%s') >= 1: |
|
175 | 178 | cmd = cmd.replace('%%s', '%s') |
|
176 | 179 | # Simple, argument-less aliases |
|
177 | 180 | cmd = '%s %s' % (cmd, rest) |
|
178 | 181 | else: |
|
179 | 182 | # Handle aliases with positional arguments |
|
180 | 183 | args = rest.split(None, nargs) |
|
181 | 184 | if len(args) < nargs: |
|
182 | 185 | raise UsageError('Alias <%s> requires %s arguments, %s given.' % |
|
183 | 186 | (self.name, nargs, len(args))) |
|
184 | 187 | cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:])) |
|
185 | 188 | |
|
186 | 189 | self.shell.system(cmd) |
|
187 | 190 | |
|
188 | 191 | #----------------------------------------------------------------------------- |
|
189 | 192 | # Main AliasManager class |
|
190 | 193 | #----------------------------------------------------------------------------- |
|
191 | 194 | |
|
192 | 195 | class AliasManager(Configurable): |
|
193 | 196 | default_aliases: List = List(default_aliases()).tag(config=True) |
|
194 | 197 | user_aliases: List = List(default_value=[]).tag(config=True) |
|
195 | 198 | shell = Instance( |
|
196 | 199 | "IPython.core.interactiveshell.InteractiveShellABC", allow_none=True |
|
197 | 200 | ) |
|
198 | 201 | |
|
199 | 202 | def __init__(self, shell=None, **kwargs): |
|
200 | 203 | super(AliasManager, self).__init__(shell=shell, **kwargs) |
|
201 | 204 | # For convenient access |
|
202 | 205 | if self.shell is not None: |
|
203 | 206 | self.linemagics = self.shell.magics_manager.magics["line"] |
|
204 | 207 | self.init_aliases() |
|
205 | 208 | |
|
206 | 209 | def init_aliases(self): |
|
207 | 210 | # Load default & user aliases |
|
208 | 211 | for name, cmd in self.default_aliases + self.user_aliases: |
|
209 | 212 | if ( |
|
210 | 213 | cmd.startswith("ls ") |
|
211 | 214 | and self.shell is not None |
|
212 | 215 | and self.shell.colors == "NoColor" |
|
213 | 216 | ): |
|
214 | 217 | cmd = cmd.replace(" --color", "") |
|
215 | 218 | self.soft_define_alias(name, cmd) |
|
216 | 219 | |
|
217 | 220 | @property |
|
218 | 221 | def aliases(self): |
|
219 | 222 | return [(n, func.cmd) for (n, func) in self.linemagics.items() |
|
220 | 223 | if isinstance(func, Alias)] |
|
221 | 224 | |
|
222 | 225 | def soft_define_alias(self, name, cmd): |
|
223 | 226 | """Define an alias, but don't raise on an AliasError.""" |
|
224 | 227 | try: |
|
225 | 228 | self.define_alias(name, cmd) |
|
226 | 229 | except AliasError as e: |
|
227 | 230 | error("Invalid alias: %s" % e) |
|
228 | 231 | |
|
229 | 232 | def define_alias(self, name, cmd): |
|
230 | 233 | """Define a new alias after validating it. |
|
231 | 234 | |
|
232 | 235 | This will raise an :exc:`AliasError` if there are validation |
|
233 | 236 | problems. |
|
234 | 237 | """ |
|
235 | 238 | caller = Alias(shell=self.shell, name=name, cmd=cmd) |
|
236 | 239 | self.shell.magics_manager.register_function(caller, magic_kind='line', |
|
237 | 240 | magic_name=name) |
|
238 | 241 | |
|
239 | 242 | def get_alias(self, name): |
|
240 | 243 | """Return an alias, or None if no alias by that name exists.""" |
|
241 | 244 | aname = self.linemagics.get(name, None) |
|
242 | 245 | return aname if isinstance(aname, Alias) else None |
|
243 | 246 | |
|
244 | 247 | def is_alias(self, name): |
|
245 | 248 | """Return whether or not a given name has been defined as an alias""" |
|
246 | 249 | return self.get_alias(name) is not None |
|
247 | 250 | |
|
248 | 251 | def undefine_alias(self, name): |
|
249 | 252 | if self.is_alias(name): |
|
250 | 253 | del self.linemagics[name] |
|
251 | 254 | else: |
|
252 | 255 | raise ValueError('%s is not an alias' % name) |
|
253 | 256 | |
|
254 | 257 | def clear_aliases(self): |
|
255 | 258 | for name, _ in self.aliases: |
|
256 | 259 | self.undefine_alias(name) |
|
257 | 260 | |
|
258 | 261 | def retrieve_alias(self, name): |
|
259 | 262 | """Retrieve the command to which an alias expands.""" |
|
260 | 263 | caller = self.get_alias(name) |
|
261 | 264 | if caller: |
|
262 | 265 | return caller.cmd |
|
263 | 266 | else: |
|
264 | 267 | raise ValueError('%s is not an alias' % name) |
@@ -1,1028 +1,1031 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | """Display formatters. |
|
3 | 3 | |
|
4 | 4 | Inheritance diagram: |
|
5 | 5 | |
|
6 | 6 | .. inheritance-diagram:: IPython.core.formatters |
|
7 | 7 | :parts: 3 |
|
8 | 8 | """ |
|
9 | 9 | |
|
10 | 10 | # Copyright (c) IPython Development Team. |
|
11 | 11 | # Distributed under the terms of the Modified BSD License. |
|
12 | 12 | |
|
13 | 13 | import abc |
|
14 | 14 | import sys |
|
15 | 15 | import traceback |
|
16 | 16 | import warnings |
|
17 | 17 | from io import StringIO |
|
18 | 18 | |
|
19 | 19 | from decorator import decorator |
|
20 | 20 | |
|
21 | 21 | from traitlets.config.configurable import Configurable |
|
22 | 22 | from .getipython import get_ipython |
|
23 | 23 | from ..utils.sentinel import Sentinel |
|
24 | 24 | from ..utils.dir2 import get_real_method |
|
25 | 25 | from ..lib import pretty |
|
26 | 26 | from traitlets import ( |
|
27 | 27 | Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List, |
|
28 | 28 | ForwardDeclaredInstance, |
|
29 | 29 | default, observe, |
|
30 | 30 | ) |
|
31 | 31 | |
|
32 | 32 | from typing import Any |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | class DisplayFormatter(Configurable): |
|
36 | 36 | |
|
37 | 37 | active_types = List(Unicode(), |
|
38 | 38 | help="""List of currently active mime-types to display. |
|
39 | 39 | You can use this to set a white-list for formats to display. |
|
40 | 40 | |
|
41 | 41 | Most users will not need to change this value. |
|
42 | 42 | """).tag(config=True) |
|
43 | 43 | |
|
44 | 44 | @default('active_types') |
|
45 | 45 | def _active_types_default(self): |
|
46 | 46 | return self.format_types |
|
47 | 47 | |
|
48 | 48 | @observe('active_types') |
|
49 | 49 | def _active_types_changed(self, change): |
|
50 | 50 | for key, formatter in self.formatters.items(): |
|
51 | 51 | if key in change['new']: |
|
52 | 52 | formatter.enabled = True |
|
53 | 53 | else: |
|
54 | 54 | formatter.enabled = False |
|
55 | 55 | |
|
56 |
ipython_display_formatter = ForwardDeclaredInstance( |
|
|
57 | @default('ipython_display_formatter') | |
|
56 | ipython_display_formatter = ForwardDeclaredInstance("FormatterABC") | |
|
57 | ||
|
58 | @default("ipython_display_formatter") | |
|
58 | 59 | def _default_formatter(self): |
|
59 | 60 | return IPythonDisplayFormatter(parent=self) |
|
60 | 61 | |
|
61 |
mimebundle_formatter = ForwardDeclaredInstance( |
|
|
62 | @default('mimebundle_formatter') | |
|
62 | mimebundle_formatter = ForwardDeclaredInstance("FormatterABC") | |
|
63 | ||
|
64 | @default("mimebundle_formatter") | |
|
63 | 65 | def _default_mime_formatter(self): |
|
64 | 66 | return MimeBundleFormatter(parent=self) |
|
65 | 67 | |
|
66 | 68 | # A dict of formatter whose keys are format types (MIME types) and whose |
|
67 | 69 | # values are subclasses of BaseFormatter. |
|
68 | 70 | formatters = Dict() |
|
69 | @default('formatters') | |
|
71 | ||
|
72 | @default("formatters") | |
|
70 | 73 | def _formatters_default(self): |
|
71 | 74 | """Activate the default formatters.""" |
|
72 | 75 | formatter_classes = [ |
|
73 | 76 | PlainTextFormatter, |
|
74 | 77 | HTMLFormatter, |
|
75 | 78 | MarkdownFormatter, |
|
76 | 79 | SVGFormatter, |
|
77 | 80 | PNGFormatter, |
|
78 | 81 | PDFFormatter, |
|
79 | 82 | JPEGFormatter, |
|
80 | 83 | LatexFormatter, |
|
81 | 84 | JSONFormatter, |
|
82 | 85 | JavascriptFormatter |
|
83 | 86 | ] |
|
84 | 87 | d = {} |
|
85 | 88 | for cls in formatter_classes: |
|
86 | 89 | f = cls(parent=self) |
|
87 | 90 | d[f.format_type] = f |
|
88 | 91 | return d |
|
89 | 92 | |
|
90 | 93 | def format(self, obj, include=None, exclude=None): |
|
91 | 94 | """Return a format data dict for an object. |
|
92 | 95 | |
|
93 | 96 | By default all format types will be computed. |
|
94 | 97 | |
|
95 | 98 | The following MIME types are usually implemented: |
|
96 | 99 | |
|
97 | 100 | * text/plain |
|
98 | 101 | * text/html |
|
99 | 102 | * text/markdown |
|
100 | 103 | * text/latex |
|
101 | 104 | * application/json |
|
102 | 105 | * application/javascript |
|
103 | 106 | * application/pdf |
|
104 | 107 | * image/png |
|
105 | 108 | * image/jpeg |
|
106 | 109 | * image/svg+xml |
|
107 | 110 | |
|
108 | 111 | Parameters |
|
109 | 112 | ---------- |
|
110 | 113 | obj : object |
|
111 | 114 | The Python object whose format data will be computed. |
|
112 | 115 | include : list, tuple or set; optional |
|
113 | 116 | A list of format type strings (MIME types) to include in the |
|
114 | 117 | format data dict. If this is set *only* the format types included |
|
115 | 118 | in this list will be computed. |
|
116 | 119 | exclude : list, tuple or set; optional |
|
117 | 120 | A list of format type string (MIME types) to exclude in the format |
|
118 | 121 | data dict. If this is set all format types will be computed, |
|
119 | 122 | except for those included in this argument. |
|
120 | 123 | Mimetypes present in exclude will take precedence over the ones in include |
|
121 | 124 | |
|
122 | 125 | Returns |
|
123 | 126 | ------- |
|
124 | 127 | (format_dict, metadata_dict) : tuple of two dicts |
|
125 | 128 | format_dict is a dictionary of key/value pairs, one of each format that was |
|
126 | 129 | generated for the object. The keys are the format types, which |
|
127 | 130 | will usually be MIME type strings and the values and JSON'able |
|
128 | 131 | data structure containing the raw data for the representation in |
|
129 | 132 | that format. |
|
130 | 133 | |
|
131 | 134 | metadata_dict is a dictionary of metadata about each mime-type output. |
|
132 | 135 | Its keys will be a strict subset of the keys in format_dict. |
|
133 | 136 | |
|
134 | 137 | Notes |
|
135 | 138 | ----- |
|
136 | 139 | If an object implement `_repr_mimebundle_` as well as various |
|
137 | 140 | `_repr_*_`, the data returned by `_repr_mimebundle_` will take |
|
138 | 141 | precedence and the corresponding `_repr_*_` for this mimetype will |
|
139 | 142 | not be called. |
|
140 | 143 | |
|
141 | 144 | """ |
|
142 | 145 | format_dict = {} |
|
143 | 146 | md_dict = {} |
|
144 | 147 | |
|
145 | 148 | if self.ipython_display_formatter(obj): |
|
146 | 149 | # object handled itself, don't proceed |
|
147 | 150 | return {}, {} |
|
148 | 151 | |
|
149 | 152 | format_dict, md_dict = self.mimebundle_formatter(obj, include=include, exclude=exclude) |
|
150 | 153 | |
|
151 | 154 | if format_dict or md_dict: |
|
152 | 155 | if include: |
|
153 | 156 | format_dict = {k:v for k,v in format_dict.items() if k in include} |
|
154 | 157 | md_dict = {k:v for k,v in md_dict.items() if k in include} |
|
155 | 158 | if exclude: |
|
156 | 159 | format_dict = {k:v for k,v in format_dict.items() if k not in exclude} |
|
157 | 160 | md_dict = {k:v for k,v in md_dict.items() if k not in exclude} |
|
158 | 161 | |
|
159 | 162 | for format_type, formatter in self.formatters.items(): |
|
160 | 163 | if format_type in format_dict: |
|
161 | 164 | # already got it from mimebundle, maybe don't render again. |
|
162 | 165 | # exception: manually registered per-mime renderer |
|
163 | 166 | # check priority: |
|
164 | 167 | # 1. user-registered per-mime formatter |
|
165 | 168 | # 2. mime-bundle (user-registered or repr method) |
|
166 | 169 | # 3. default per-mime formatter (e.g. repr method) |
|
167 | 170 | try: |
|
168 | 171 | formatter.lookup(obj) |
|
169 | 172 | except KeyError: |
|
170 | 173 | # no special formatter, use mime-bundle-provided value |
|
171 | 174 | continue |
|
172 | 175 | if include and format_type not in include: |
|
173 | 176 | continue |
|
174 | 177 | if exclude and format_type in exclude: |
|
175 | 178 | continue |
|
176 | 179 | |
|
177 | 180 | md = None |
|
178 | 181 | try: |
|
179 | 182 | data = formatter(obj) |
|
180 | 183 | except: |
|
181 | 184 | # FIXME: log the exception |
|
182 | 185 | raise |
|
183 | 186 | |
|
184 | 187 | # formatters can return raw data or (data, metadata) |
|
185 | 188 | if isinstance(data, tuple) and len(data) == 2: |
|
186 | 189 | data, md = data |
|
187 | 190 | |
|
188 | 191 | if data is not None: |
|
189 | 192 | format_dict[format_type] = data |
|
190 | 193 | if md is not None: |
|
191 | 194 | md_dict[format_type] = md |
|
192 | 195 | return format_dict, md_dict |
|
193 | 196 | |
|
194 | 197 | @property |
|
195 | 198 | def format_types(self): |
|
196 | 199 | """Return the format types (MIME types) of the active formatters.""" |
|
197 | 200 | return list(self.formatters.keys()) |
|
198 | 201 | |
|
199 | 202 | |
|
200 | 203 | #----------------------------------------------------------------------------- |
|
201 | 204 | # Formatters for specific format types (text, html, svg, etc.) |
|
202 | 205 | #----------------------------------------------------------------------------- |
|
203 | 206 | |
|
204 | 207 | |
|
205 | 208 | def _safe_repr(obj): |
|
206 | 209 | """Try to return a repr of an object |
|
207 | 210 | |
|
208 | 211 | always returns a string, at least. |
|
209 | 212 | """ |
|
210 | 213 | try: |
|
211 | 214 | return repr(obj) |
|
212 | 215 | except Exception as e: |
|
213 | 216 | return "un-repr-able object (%r)" % e |
|
214 | 217 | |
|
215 | 218 | |
|
216 | 219 | class FormatterWarning(UserWarning): |
|
217 | 220 | """Warning class for errors in formatters""" |
|
218 | 221 | |
|
219 | 222 | @decorator |
|
220 | 223 | def catch_format_error(method, self, *args, **kwargs): |
|
221 | 224 | """show traceback on failed format call""" |
|
222 | 225 | try: |
|
223 | 226 | r = method(self, *args, **kwargs) |
|
224 | 227 | except NotImplementedError: |
|
225 | 228 | # don't warn on NotImplementedErrors |
|
226 | 229 | return self._check_return(None, args[0]) |
|
227 | 230 | except Exception: |
|
228 | 231 | exc_info = sys.exc_info() |
|
229 | 232 | ip = get_ipython() |
|
230 | 233 | if ip is not None: |
|
231 | 234 | ip.showtraceback(exc_info) |
|
232 | 235 | else: |
|
233 | 236 | traceback.print_exception(*exc_info) |
|
234 | 237 | return self._check_return(None, args[0]) |
|
235 | 238 | return self._check_return(r, args[0]) |
|
236 | 239 | |
|
237 | 240 | |
|
238 | 241 | class FormatterABC(metaclass=abc.ABCMeta): |
|
239 | 242 | """ Abstract base class for Formatters. |
|
240 | 243 | |
|
241 | 244 | A formatter is a callable class that is responsible for computing the |
|
242 | 245 | raw format data for a particular format type (MIME type). For example, |
|
243 | 246 | an HTML formatter would have a format type of `text/html` and would return |
|
244 | 247 | the HTML representation of the object when called. |
|
245 | 248 | """ |
|
246 | 249 | |
|
247 | 250 | # The format type of the data returned, usually a MIME type. |
|
248 | 251 | format_type = 'text/plain' |
|
249 | 252 | |
|
250 | 253 | # Is the formatter enabled... |
|
251 | 254 | enabled = True |
|
252 | 255 | |
|
253 | 256 | @abc.abstractmethod |
|
254 | 257 | def __call__(self, obj): |
|
255 | 258 | """Return a JSON'able representation of the object. |
|
256 | 259 | |
|
257 | 260 | If the object cannot be formatted by this formatter, |
|
258 | 261 | warn and return None. |
|
259 | 262 | """ |
|
260 | 263 | return repr(obj) |
|
261 | 264 | |
|
262 | 265 | |
|
263 | 266 | def _mod_name_key(typ): |
|
264 | 267 | """Return a (__module__, __name__) tuple for a type. |
|
265 | 268 | |
|
266 | 269 | Used as key in Formatter.deferred_printers. |
|
267 | 270 | """ |
|
268 | 271 | module = getattr(typ, '__module__', None) |
|
269 | 272 | name = getattr(typ, '__name__', None) |
|
270 | 273 | return (module, name) |
|
271 | 274 | |
|
272 | 275 | |
|
273 | 276 | def _get_type(obj): |
|
274 | 277 | """Return the type of an instance (old and new-style)""" |
|
275 | 278 | return getattr(obj, '__class__', None) or type(obj) |
|
276 | 279 | |
|
277 | 280 | |
|
278 | 281 | _raise_key_error = Sentinel('_raise_key_error', __name__, |
|
279 | 282 | """ |
|
280 | 283 | Special value to raise a KeyError |
|
281 | 284 | |
|
282 | 285 | Raise KeyError in `BaseFormatter.pop` if passed as the default value to `pop` |
|
283 | 286 | """) |
|
284 | 287 | |
|
285 | 288 | |
|
286 | 289 | class BaseFormatter(Configurable): |
|
287 | 290 | """A base formatter class that is configurable. |
|
288 | 291 | |
|
289 | 292 | This formatter should usually be used as the base class of all formatters. |
|
290 | 293 | It is a traited :class:`Configurable` class and includes an extensible |
|
291 | 294 | API for users to determine how their objects are formatted. The following |
|
292 | 295 | logic is used to find a function to format an given object. |
|
293 | 296 | |
|
294 | 297 | 1. The object is introspected to see if it has a method with the name |
|
295 | 298 | :attr:`print_method`. If is does, that object is passed to that method |
|
296 | 299 | for formatting. |
|
297 | 300 | 2. If no print method is found, three internal dictionaries are consulted |
|
298 | 301 | to find print method: :attr:`singleton_printers`, :attr:`type_printers` |
|
299 | 302 | and :attr:`deferred_printers`. |
|
300 | 303 | |
|
301 | 304 | Users should use these dictionaries to register functions that will be |
|
302 | 305 | used to compute the format data for their objects (if those objects don't |
|
303 | 306 | have the special print methods). The easiest way of using these |
|
304 | 307 | dictionaries is through the :meth:`for_type` and :meth:`for_type_by_name` |
|
305 | 308 | methods. |
|
306 | 309 | |
|
307 | 310 | If no function/callable is found to compute the format data, ``None`` is |
|
308 | 311 | returned and this format type is not used. |
|
309 | 312 | """ |
|
310 | 313 | |
|
311 | 314 | format_type = Unicode("text/plain") |
|
312 | 315 | _return_type: Any = str |
|
313 | 316 | |
|
314 | 317 | enabled = Bool(True).tag(config=True) |
|
315 | 318 | |
|
316 | 319 | print_method = ObjectName('__repr__') |
|
317 | 320 | |
|
318 | 321 | # The singleton printers. |
|
319 | 322 | # Maps the IDs of the builtin singleton objects to the format functions. |
|
320 | 323 | singleton_printers = Dict().tag(config=True) |
|
321 | 324 | |
|
322 | 325 | # The type-specific printers. |
|
323 | 326 | # Map type objects to the format functions. |
|
324 | 327 | type_printers = Dict().tag(config=True) |
|
325 | 328 | |
|
326 | 329 | # The deferred-import type-specific printers. |
|
327 | 330 | # Map (modulename, classname) pairs to the format functions. |
|
328 | 331 | deferred_printers = Dict().tag(config=True) |
|
329 | 332 | |
|
330 | 333 | @catch_format_error |
|
331 | 334 | def __call__(self, obj): |
|
332 | 335 | """Compute the format for an object.""" |
|
333 | 336 | if self.enabled: |
|
334 | 337 | # lookup registered printer |
|
335 | 338 | try: |
|
336 | 339 | printer = self.lookup(obj) |
|
337 | 340 | except KeyError: |
|
338 | 341 | pass |
|
339 | 342 | else: |
|
340 | 343 | return printer(obj) |
|
341 | 344 | # Finally look for special method names |
|
342 | 345 | method = get_real_method(obj, self.print_method) |
|
343 | 346 | if method is not None: |
|
344 | 347 | return method() |
|
345 | 348 | return None |
|
346 | 349 | else: |
|
347 | 350 | return None |
|
348 | 351 | |
|
349 | 352 | def __contains__(self, typ): |
|
350 | 353 | """map in to lookup_by_type""" |
|
351 | 354 | try: |
|
352 | 355 | self.lookup_by_type(typ) |
|
353 | 356 | except KeyError: |
|
354 | 357 | return False |
|
355 | 358 | else: |
|
356 | 359 | return True |
|
357 | 360 | |
|
358 | 361 | def _check_return(self, r, obj): |
|
359 | 362 | """Check that a return value is appropriate |
|
360 | 363 | |
|
361 | 364 | Return the value if so, None otherwise, warning if invalid. |
|
362 | 365 | """ |
|
363 | 366 | if r is None or isinstance(r, self._return_type) or \ |
|
364 | 367 | (isinstance(r, tuple) and r and isinstance(r[0], self._return_type)): |
|
365 | 368 | return r |
|
366 | 369 | else: |
|
367 | 370 | warnings.warn( |
|
368 | 371 | "%s formatter returned invalid type %s (expected %s) for object: %s" % \ |
|
369 | 372 | (self.format_type, type(r), self._return_type, _safe_repr(obj)), |
|
370 | 373 | FormatterWarning |
|
371 | 374 | ) |
|
372 | 375 | |
|
373 | 376 | def lookup(self, obj): |
|
374 | 377 | """Look up the formatter for a given instance. |
|
375 | 378 | |
|
376 | 379 | Parameters |
|
377 | 380 | ---------- |
|
378 | 381 | obj : object instance |
|
379 | 382 | |
|
380 | 383 | Returns |
|
381 | 384 | ------- |
|
382 | 385 | f : callable |
|
383 | 386 | The registered formatting callable for the type. |
|
384 | 387 | |
|
385 | 388 | Raises |
|
386 | 389 | ------ |
|
387 | 390 | KeyError if the type has not been registered. |
|
388 | 391 | """ |
|
389 | 392 | # look for singleton first |
|
390 | 393 | obj_id = id(obj) |
|
391 | 394 | if obj_id in self.singleton_printers: |
|
392 | 395 | return self.singleton_printers[obj_id] |
|
393 | 396 | # then lookup by type |
|
394 | 397 | return self.lookup_by_type(_get_type(obj)) |
|
395 | 398 | |
|
396 | 399 | def lookup_by_type(self, typ): |
|
397 | 400 | """Look up the registered formatter for a type. |
|
398 | 401 | |
|
399 | 402 | Parameters |
|
400 | 403 | ---------- |
|
401 | 404 | typ : type or '__module__.__name__' string for a type |
|
402 | 405 | |
|
403 | 406 | Returns |
|
404 | 407 | ------- |
|
405 | 408 | f : callable |
|
406 | 409 | The registered formatting callable for the type. |
|
407 | 410 | |
|
408 | 411 | Raises |
|
409 | 412 | ------ |
|
410 | 413 | KeyError if the type has not been registered. |
|
411 | 414 | """ |
|
412 | 415 | if isinstance(typ, str): |
|
413 | 416 | typ_key = tuple(typ.rsplit('.',1)) |
|
414 | 417 | if typ_key not in self.deferred_printers: |
|
415 | 418 | # We may have it cached in the type map. We will have to |
|
416 | 419 | # iterate over all of the types to check. |
|
417 | 420 | for cls in self.type_printers: |
|
418 | 421 | if _mod_name_key(cls) == typ_key: |
|
419 | 422 | return self.type_printers[cls] |
|
420 | 423 | else: |
|
421 | 424 | return self.deferred_printers[typ_key] |
|
422 | 425 | else: |
|
423 | 426 | for cls in pretty._get_mro(typ): |
|
424 | 427 | if cls in self.type_printers or self._in_deferred_types(cls): |
|
425 | 428 | return self.type_printers[cls] |
|
426 | 429 | |
|
427 | 430 | # If we have reached here, the lookup failed. |
|
428 | 431 | raise KeyError("No registered printer for {0!r}".format(typ)) |
|
429 | 432 | |
|
430 | 433 | def for_type(self, typ, func=None): |
|
431 | 434 | """Add a format function for a given type. |
|
432 | 435 | |
|
433 | 436 | Parameters |
|
434 | 437 | ---------- |
|
435 | 438 | typ : type or '__module__.__name__' string for a type |
|
436 | 439 | The class of the object that will be formatted using `func`. |
|
437 | 440 | |
|
438 | 441 | func : callable |
|
439 | 442 | A callable for computing the format data. |
|
440 | 443 | `func` will be called with the object to be formatted, |
|
441 | 444 | and will return the raw data in this formatter's format. |
|
442 | 445 | Subclasses may use a different call signature for the |
|
443 | 446 | `func` argument. |
|
444 | 447 | |
|
445 | 448 | If `func` is None or not specified, there will be no change, |
|
446 | 449 | only returning the current value. |
|
447 | 450 | |
|
448 | 451 | Returns |
|
449 | 452 | ------- |
|
450 | 453 | oldfunc : callable |
|
451 | 454 | The currently registered callable. |
|
452 | 455 | If you are registering a new formatter, |
|
453 | 456 | this will be the previous value (to enable restoring later). |
|
454 | 457 | """ |
|
455 | 458 | # if string given, interpret as 'pkg.module.class_name' |
|
456 | 459 | if isinstance(typ, str): |
|
457 | 460 | type_module, type_name = typ.rsplit('.', 1) |
|
458 | 461 | return self.for_type_by_name(type_module, type_name, func) |
|
459 | 462 | |
|
460 | 463 | try: |
|
461 | 464 | oldfunc = self.lookup_by_type(typ) |
|
462 | 465 | except KeyError: |
|
463 | 466 | oldfunc = None |
|
464 | 467 | |
|
465 | 468 | if func is not None: |
|
466 | 469 | self.type_printers[typ] = func |
|
467 | 470 | |
|
468 | 471 | return oldfunc |
|
469 | 472 | |
|
470 | 473 | def for_type_by_name(self, type_module, type_name, func=None): |
|
471 | 474 | """Add a format function for a type specified by the full dotted |
|
472 | 475 | module and name of the type, rather than the type of the object. |
|
473 | 476 | |
|
474 | 477 | Parameters |
|
475 | 478 | ---------- |
|
476 | 479 | type_module : str |
|
477 | 480 | The full dotted name of the module the type is defined in, like |
|
478 | 481 | ``numpy``. |
|
479 | 482 | |
|
480 | 483 | type_name : str |
|
481 | 484 | The name of the type (the class name), like ``dtype`` |
|
482 | 485 | |
|
483 | 486 | func : callable |
|
484 | 487 | A callable for computing the format data. |
|
485 | 488 | `func` will be called with the object to be formatted, |
|
486 | 489 | and will return the raw data in this formatter's format. |
|
487 | 490 | Subclasses may use a different call signature for the |
|
488 | 491 | `func` argument. |
|
489 | 492 | |
|
490 | 493 | If `func` is None or unspecified, there will be no change, |
|
491 | 494 | only returning the current value. |
|
492 | 495 | |
|
493 | 496 | Returns |
|
494 | 497 | ------- |
|
495 | 498 | oldfunc : callable |
|
496 | 499 | The currently registered callable. |
|
497 | 500 | If you are registering a new formatter, |
|
498 | 501 | this will be the previous value (to enable restoring later). |
|
499 | 502 | """ |
|
500 | 503 | key = (type_module, type_name) |
|
501 | 504 | |
|
502 | 505 | try: |
|
503 | 506 | oldfunc = self.lookup_by_type("%s.%s" % key) |
|
504 | 507 | except KeyError: |
|
505 | 508 | oldfunc = None |
|
506 | 509 | |
|
507 | 510 | if func is not None: |
|
508 | 511 | self.deferred_printers[key] = func |
|
509 | 512 | return oldfunc |
|
510 | 513 | |
|
511 | 514 | def pop(self, typ, default=_raise_key_error): |
|
512 | 515 | """Pop a formatter for the given type. |
|
513 | 516 | |
|
514 | 517 | Parameters |
|
515 | 518 | ---------- |
|
516 | 519 | typ : type or '__module__.__name__' string for a type |
|
517 | 520 | default : object |
|
518 | 521 | value to be returned if no formatter is registered for typ. |
|
519 | 522 | |
|
520 | 523 | Returns |
|
521 | 524 | ------- |
|
522 | 525 | obj : object |
|
523 | 526 | The last registered object for the type. |
|
524 | 527 | |
|
525 | 528 | Raises |
|
526 | 529 | ------ |
|
527 | 530 | KeyError if the type is not registered and default is not specified. |
|
528 | 531 | """ |
|
529 | 532 | |
|
530 | 533 | if isinstance(typ, str): |
|
531 | 534 | typ_key = tuple(typ.rsplit('.',1)) |
|
532 | 535 | if typ_key not in self.deferred_printers: |
|
533 | 536 | # We may have it cached in the type map. We will have to |
|
534 | 537 | # iterate over all of the types to check. |
|
535 | 538 | for cls in self.type_printers: |
|
536 | 539 | if _mod_name_key(cls) == typ_key: |
|
537 | 540 | old = self.type_printers.pop(cls) |
|
538 | 541 | break |
|
539 | 542 | else: |
|
540 | 543 | old = default |
|
541 | 544 | else: |
|
542 | 545 | old = self.deferred_printers.pop(typ_key) |
|
543 | 546 | else: |
|
544 | 547 | if typ in self.type_printers: |
|
545 | 548 | old = self.type_printers.pop(typ) |
|
546 | 549 | else: |
|
547 | 550 | old = self.deferred_printers.pop(_mod_name_key(typ), default) |
|
548 | 551 | if old is _raise_key_error: |
|
549 | 552 | raise KeyError("No registered value for {0!r}".format(typ)) |
|
550 | 553 | return old |
|
551 | 554 | |
|
552 | 555 | def _in_deferred_types(self, cls): |
|
553 | 556 | """ |
|
554 | 557 | Check if the given class is specified in the deferred type registry. |
|
555 | 558 | |
|
556 | 559 | Successful matches will be moved to the regular type registry for future use. |
|
557 | 560 | """ |
|
558 | 561 | mod = getattr(cls, '__module__', None) |
|
559 | 562 | name = getattr(cls, '__name__', None) |
|
560 | 563 | key = (mod, name) |
|
561 | 564 | if key in self.deferred_printers: |
|
562 | 565 | # Move the printer over to the regular registry. |
|
563 | 566 | printer = self.deferred_printers.pop(key) |
|
564 | 567 | self.type_printers[cls] = printer |
|
565 | 568 | return True |
|
566 | 569 | return False |
|
567 | 570 | |
|
568 | 571 | |
|
569 | 572 | class PlainTextFormatter(BaseFormatter): |
|
570 | 573 | """The default pretty-printer. |
|
571 | 574 | |
|
572 | 575 | This uses :mod:`IPython.lib.pretty` to compute the format data of |
|
573 | 576 | the object. If the object cannot be pretty printed, :func:`repr` is used. |
|
574 | 577 | See the documentation of :mod:`IPython.lib.pretty` for details on |
|
575 | 578 | how to write pretty printers. Here is a simple example:: |
|
576 | 579 | |
|
577 | 580 | def dtype_pprinter(obj, p, cycle): |
|
578 | 581 | if cycle: |
|
579 | 582 | return p.text('dtype(...)') |
|
580 | 583 | if hasattr(obj, 'fields'): |
|
581 | 584 | if obj.fields is None: |
|
582 | 585 | p.text(repr(obj)) |
|
583 | 586 | else: |
|
584 | 587 | p.begin_group(7, 'dtype([') |
|
585 | 588 | for i, field in enumerate(obj.descr): |
|
586 | 589 | if i > 0: |
|
587 | 590 | p.text(',') |
|
588 | 591 | p.breakable() |
|
589 | 592 | p.pretty(field) |
|
590 | 593 | p.end_group(7, '])') |
|
591 | 594 | """ |
|
592 | 595 | |
|
593 | 596 | # The format type of data returned. |
|
594 | 597 | format_type = Unicode('text/plain') |
|
595 | 598 | |
|
596 | 599 | # This subclass ignores this attribute as it always need to return |
|
597 | 600 | # something. |
|
598 | 601 | enabled = Bool(True).tag(config=False) |
|
599 | 602 | |
|
600 | 603 | max_seq_length = Integer(pretty.MAX_SEQ_LENGTH, |
|
601 | 604 | help="""Truncate large collections (lists, dicts, tuples, sets) to this size. |
|
602 | 605 | |
|
603 | 606 | Set to 0 to disable truncation. |
|
604 | 607 | """ |
|
605 | 608 | ).tag(config=True) |
|
606 | 609 | |
|
607 | 610 | # Look for a _repr_pretty_ methods to use for pretty printing. |
|
608 | 611 | print_method = ObjectName('_repr_pretty_') |
|
609 | 612 | |
|
610 | 613 | # Whether to pretty-print or not. |
|
611 | 614 | pprint = Bool(True).tag(config=True) |
|
612 | 615 | |
|
613 | 616 | # Whether to be verbose or not. |
|
614 | 617 | verbose = Bool(False).tag(config=True) |
|
615 | 618 | |
|
616 | 619 | # The maximum width. |
|
617 | 620 | max_width = Integer(79).tag(config=True) |
|
618 | 621 | |
|
619 | 622 | # The newline character. |
|
620 | 623 | newline = Unicode('\n').tag(config=True) |
|
621 | 624 | |
|
622 | 625 | # format-string for pprinting floats |
|
623 | 626 | float_format = Unicode('%r') |
|
624 | 627 | # setter for float precision, either int or direct format-string |
|
625 | 628 | float_precision = CUnicode('').tag(config=True) |
|
626 | 629 | |
|
627 | 630 | @observe('float_precision') |
|
628 | 631 | def _float_precision_changed(self, change): |
|
629 | 632 | """float_precision changed, set float_format accordingly. |
|
630 | 633 | |
|
631 | 634 | float_precision can be set by int or str. |
|
632 | 635 | This will set float_format, after interpreting input. |
|
633 | 636 | If numpy has been imported, numpy print precision will also be set. |
|
634 | 637 | |
|
635 | 638 | integer `n` sets format to '%.nf', otherwise, format set directly. |
|
636 | 639 | |
|
637 | 640 | An empty string returns to defaults (repr for float, 8 for numpy). |
|
638 | 641 | |
|
639 | 642 | This parameter can be set via the '%precision' magic. |
|
640 | 643 | """ |
|
641 | 644 | new = change['new'] |
|
642 | 645 | if '%' in new: |
|
643 | 646 | # got explicit format string |
|
644 | 647 | fmt = new |
|
645 | 648 | try: |
|
646 | 649 | fmt%3.14159 |
|
647 | 650 | except Exception as e: |
|
648 | 651 | raise ValueError("Precision must be int or format string, not %r"%new) from e |
|
649 | 652 | elif new: |
|
650 | 653 | # otherwise, should be an int |
|
651 | 654 | try: |
|
652 | 655 | i = int(new) |
|
653 | 656 | assert i >= 0 |
|
654 | 657 | except ValueError as e: |
|
655 | 658 | raise ValueError("Precision must be int or format string, not %r"%new) from e |
|
656 | 659 | except AssertionError as e: |
|
657 | 660 | raise ValueError("int precision must be non-negative, not %r"%i) from e |
|
658 | 661 | |
|
659 | 662 | fmt = '%%.%if'%i |
|
660 | 663 | if 'numpy' in sys.modules: |
|
661 | 664 | # set numpy precision if it has been imported |
|
662 | 665 | import numpy |
|
663 | 666 | numpy.set_printoptions(precision=i) |
|
664 | 667 | else: |
|
665 | 668 | # default back to repr |
|
666 | 669 | fmt = '%r' |
|
667 | 670 | if 'numpy' in sys.modules: |
|
668 | 671 | import numpy |
|
669 | 672 | # numpy default is 8 |
|
670 | 673 | numpy.set_printoptions(precision=8) |
|
671 | 674 | self.float_format = fmt |
|
672 | 675 | |
|
673 | 676 | # Use the default pretty printers from IPython.lib.pretty. |
|
674 | 677 | @default('singleton_printers') |
|
675 | 678 | def _singleton_printers_default(self): |
|
676 | 679 | return pretty._singleton_pprinters.copy() |
|
677 | 680 | |
|
678 | 681 | @default('type_printers') |
|
679 | 682 | def _type_printers_default(self): |
|
680 | 683 | d = pretty._type_pprinters.copy() |
|
681 | 684 | d[float] = lambda obj,p,cycle: p.text(self.float_format%obj) |
|
682 | 685 | # if NumPy is used, set precision for its float64 type |
|
683 | 686 | if "numpy" in sys.modules: |
|
684 | 687 | import numpy |
|
685 | 688 | |
|
686 | 689 | d[numpy.float64] = lambda obj, p, cycle: p.text(self.float_format % obj) |
|
687 | 690 | return d |
|
688 | 691 | |
|
689 | 692 | @default('deferred_printers') |
|
690 | 693 | def _deferred_printers_default(self): |
|
691 | 694 | return pretty._deferred_type_pprinters.copy() |
|
692 | 695 | |
|
693 | 696 | #### FormatterABC interface #### |
|
694 | 697 | |
|
695 | 698 | @catch_format_error |
|
696 | 699 | def __call__(self, obj): |
|
697 | 700 | """Compute the pretty representation of the object.""" |
|
698 | 701 | if not self.pprint: |
|
699 | 702 | return repr(obj) |
|
700 | 703 | else: |
|
701 | 704 | stream = StringIO() |
|
702 | 705 | printer = pretty.RepresentationPrinter(stream, self.verbose, |
|
703 | 706 | self.max_width, self.newline, |
|
704 | 707 | max_seq_length=self.max_seq_length, |
|
705 | 708 | singleton_pprinters=self.singleton_printers, |
|
706 | 709 | type_pprinters=self.type_printers, |
|
707 | 710 | deferred_pprinters=self.deferred_printers) |
|
708 | 711 | printer.pretty(obj) |
|
709 | 712 | printer.flush() |
|
710 | 713 | return stream.getvalue() |
|
711 | 714 | |
|
712 | 715 | |
|
713 | 716 | class HTMLFormatter(BaseFormatter): |
|
714 | 717 | """An HTML formatter. |
|
715 | 718 | |
|
716 | 719 | To define the callables that compute the HTML representation of your |
|
717 | 720 | objects, define a :meth:`_repr_html_` method or use the :meth:`for_type` |
|
718 | 721 | or :meth:`for_type_by_name` methods to register functions that handle |
|
719 | 722 | this. |
|
720 | 723 | |
|
721 | 724 | The return value of this formatter should be a valid HTML snippet that |
|
722 | 725 | could be injected into an existing DOM. It should *not* include the |
|
723 | 726 | ```<html>`` or ```<body>`` tags. |
|
724 | 727 | """ |
|
725 | 728 | format_type = Unicode('text/html') |
|
726 | 729 | |
|
727 | 730 | print_method = ObjectName('_repr_html_') |
|
728 | 731 | |
|
729 | 732 | |
|
730 | 733 | class MarkdownFormatter(BaseFormatter): |
|
731 | 734 | """A Markdown formatter. |
|
732 | 735 | |
|
733 | 736 | To define the callables that compute the Markdown representation of your |
|
734 | 737 | objects, define a :meth:`_repr_markdown_` method or use the :meth:`for_type` |
|
735 | 738 | or :meth:`for_type_by_name` methods to register functions that handle |
|
736 | 739 | this. |
|
737 | 740 | |
|
738 | 741 | The return value of this formatter should be a valid Markdown. |
|
739 | 742 | """ |
|
740 | 743 | format_type = Unicode('text/markdown') |
|
741 | 744 | |
|
742 | 745 | print_method = ObjectName('_repr_markdown_') |
|
743 | 746 | |
|
744 | 747 | class SVGFormatter(BaseFormatter): |
|
745 | 748 | """An SVG formatter. |
|
746 | 749 | |
|
747 | 750 | To define the callables that compute the SVG representation of your |
|
748 | 751 | objects, define a :meth:`_repr_svg_` method or use the :meth:`for_type` |
|
749 | 752 | or :meth:`for_type_by_name` methods to register functions that handle |
|
750 | 753 | this. |
|
751 | 754 | |
|
752 | 755 | The return value of this formatter should be valid SVG enclosed in |
|
753 | 756 | ```<svg>``` tags, that could be injected into an existing DOM. It should |
|
754 | 757 | *not* include the ```<html>`` or ```<body>`` tags. |
|
755 | 758 | """ |
|
756 | 759 | format_type = Unicode('image/svg+xml') |
|
757 | 760 | |
|
758 | 761 | print_method = ObjectName('_repr_svg_') |
|
759 | 762 | |
|
760 | 763 | |
|
761 | 764 | class PNGFormatter(BaseFormatter): |
|
762 | 765 | """A PNG formatter. |
|
763 | 766 | |
|
764 | 767 | To define the callables that compute the PNG representation of your |
|
765 | 768 | objects, define a :meth:`_repr_png_` method or use the :meth:`for_type` |
|
766 | 769 | or :meth:`for_type_by_name` methods to register functions that handle |
|
767 | 770 | this. |
|
768 | 771 | |
|
769 | 772 | The return value of this formatter should be raw PNG data, *not* |
|
770 | 773 | base64 encoded. |
|
771 | 774 | """ |
|
772 | 775 | format_type = Unicode('image/png') |
|
773 | 776 | |
|
774 | 777 | print_method = ObjectName('_repr_png_') |
|
775 | 778 | |
|
776 | 779 | _return_type = (bytes, str) |
|
777 | 780 | |
|
778 | 781 | |
|
779 | 782 | class JPEGFormatter(BaseFormatter): |
|
780 | 783 | """A JPEG formatter. |
|
781 | 784 | |
|
782 | 785 | To define the callables that compute the JPEG representation of your |
|
783 | 786 | objects, define a :meth:`_repr_jpeg_` method or use the :meth:`for_type` |
|
784 | 787 | or :meth:`for_type_by_name` methods to register functions that handle |
|
785 | 788 | this. |
|
786 | 789 | |
|
787 | 790 | The return value of this formatter should be raw JPEG data, *not* |
|
788 | 791 | base64 encoded. |
|
789 | 792 | """ |
|
790 | 793 | format_type = Unicode('image/jpeg') |
|
791 | 794 | |
|
792 | 795 | print_method = ObjectName('_repr_jpeg_') |
|
793 | 796 | |
|
794 | 797 | _return_type = (bytes, str) |
|
795 | 798 | |
|
796 | 799 | |
|
797 | 800 | class LatexFormatter(BaseFormatter): |
|
798 | 801 | """A LaTeX formatter. |
|
799 | 802 | |
|
800 | 803 | To define the callables that compute the LaTeX representation of your |
|
801 | 804 | objects, define a :meth:`_repr_latex_` method or use the :meth:`for_type` |
|
802 | 805 | or :meth:`for_type_by_name` methods to register functions that handle |
|
803 | 806 | this. |
|
804 | 807 | |
|
805 | 808 | The return value of this formatter should be a valid LaTeX equation, |
|
806 | 809 | enclosed in either ```$```, ```$$``` or another LaTeX equation |
|
807 | 810 | environment. |
|
808 | 811 | """ |
|
809 | 812 | format_type = Unicode('text/latex') |
|
810 | 813 | |
|
811 | 814 | print_method = ObjectName('_repr_latex_') |
|
812 | 815 | |
|
813 | 816 | |
|
814 | 817 | class JSONFormatter(BaseFormatter): |
|
815 | 818 | """A JSON string formatter. |
|
816 | 819 | |
|
817 | 820 | To define the callables that compute the JSONable representation of |
|
818 | 821 | your objects, define a :meth:`_repr_json_` method or use the :meth:`for_type` |
|
819 | 822 | or :meth:`for_type_by_name` methods to register functions that handle |
|
820 | 823 | this. |
|
821 | 824 | |
|
822 | 825 | The return value of this formatter should be a JSONable list or dict. |
|
823 | 826 | JSON scalars (None, number, string) are not allowed, only dict or list containers. |
|
824 | 827 | """ |
|
825 | 828 | format_type = Unicode('application/json') |
|
826 | 829 | _return_type = (list, dict) |
|
827 | 830 | |
|
828 | 831 | print_method = ObjectName('_repr_json_') |
|
829 | 832 | |
|
830 | 833 | def _check_return(self, r, obj): |
|
831 | 834 | """Check that a return value is appropriate |
|
832 | 835 | |
|
833 | 836 | Return the value if so, None otherwise, warning if invalid. |
|
834 | 837 | """ |
|
835 | 838 | if r is None: |
|
836 | 839 | return |
|
837 | 840 | md = None |
|
838 | 841 | if isinstance(r, tuple): |
|
839 | 842 | # unpack data, metadata tuple for type checking on first element |
|
840 | 843 | r, md = r |
|
841 | 844 | |
|
842 | 845 | assert not isinstance( |
|
843 | 846 | r, str |
|
844 | 847 | ), "JSON-as-string has been deprecated since IPython < 3" |
|
845 | 848 | |
|
846 | 849 | if md is not None: |
|
847 | 850 | # put the tuple back together |
|
848 | 851 | r = (r, md) |
|
849 | 852 | return super(JSONFormatter, self)._check_return(r, obj) |
|
850 | 853 | |
|
851 | 854 | |
|
852 | 855 | class JavascriptFormatter(BaseFormatter): |
|
853 | 856 | """A Javascript formatter. |
|
854 | 857 | |
|
855 | 858 | To define the callables that compute the Javascript representation of |
|
856 | 859 | your objects, define a :meth:`_repr_javascript_` method or use the |
|
857 | 860 | :meth:`for_type` or :meth:`for_type_by_name` methods to register functions |
|
858 | 861 | that handle this. |
|
859 | 862 | |
|
860 | 863 | The return value of this formatter should be valid Javascript code and |
|
861 | 864 | should *not* be enclosed in ```<script>``` tags. |
|
862 | 865 | """ |
|
863 | 866 | format_type = Unicode('application/javascript') |
|
864 | 867 | |
|
865 | 868 | print_method = ObjectName('_repr_javascript_') |
|
866 | 869 | |
|
867 | 870 | |
|
868 | 871 | class PDFFormatter(BaseFormatter): |
|
869 | 872 | """A PDF formatter. |
|
870 | 873 | |
|
871 | 874 | To define the callables that compute the PDF representation of your |
|
872 | 875 | objects, define a :meth:`_repr_pdf_` method or use the :meth:`for_type` |
|
873 | 876 | or :meth:`for_type_by_name` methods to register functions that handle |
|
874 | 877 | this. |
|
875 | 878 | |
|
876 | 879 | The return value of this formatter should be raw PDF data, *not* |
|
877 | 880 | base64 encoded. |
|
878 | 881 | """ |
|
879 | 882 | format_type = Unicode('application/pdf') |
|
880 | 883 | |
|
881 | 884 | print_method = ObjectName('_repr_pdf_') |
|
882 | 885 | |
|
883 | 886 | _return_type = (bytes, str) |
|
884 | 887 | |
|
885 | 888 | class IPythonDisplayFormatter(BaseFormatter): |
|
886 | 889 | """An escape-hatch Formatter for objects that know how to display themselves. |
|
887 | 890 | |
|
888 | 891 | To define the callables that compute the representation of your |
|
889 | 892 | objects, define a :meth:`_ipython_display_` method or use the :meth:`for_type` |
|
890 | 893 | or :meth:`for_type_by_name` methods to register functions that handle |
|
891 | 894 | this. Unlike mime-type displays, this method should not return anything, |
|
892 | 895 | instead calling any appropriate display methods itself. |
|
893 | 896 | |
|
894 | 897 | This display formatter has highest priority. |
|
895 | 898 | If it fires, no other display formatter will be called. |
|
896 | 899 | |
|
897 | 900 | Prior to IPython 6.1, `_ipython_display_` was the only way to display custom mime-types |
|
898 | 901 | without registering a new Formatter. |
|
899 | 902 | |
|
900 | 903 | IPython 6.1 introduces `_repr_mimebundle_` for displaying custom mime-types, |
|
901 | 904 | so `_ipython_display_` should only be used for objects that require unusual |
|
902 | 905 | display patterns, such as multiple display calls. |
|
903 | 906 | """ |
|
904 | 907 | print_method = ObjectName('_ipython_display_') |
|
905 | 908 | _return_type = (type(None), bool) |
|
906 | 909 | |
|
907 | 910 | @catch_format_error |
|
908 | 911 | def __call__(self, obj): |
|
909 | 912 | """Compute the format for an object.""" |
|
910 | 913 | if self.enabled: |
|
911 | 914 | # lookup registered printer |
|
912 | 915 | try: |
|
913 | 916 | printer = self.lookup(obj) |
|
914 | 917 | except KeyError: |
|
915 | 918 | pass |
|
916 | 919 | else: |
|
917 | 920 | printer(obj) |
|
918 | 921 | return True |
|
919 | 922 | # Finally look for special method names |
|
920 | 923 | method = get_real_method(obj, self.print_method) |
|
921 | 924 | if method is not None: |
|
922 | 925 | method() |
|
923 | 926 | return True |
|
924 | 927 | |
|
925 | 928 | |
|
926 | 929 | class MimeBundleFormatter(BaseFormatter): |
|
927 | 930 | """A Formatter for arbitrary mime-types. |
|
928 | 931 | |
|
929 | 932 | Unlike other `_repr_<mimetype>_` methods, |
|
930 | 933 | `_repr_mimebundle_` should return mime-bundle data, |
|
931 | 934 | either the mime-keyed `data` dictionary or the tuple `(data, metadata)`. |
|
932 | 935 | Any mime-type is valid. |
|
933 | 936 | |
|
934 | 937 | To define the callables that compute the mime-bundle representation of your |
|
935 | 938 | objects, define a :meth:`_repr_mimebundle_` method or use the :meth:`for_type` |
|
936 | 939 | or :meth:`for_type_by_name` methods to register functions that handle |
|
937 | 940 | this. |
|
938 | 941 | |
|
939 | 942 | .. versionadded:: 6.1 |
|
940 | 943 | """ |
|
941 | 944 | print_method = ObjectName('_repr_mimebundle_') |
|
942 | 945 | _return_type = dict |
|
943 | 946 | |
|
944 | 947 | def _check_return(self, r, obj): |
|
945 | 948 | r = super(MimeBundleFormatter, self)._check_return(r, obj) |
|
946 | 949 | # always return (data, metadata): |
|
947 | 950 | if r is None: |
|
948 | 951 | return {}, {} |
|
949 | 952 | if not isinstance(r, tuple): |
|
950 | 953 | return r, {} |
|
951 | 954 | return r |
|
952 | 955 | |
|
953 | 956 | @catch_format_error |
|
954 | 957 | def __call__(self, obj, include=None, exclude=None): |
|
955 | 958 | """Compute the format for an object. |
|
956 | 959 | |
|
957 | 960 | Identical to parent's method but we pass extra parameters to the method. |
|
958 | 961 | |
|
959 | 962 | Unlike other _repr_*_ `_repr_mimebundle_` should allow extra kwargs, in |
|
960 | 963 | particular `include` and `exclude`. |
|
961 | 964 | """ |
|
962 | 965 | if self.enabled: |
|
963 | 966 | # lookup registered printer |
|
964 | 967 | try: |
|
965 | 968 | printer = self.lookup(obj) |
|
966 | 969 | except KeyError: |
|
967 | 970 | pass |
|
968 | 971 | else: |
|
969 | 972 | return printer(obj) |
|
970 | 973 | # Finally look for special method names |
|
971 | 974 | method = get_real_method(obj, self.print_method) |
|
972 | 975 | |
|
973 | 976 | if method is not None: |
|
974 | 977 | return method(include=include, exclude=exclude) |
|
975 | 978 | return None |
|
976 | 979 | else: |
|
977 | 980 | return None |
|
978 | 981 | |
|
979 | 982 | |
|
980 | 983 | FormatterABC.register(BaseFormatter) |
|
981 | 984 | FormatterABC.register(PlainTextFormatter) |
|
982 | 985 | FormatterABC.register(HTMLFormatter) |
|
983 | 986 | FormatterABC.register(MarkdownFormatter) |
|
984 | 987 | FormatterABC.register(SVGFormatter) |
|
985 | 988 | FormatterABC.register(PNGFormatter) |
|
986 | 989 | FormatterABC.register(PDFFormatter) |
|
987 | 990 | FormatterABC.register(JPEGFormatter) |
|
988 | 991 | FormatterABC.register(LatexFormatter) |
|
989 | 992 | FormatterABC.register(JSONFormatter) |
|
990 | 993 | FormatterABC.register(JavascriptFormatter) |
|
991 | 994 | FormatterABC.register(IPythonDisplayFormatter) |
|
992 | 995 | FormatterABC.register(MimeBundleFormatter) |
|
993 | 996 | |
|
994 | 997 | |
|
995 | 998 | def format_display_data(obj, include=None, exclude=None): |
|
996 | 999 | """Return a format data dict for an object. |
|
997 | 1000 | |
|
998 | 1001 | By default all format types will be computed. |
|
999 | 1002 | |
|
1000 | 1003 | Parameters |
|
1001 | 1004 | ---------- |
|
1002 | 1005 | obj : object |
|
1003 | 1006 | The Python object whose format data will be computed. |
|
1004 | 1007 | |
|
1005 | 1008 | Returns |
|
1006 | 1009 | ------- |
|
1007 | 1010 | format_dict : dict |
|
1008 | 1011 | A dictionary of key/value pairs, one or each format that was |
|
1009 | 1012 | generated for the object. The keys are the format types, which |
|
1010 | 1013 | will usually be MIME type strings and the values and JSON'able |
|
1011 | 1014 | data structure containing the raw data for the representation in |
|
1012 | 1015 | that format. |
|
1013 | 1016 | include : list or tuple, optional |
|
1014 | 1017 | A list of format type strings (MIME types) to include in the |
|
1015 | 1018 | format data dict. If this is set *only* the format types included |
|
1016 | 1019 | in this list will be computed. |
|
1017 | 1020 | exclude : list or tuple, optional |
|
1018 | 1021 | A list of format type string (MIME types) to exclude in the format |
|
1019 | 1022 | data dict. If this is set all format types will be computed, |
|
1020 | 1023 | except for those included in this argument. |
|
1021 | 1024 | """ |
|
1022 | 1025 | from .interactiveshell import InteractiveShell |
|
1023 | 1026 | |
|
1024 | 1027 | return InteractiveShell.instance().display_formatter.format( |
|
1025 | 1028 | obj, |
|
1026 | 1029 | include, |
|
1027 | 1030 | exclude |
|
1028 | 1031 | ) |
@@ -1,320 +1,320 b'' | |||
|
1 | 1 | """ |
|
2 | 2 | This module contains utility function and classes to inject simple ast |
|
3 | 3 | transformations based on code strings into IPython. While it is already possible |
|
4 | 4 | with ast-transformers it is not easy to directly manipulate ast. |
|
5 | 5 | |
|
6 | 6 | |
|
7 | 7 | IPython has pre-code and post-code hooks, but are ran from within the IPython |
|
8 | 8 | machinery so may be inappropriate, for example for performance mesurement. |
|
9 | 9 | |
|
10 | 10 | This module give you tools to simplify this, and expose 2 classes: |
|
11 | 11 | |
|
12 | 12 | - `ReplaceCodeTransformer` which is a simple ast transformer based on code |
|
13 | 13 | template, |
|
14 | 14 | |
|
15 | 15 | and for advance case: |
|
16 | 16 | |
|
17 | 17 | - `Mangler` which is a simple ast transformer that mangle names in the ast. |
|
18 | 18 | |
|
19 | 19 | |
|
20 | 20 | Example, let's try to make a simple version of the ``timeit`` magic, that run a |
|
21 | 21 | code snippet 10 times and print the average time taken. |
|
22 | 22 | |
|
23 | 23 | Basically we want to run : |
|
24 | 24 | |
|
25 | 25 | .. code-block:: python |
|
26 | 26 | |
|
27 | 27 | from time import perf_counter |
|
28 | 28 | now = perf_counter() |
|
29 | 29 | for i in range(10): |
|
30 | 30 | __code__ # our code |
|
31 | 31 | print(f"Time taken: {(perf_counter() - now)/10}") |
|
32 | 32 | __ret__ # the result of the last statement |
|
33 | 33 | |
|
34 | 34 | Where ``__code__`` is the code snippet we want to run, and ``__ret__`` is the |
|
35 | 35 | result, so that if we for example run `dataframe.head()` IPython still display |
|
36 | 36 | the head of dataframe instead of nothing. |
|
37 | 37 | |
|
38 | 38 | Here is a complete example of a file `timit2.py` that define such a magic: |
|
39 | 39 | |
|
40 | 40 | .. code-block:: python |
|
41 | 41 | |
|
42 | 42 | from IPython.core.magic import ( |
|
43 | 43 | Magics, |
|
44 | 44 | magics_class, |
|
45 | 45 | line_cell_magic, |
|
46 | 46 | ) |
|
47 | 47 | from IPython.core.magics.ast_mod import ReplaceCodeTransformer |
|
48 | 48 | from textwrap import dedent |
|
49 | 49 | import ast |
|
50 | 50 | |
|
51 | 51 | template = template = dedent(''' |
|
52 | 52 | from time import perf_counter |
|
53 | 53 | now = perf_counter() |
|
54 | 54 | for i in range(10): |
|
55 | 55 | __code__ |
|
56 | 56 | print(f"Time taken: {(perf_counter() - now)/10}") |
|
57 | 57 | __ret__ |
|
58 | 58 | ''' |
|
59 | 59 | ) |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | @magics_class |
|
63 | 63 | class AstM(Magics): |
|
64 | 64 | @line_cell_magic |
|
65 | 65 | def t2(self, line, cell): |
|
66 | 66 | transformer = ReplaceCodeTransformer.from_string(template) |
|
67 | 67 | transformer.debug = True |
|
68 | 68 | transformer.mangler.debug = True |
|
69 | 69 | new_code = transformer.visit(ast.parse(cell)) |
|
70 | 70 | return exec(compile(new_code, "<ast>", "exec")) |
|
71 | 71 | |
|
72 | 72 | |
|
73 | 73 | def load_ipython_extension(ip): |
|
74 | 74 | ip.register_magics(AstM) |
|
75 | 75 | |
|
76 | 76 | |
|
77 | 77 | |
|
78 | 78 | .. code-block:: python |
|
79 | 79 | |
|
80 | 80 | In [1]: %load_ext timit2 |
|
81 | 81 | |
|
82 | 82 | In [2]: %%t2 |
|
83 | 83 | ...: import time |
|
84 | 84 | ...: time.sleep(0.05) |
|
85 | 85 | ...: |
|
86 | 86 | ...: |
|
87 | 87 | Time taken: 0.05435649999999441 |
|
88 | 88 | |
|
89 | 89 | |
|
90 | 90 | If you wish to ran all the code enter in IPython in an ast transformer, you can |
|
91 | 91 | do so as well: |
|
92 | 92 | |
|
93 | 93 | .. code-block:: python |
|
94 | 94 | |
|
95 | 95 | In [1]: from IPython.core.magics.ast_mod import ReplaceCodeTransformer |
|
96 | 96 | ...: |
|
97 | 97 | ...: template = ''' |
|
98 | 98 | ...: from time import perf_counter |
|
99 | 99 | ...: now = perf_counter() |
|
100 | 100 | ...: __code__ |
|
101 | 101 | ...: print(f"Code ran in {perf_counter()-now}") |
|
102 | 102 | ...: __ret__''' |
|
103 | 103 | ...: |
|
104 | 104 | ...: get_ipython().ast_transformers.append(ReplaceCodeTransformer.from_string(template)) |
|
105 | 105 | |
|
106 | 106 | In [2]: 1+1 |
|
107 | 107 | Code ran in 3.40410006174352e-05 |
|
108 | 108 | Out[2]: 2 |
|
109 | 109 | |
|
110 | 110 | |
|
111 | 111 | |
|
112 | 112 | Hygiene and Mangling |
|
113 | 113 | -------------------- |
|
114 | 114 | |
|
115 | 115 | The ast transformer above is not hygienic, it may not work if the user code use |
|
116 | 116 | the same variable names as the ones used in the template. For example. |
|
117 | 117 | |
|
118 | 118 | To help with this by default the `ReplaceCodeTransformer` will mangle all names |
|
119 | 119 | staring with 3 underscores. This is a simple heuristic that should work in most |
|
120 | 120 | case, but can be cumbersome in some case. We provide a `Mangler` class that can |
|
121 | 121 | be overridden to change the mangling heuristic, or simply use the `mangle_all` |
|
122 | 122 | utility function. It will _try_ to mangle all names (except `__ret__` and |
|
123 | 123 | `__code__`), but this include builtins (``print``, ``range``, ``type``) and |
|
124 | 124 | replace those by invalid identifiers py prepending ``mangle-``: |
|
125 | 125 | ``mangle-print``, ``mangle-range``, ``mangle-type`` etc. This is not a problem |
|
126 | 126 | as currently Python AST support invalid identifiers, but it may not be the case |
|
127 | 127 | in the future. |
|
128 | 128 | |
|
129 | 129 | You can set `ReplaceCodeTransformer.debug=True` and |
|
130 | 130 | `ReplaceCodeTransformer.mangler.debug=True` to see the code after mangling and |
|
131 | 131 | transforming: |
|
132 | 132 | |
|
133 | 133 | .. code-block:: python |
|
134 | 134 | |
|
135 | 135 | |
|
136 | 136 | In [1]: from IPython.core.magics.ast_mod import ReplaceCodeTransformer, mangle_all |
|
137 | 137 | ...: |
|
138 | 138 | ...: template = ''' |
|
139 | 139 | ...: from builtins import type, print |
|
140 | 140 | ...: from time import perf_counter |
|
141 | 141 | ...: now = perf_counter() |
|
142 | 142 | ...: __code__ |
|
143 | 143 | ...: print(f"Code ran in {perf_counter()-now}") |
|
144 | 144 | ...: __ret__''' |
|
145 | 145 | ...: |
|
146 | 146 | ...: transformer = ReplaceCodeTransformer.from_string(template, mangling_predicate=mangle_all) |
|
147 | 147 | |
|
148 | 148 | |
|
149 | 149 | In [2]: transformer.debug = True |
|
150 | 150 | ...: transformer.mangler.debug = True |
|
151 | 151 | ...: get_ipython().ast_transformers.append(transformer) |
|
152 | 152 | |
|
153 | 153 | In [3]: 1+1 |
|
154 | 154 | Mangling Alias mangle-type |
|
155 | 155 | Mangling Alias mangle-print |
|
156 | 156 | Mangling Alias mangle-perf_counter |
|
157 | 157 | Mangling now |
|
158 | 158 | Mangling perf_counter |
|
159 | 159 | Not mangling __code__ |
|
160 | 160 | Mangling print |
|
161 | 161 | Mangling perf_counter |
|
162 | 162 | Mangling now |
|
163 | 163 | Not mangling __ret__ |
|
164 | 164 | ---- Transformed code ---- |
|
165 | 165 | from builtins import type as mangle-type, print as mangle-print |
|
166 | 166 | from time import perf_counter as mangle-perf_counter |
|
167 | 167 | mangle-now = mangle-perf_counter() |
|
168 | 168 | ret-tmp = 1 + 1 |
|
169 | 169 | mangle-print(f'Code ran in {mangle-perf_counter() - mangle-now}') |
|
170 | 170 | ret-tmp |
|
171 | 171 | ---- ---------------- ---- |
|
172 | 172 | Code ran in 0.00013654199938173406 |
|
173 | 173 | Out[3]: 2 |
|
174 | 174 | |
|
175 | 175 | |
|
176 | 176 | """ |
|
177 | 177 | |
|
178 | 178 | __skip_doctest__ = True |
|
179 | 179 | |
|
180 | 180 | |
|
181 | from ast import NodeTransformer, Store, Load, Name, Expr, Assign, Module | |
|
181 | from ast import NodeTransformer, Store, Load, Name, Expr, Assign, Module, Import, ImportFrom | |
|
182 | 182 | import ast |
|
183 | 183 | import copy |
|
184 | 184 | |
|
185 | from typing import Dict, Optional | |
|
185 | from typing import Dict, Optional, Union | |
|
186 | 186 | |
|
187 | 187 | |
|
188 | 188 | mangle_all = lambda name: False if name in ("__ret__", "__code__") else True |
|
189 | 189 | |
|
190 | 190 | |
|
191 | 191 | class Mangler(NodeTransformer): |
|
192 | 192 | """ |
|
193 | 193 | Mangle given names in and ast tree to make sure they do not conflict with |
|
194 | 194 | user code. |
|
195 | 195 | """ |
|
196 | 196 | |
|
197 | 197 | enabled: bool = True |
|
198 | 198 | debug: bool = False |
|
199 | 199 | |
|
200 | 200 | def log(self, *args, **kwargs): |
|
201 | 201 | if self.debug: |
|
202 | 202 | print(*args, **kwargs) |
|
203 | 203 | |
|
204 | 204 | def __init__(self, predicate=None): |
|
205 | 205 | if predicate is None: |
|
206 | 206 | predicate = lambda name: name.startswith("___") |
|
207 | 207 | self.predicate = predicate |
|
208 | 208 | |
|
209 | 209 | def visit_Name(self, node): |
|
210 | 210 | if self.predicate(node.id): |
|
211 | 211 | self.log("Mangling", node.id) |
|
212 | 212 | # Once in the ast we do not need |
|
213 | 213 | # names to be valid identifiers. |
|
214 | 214 | node.id = "mangle-" + node.id |
|
215 | 215 | else: |
|
216 | 216 | self.log("Not mangling", node.id) |
|
217 | 217 | return node |
|
218 | 218 | |
|
219 | 219 | def visit_FunctionDef(self, node): |
|
220 | 220 | if self.predicate(node.name): |
|
221 | 221 | self.log("Mangling", node.name) |
|
222 | 222 | node.name = "mangle-" + node.name |
|
223 | 223 | else: |
|
224 | 224 | self.log("Not mangling", node.name) |
|
225 | 225 | |
|
226 | 226 | for arg in node.args.args: |
|
227 | 227 | if self.predicate(arg.arg): |
|
228 | 228 | self.log("Mangling function arg", arg.arg) |
|
229 | 229 | arg.arg = "mangle-" + arg.arg |
|
230 | 230 | else: |
|
231 | 231 | self.log("Not mangling function arg", arg.arg) |
|
232 | 232 | return self.generic_visit(node) |
|
233 | 233 | |
|
234 | def visit_ImportFrom(self, node): | |
|
234 | def visit_ImportFrom(self, node:ImportFrom): | |
|
235 | 235 | return self._visit_Import_and_ImportFrom(node) |
|
236 | 236 | |
|
237 | def visit_Import(self, node): | |
|
237 | def visit_Import(self, node:Import): | |
|
238 | 238 | return self._visit_Import_and_ImportFrom(node) |
|
239 | 239 | |
|
240 | def _visit_Import_and_ImportFrom(self, node): | |
|
240 | def _visit_Import_and_ImportFrom(self, node:Union[Import, ImportFrom]): | |
|
241 | 241 | for alias in node.names: |
|
242 | 242 | asname = alias.name if alias.asname is None else alias.asname |
|
243 | 243 | if self.predicate(asname): |
|
244 | 244 | new_name: str = "mangle-" + asname |
|
245 | 245 | self.log("Mangling Alias", new_name) |
|
246 | 246 | alias.asname = new_name |
|
247 | 247 | else: |
|
248 | 248 | self.log("Not mangling Alias", alias.asname) |
|
249 | 249 | return node |
|
250 | 250 | |
|
251 | 251 | |
|
252 | 252 | class ReplaceCodeTransformer(NodeTransformer): |
|
253 | 253 | enabled: bool = True |
|
254 | 254 | debug: bool = False |
|
255 | 255 | mangler: Mangler |
|
256 | 256 | |
|
257 | 257 | def __init__( |
|
258 | 258 | self, template: Module, mapping: Optional[Dict] = None, mangling_predicate=None |
|
259 | 259 | ): |
|
260 | 260 | assert isinstance(mapping, (dict, type(None))) |
|
261 | 261 | assert isinstance(mangling_predicate, (type(None), type(lambda: None))) |
|
262 | 262 | assert isinstance(template, ast.Module) |
|
263 | 263 | self.template = template |
|
264 | 264 | self.mangler = Mangler(predicate=mangling_predicate) |
|
265 | 265 | if mapping is None: |
|
266 | 266 | mapping = {} |
|
267 | 267 | self.mapping = mapping |
|
268 | 268 | |
|
269 | 269 | @classmethod |
|
270 | 270 | def from_string( |
|
271 | 271 | cls, template: str, mapping: Optional[Dict] = None, mangling_predicate=None |
|
272 | 272 | ): |
|
273 | 273 | return cls( |
|
274 | 274 | ast.parse(template), mapping=mapping, mangling_predicate=mangling_predicate |
|
275 | 275 | ) |
|
276 | 276 | |
|
277 | 277 | def visit_Module(self, code): |
|
278 | 278 | if not self.enabled: |
|
279 | 279 | return code |
|
280 | 280 | # if not isinstance(code, ast.Module): |
|
281 | 281 | # recursively called... |
|
282 | 282 | # return generic_visit(self, code) |
|
283 | 283 | last = code.body[-1] |
|
284 | 284 | if isinstance(last, Expr): |
|
285 | 285 | code.body.pop() |
|
286 | 286 | code.body.append(Assign([Name("ret-tmp", ctx=Store())], value=last.value)) |
|
287 | 287 | ast.fix_missing_locations(code) |
|
288 | 288 | ret = Expr(value=Name("ret-tmp", ctx=Load())) |
|
289 | 289 | ret = ast.fix_missing_locations(ret) |
|
290 | 290 | self.mapping["__ret__"] = ret |
|
291 | 291 | else: |
|
292 | 292 | self.mapping["__ret__"] = ast.parse("None").body[0] |
|
293 | 293 | self.mapping["__code__"] = code.body |
|
294 | 294 | tpl = ast.fix_missing_locations(self.template) |
|
295 | 295 | |
|
296 | 296 | tx = copy.deepcopy(tpl) |
|
297 | 297 | tx = self.mangler.visit(tx) |
|
298 | 298 | node = self.generic_visit(tx) |
|
299 | 299 | node_2 = ast.fix_missing_locations(node) |
|
300 | 300 | if self.debug: |
|
301 | 301 | print("---- Transformed code ----") |
|
302 | 302 | print(ast.unparse(node_2)) |
|
303 | 303 | print("---- ---------------- ----") |
|
304 | 304 | return node_2 |
|
305 | 305 | |
|
306 | 306 | # this does not work as the name might be in a list and one might want to extend the list. |
|
307 | 307 | # def visit_Name(self, name): |
|
308 | 308 | # if name.id in self.mapping and name.id == "__ret__": |
|
309 | 309 | # print(name, "in mapping") |
|
310 | 310 | # if isinstance(name.ctx, ast.Store): |
|
311 | 311 | # return Name("tmp", ctx=Store()) |
|
312 | 312 | # else: |
|
313 | 313 | # return copy.deepcopy(self.mapping[name.id]) |
|
314 | 314 | # return name |
|
315 | 315 | |
|
316 | 316 | def visit_Expr(self, expr): |
|
317 | 317 | if isinstance(expr.value, Name) and expr.value.id in self.mapping: |
|
318 | 318 | if self.mapping[expr.value.id] is not None: |
|
319 | 319 | return copy.deepcopy(self.mapping[expr.value.id]) |
|
320 | 320 | return self.generic_visit(expr) |
@@ -1,799 +1,805 b'' | |||
|
1 | # encoding: utf-8 | |
|
2 | 1 |
|
|
3 | 2 | Utilities for working with strings and text. |
|
4 | 3 | |
|
5 | 4 | Inheritance diagram: |
|
6 | 5 | |
|
7 | 6 | .. inheritance-diagram:: IPython.utils.text |
|
8 | 7 | :parts: 3 |
|
9 | 8 | """ |
|
10 | 9 | |
|
11 | 10 | import os |
|
12 | 11 | import re |
|
13 | 12 | import string |
|
14 | import sys | |
|
15 | 13 | import textwrap |
|
16 | 14 | import warnings |
|
17 | 15 | from string import Formatter |
|
18 | 16 | from pathlib import Path |
|
19 | 17 | |
|
20 | from typing import List, Dict, Tuple | |
|
18 | from typing import List, Dict, Tuple, Optional, cast | |
|
21 | 19 | |
|
22 | 20 | |
|
23 | 21 | class LSString(str): |
|
24 | 22 | """String derivative with a special access attributes. |
|
25 | 23 | |
|
26 | 24 | These are normal strings, but with the special attributes: |
|
27 | 25 | |
|
28 | 26 | .l (or .list) : value as list (split on newlines). |
|
29 | 27 | .n (or .nlstr): original value (the string itself). |
|
30 | 28 | .s (or .spstr): value as whitespace-separated string. |
|
31 | 29 | .p (or .paths): list of path objects (requires path.py package) |
|
32 | 30 | |
|
33 | 31 | Any values which require transformations are computed only once and |
|
34 | 32 | cached. |
|
35 | 33 | |
|
36 | 34 | Such strings are very useful to efficiently interact with the shell, which |
|
37 | 35 | typically only understands whitespace-separated options for commands.""" |
|
38 | 36 | |
|
39 | 37 | def get_list(self): |
|
40 | 38 | try: |
|
41 | 39 | return self.__list |
|
42 | 40 | except AttributeError: |
|
43 | 41 | self.__list = self.split('\n') |
|
44 | 42 | return self.__list |
|
45 | 43 | |
|
46 | 44 | l = list = property(get_list) |
|
47 | 45 | |
|
48 | 46 | def get_spstr(self): |
|
49 | 47 | try: |
|
50 | 48 | return self.__spstr |
|
51 | 49 | except AttributeError: |
|
52 | 50 | self.__spstr = self.replace('\n',' ') |
|
53 | 51 | return self.__spstr |
|
54 | 52 | |
|
55 | 53 | s = spstr = property(get_spstr) |
|
56 | 54 | |
|
57 | 55 | def get_nlstr(self): |
|
58 | 56 | return self |
|
59 | 57 | |
|
60 | 58 | n = nlstr = property(get_nlstr) |
|
61 | 59 | |
|
62 | 60 | def get_paths(self): |
|
63 | 61 | try: |
|
64 | 62 | return self.__paths |
|
65 | 63 | except AttributeError: |
|
66 | 64 | self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)] |
|
67 | 65 | return self.__paths |
|
68 | 66 | |
|
69 | 67 | p = paths = property(get_paths) |
|
70 | 68 | |
|
71 | 69 | # FIXME: We need to reimplement type specific displayhook and then add this |
|
72 | 70 | # back as a custom printer. This should also be moved outside utils into the |
|
73 | 71 | # core. |
|
74 | 72 | |
|
75 | 73 | # def print_lsstring(arg): |
|
76 | 74 | # """ Prettier (non-repr-like) and more informative printer for LSString """ |
|
77 | 75 | # print "LSString (.p, .n, .l, .s available). Value:" |
|
78 | 76 | # print arg |
|
79 | 77 | # |
|
80 | 78 | # |
|
81 | 79 | # print_lsstring = result_display.register(LSString)(print_lsstring) |
|
82 | 80 | |
|
83 | 81 | |
|
84 | 82 | class SList(list): |
|
85 | 83 | """List derivative with a special access attributes. |
|
86 | 84 | |
|
87 | 85 | These are normal lists, but with the special attributes: |
|
88 | 86 | |
|
89 | 87 | * .l (or .list) : value as list (the list itself). |
|
90 | 88 | * .n (or .nlstr): value as a string, joined on newlines. |
|
91 | 89 | * .s (or .spstr): value as a string, joined on spaces. |
|
92 | 90 | * .p (or .paths): list of path objects (requires path.py package) |
|
93 | 91 | |
|
94 | 92 | Any values which require transformations are computed only once and |
|
95 | 93 | cached.""" |
|
96 | 94 | |
|
97 | 95 | def get_list(self): |
|
98 | 96 | return self |
|
99 | 97 | |
|
100 | 98 | l = list = property(get_list) |
|
101 | 99 | |
|
102 | 100 | def get_spstr(self): |
|
103 | 101 | try: |
|
104 | 102 | return self.__spstr |
|
105 | 103 | except AttributeError: |
|
106 | 104 | self.__spstr = ' '.join(self) |
|
107 | 105 | return self.__spstr |
|
108 | 106 | |
|
109 | 107 | s = spstr = property(get_spstr) |
|
110 | 108 | |
|
111 | 109 | def get_nlstr(self): |
|
112 | 110 | try: |
|
113 | 111 | return self.__nlstr |
|
114 | 112 | except AttributeError: |
|
115 | 113 | self.__nlstr = '\n'.join(self) |
|
116 | 114 | return self.__nlstr |
|
117 | 115 | |
|
118 | 116 | n = nlstr = property(get_nlstr) |
|
119 | 117 | |
|
120 | 118 | def get_paths(self): |
|
121 | 119 | try: |
|
122 | 120 | return self.__paths |
|
123 | 121 | except AttributeError: |
|
124 | 122 | self.__paths = [Path(p) for p in self if os.path.exists(p)] |
|
125 | 123 | return self.__paths |
|
126 | 124 | |
|
127 | 125 | p = paths = property(get_paths) |
|
128 | 126 | |
|
129 | 127 | def grep(self, pattern, prune = False, field = None): |
|
130 | 128 | """ Return all strings matching 'pattern' (a regex or callable) |
|
131 | 129 | |
|
132 | 130 | This is case-insensitive. If prune is true, return all items |
|
133 | 131 | NOT matching the pattern. |
|
134 | 132 | |
|
135 | 133 | If field is specified, the match must occur in the specified |
|
136 | 134 | whitespace-separated field. |
|
137 | 135 | |
|
138 | 136 | Examples:: |
|
139 | 137 | |
|
140 | 138 | a.grep( lambda x: x.startswith('C') ) |
|
141 | 139 | a.grep('Cha.*log', prune=1) |
|
142 | 140 | a.grep('chm', field=-1) |
|
143 | 141 | """ |
|
144 | 142 | |
|
145 | 143 | def match_target(s): |
|
146 | 144 | if field is None: |
|
147 | 145 | return s |
|
148 | 146 | parts = s.split() |
|
149 | 147 | try: |
|
150 | 148 | tgt = parts[field] |
|
151 | 149 | return tgt |
|
152 | 150 | except IndexError: |
|
153 | 151 | return "" |
|
154 | 152 | |
|
155 | 153 | if isinstance(pattern, str): |
|
156 | 154 | pred = lambda x : re.search(pattern, x, re.IGNORECASE) |
|
157 | 155 | else: |
|
158 | 156 | pred = pattern |
|
159 | 157 | if not prune: |
|
160 | 158 | return SList([el for el in self if pred(match_target(el))]) |
|
161 | 159 | else: |
|
162 | 160 | return SList([el for el in self if not pred(match_target(el))]) |
|
163 | 161 | |
|
164 | 162 | def fields(self, *fields): |
|
165 | 163 | """ Collect whitespace-separated fields from string list |
|
166 | 164 | |
|
167 | 165 | Allows quick awk-like usage of string lists. |
|
168 | 166 | |
|
169 | 167 | Example data (in var a, created by 'a = !ls -l'):: |
|
170 | 168 | |
|
171 | 169 | -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog |
|
172 | 170 | drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython |
|
173 | 171 | |
|
174 | 172 | * ``a.fields(0)`` is ``['-rwxrwxrwx', 'drwxrwxrwx+']`` |
|
175 | 173 | * ``a.fields(1,0)`` is ``['1 -rwxrwxrwx', '6 drwxrwxrwx+']`` |
|
176 | 174 | (note the joining by space). |
|
177 | 175 | * ``a.fields(-1)`` is ``['ChangeLog', 'IPython']`` |
|
178 | 176 | |
|
179 | 177 | IndexErrors are ignored. |
|
180 | 178 | |
|
181 | 179 | Without args, fields() just split()'s the strings. |
|
182 | 180 | """ |
|
183 | 181 | if len(fields) == 0: |
|
184 | 182 | return [el.split() for el in self] |
|
185 | 183 | |
|
186 | 184 | res = SList() |
|
187 | 185 | for el in [f.split() for f in self]: |
|
188 | 186 | lineparts = [] |
|
189 | 187 | |
|
190 | 188 | for fd in fields: |
|
191 | 189 | try: |
|
192 | 190 | lineparts.append(el[fd]) |
|
193 | 191 | except IndexError: |
|
194 | 192 | pass |
|
195 | 193 | if lineparts: |
|
196 | 194 | res.append(" ".join(lineparts)) |
|
197 | 195 | |
|
198 | 196 | return res |
|
199 | 197 | |
|
200 | 198 | def sort(self,field= None, nums = False): |
|
201 | 199 | """ sort by specified fields (see fields()) |
|
202 | 200 | |
|
203 | 201 | Example:: |
|
204 | 202 | |
|
205 | 203 | a.sort(1, nums = True) |
|
206 | 204 | |
|
207 | 205 | Sorts a by second field, in numerical order (so that 21 > 3) |
|
208 | 206 | |
|
209 | 207 | """ |
|
210 | 208 | |
|
211 | 209 | #decorate, sort, undecorate |
|
212 | 210 | if field is not None: |
|
213 | 211 | dsu = [[SList([line]).fields(field), line] for line in self] |
|
214 | 212 | else: |
|
215 | 213 | dsu = [[line, line] for line in self] |
|
216 | 214 | if nums: |
|
217 | 215 | for i in range(len(dsu)): |
|
218 | 216 | numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()]) |
|
219 | 217 | try: |
|
220 | 218 | n = int(numstr) |
|
221 | 219 | except ValueError: |
|
222 | 220 | n = 0 |
|
223 | 221 | dsu[i][0] = n |
|
224 | 222 | |
|
225 | 223 | |
|
226 | 224 | dsu.sort() |
|
227 | 225 | return SList([t[1] for t in dsu]) |
|
228 | 226 | |
|
229 | 227 | |
|
230 | 228 | # FIXME: We need to reimplement type specific displayhook and then add this |
|
231 | 229 | # back as a custom printer. This should also be moved outside utils into the |
|
232 | 230 | # core. |
|
233 | 231 | |
|
234 | 232 | # def print_slist(arg): |
|
235 | 233 | # """ Prettier (non-repr-like) and more informative printer for SList """ |
|
236 | 234 | # print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):" |
|
237 | 235 | # if hasattr(arg, 'hideonce') and arg.hideonce: |
|
238 | 236 | # arg.hideonce = False |
|
239 | 237 | # return |
|
240 | 238 | # |
|
241 | 239 | # nlprint(arg) # This was a nested list printer, now removed. |
|
242 | 240 | # |
|
243 | 241 | # print_slist = result_display.register(SList)(print_slist) |
|
244 | 242 | |
|
245 | 243 | |
|
246 | 244 | def indent(instr,nspaces=4, ntabs=0, flatten=False): |
|
247 | 245 | """Indent a string a given number of spaces or tabstops. |
|
248 | 246 | |
|
249 | 247 | indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces. |
|
250 | 248 | |
|
251 | 249 | Parameters |
|
252 | 250 | ---------- |
|
253 | 251 | instr : basestring |
|
254 | 252 | The string to be indented. |
|
255 | 253 | nspaces : int (default: 4) |
|
256 | 254 | The number of spaces to be indented. |
|
257 | 255 | ntabs : int (default: 0) |
|
258 | 256 | The number of tabs to be indented. |
|
259 | 257 | flatten : bool (default: False) |
|
260 | 258 | Whether to scrub existing indentation. If True, all lines will be |
|
261 | 259 | aligned to the same indentation. If False, existing indentation will |
|
262 | 260 | be strictly increased. |
|
263 | 261 | |
|
264 | 262 | Returns |
|
265 | 263 | ------- |
|
266 | 264 | str|unicode : string indented by ntabs and nspaces. |
|
267 | 265 | |
|
268 | 266 | """ |
|
269 | 267 | if instr is None: |
|
270 | 268 | return |
|
271 | 269 | ind = '\t'*ntabs+' '*nspaces |
|
272 | 270 | if flatten: |
|
273 | 271 | pat = re.compile(r'^\s*', re.MULTILINE) |
|
274 | 272 | else: |
|
275 | 273 | pat = re.compile(r'^', re.MULTILINE) |
|
276 | 274 | outstr = re.sub(pat, ind, instr) |
|
277 | 275 | if outstr.endswith(os.linesep+ind): |
|
278 | 276 | return outstr[:-len(ind)] |
|
279 | 277 | else: |
|
280 | 278 | return outstr |
|
281 | 279 | |
|
282 | 280 | |
|
283 | 281 | def list_strings(arg): |
|
284 | 282 | """Always return a list of strings, given a string or list of strings |
|
285 | 283 | as input. |
|
286 | 284 | |
|
287 | 285 | Examples |
|
288 | 286 | -------- |
|
289 | 287 | :: |
|
290 | 288 | |
|
291 | 289 | In [7]: list_strings('A single string') |
|
292 | 290 | Out[7]: ['A single string'] |
|
293 | 291 | |
|
294 | 292 | In [8]: list_strings(['A single string in a list']) |
|
295 | 293 | Out[8]: ['A single string in a list'] |
|
296 | 294 | |
|
297 | 295 | In [9]: list_strings(['A','list','of','strings']) |
|
298 | 296 | Out[9]: ['A', 'list', 'of', 'strings'] |
|
299 | 297 | """ |
|
300 | 298 | |
|
301 | 299 | if isinstance(arg, str): |
|
302 | 300 | return [arg] |
|
303 | 301 | else: |
|
304 | 302 | return arg |
|
305 | 303 | |
|
306 | 304 | |
|
307 | 305 | def marquee(txt='',width=78,mark='*'): |
|
308 | 306 | """Return the input string centered in a 'marquee'. |
|
309 | 307 | |
|
310 | 308 | Examples |
|
311 | 309 | -------- |
|
312 | 310 | :: |
|
313 | 311 | |
|
314 | 312 | In [16]: marquee('A test',40) |
|
315 | 313 | Out[16]: '**************** A test ****************' |
|
316 | 314 | |
|
317 | 315 | In [17]: marquee('A test',40,'-') |
|
318 | 316 | Out[17]: '---------------- A test ----------------' |
|
319 | 317 | |
|
320 | 318 | In [18]: marquee('A test',40,' ') |
|
321 | 319 | Out[18]: ' A test ' |
|
322 | 320 | |
|
323 | 321 | """ |
|
324 | 322 | if not txt: |
|
325 | 323 | return (mark*width)[:width] |
|
326 | 324 | nmark = (width-len(txt)-2)//len(mark)//2 |
|
327 | 325 | if nmark < 0: nmark =0 |
|
328 | 326 | marks = mark*nmark |
|
329 | 327 | return '%s %s %s' % (marks,txt,marks) |
|
330 | 328 | |
|
331 | 329 | |
|
332 | 330 | ini_spaces_re = re.compile(r'^(\s+)') |
|
333 | 331 | |
|
334 | 332 | def num_ini_spaces(strng): |
|
335 | 333 | """Return the number of initial spaces in a string""" |
|
336 | 334 | warnings.warn( |
|
337 | 335 | "`num_ini_spaces` is Pending Deprecation since IPython 8.17." |
|
338 | 336 | "It is considered fro removal in in future version. " |
|
339 | 337 | "Please open an issue if you believe it should be kept.", |
|
340 | 338 | stacklevel=2, |
|
341 | 339 | category=PendingDeprecationWarning, |
|
342 | 340 | ) |
|
343 | 341 | ini_spaces = ini_spaces_re.match(strng) |
|
344 | 342 | if ini_spaces: |
|
345 | 343 | return ini_spaces.end() |
|
346 | 344 | else: |
|
347 | 345 | return 0 |
|
348 | 346 | |
|
349 | 347 | |
|
350 | 348 | def format_screen(strng): |
|
351 | 349 | """Format a string for screen printing. |
|
352 | 350 | |
|
353 | 351 | This removes some latex-type format codes.""" |
|
354 | 352 | # Paragraph continue |
|
355 | 353 | par_re = re.compile(r'\\$',re.MULTILINE) |
|
356 | 354 | strng = par_re.sub('',strng) |
|
357 | 355 | return strng |
|
358 | 356 | |
|
359 | 357 | |
|
360 | 358 | def dedent(text: str) -> str: |
|
361 | 359 | """Equivalent of textwrap.dedent that ignores unindented first line. |
|
362 | 360 | |
|
363 | 361 | This means it will still dedent strings like: |
|
364 | 362 | '''foo |
|
365 | 363 | is a bar |
|
366 | 364 | ''' |
|
367 | 365 | |
|
368 | 366 | For use in wrap_paragraphs. |
|
369 | 367 | """ |
|
370 | 368 | |
|
371 | 369 | if text.startswith('\n'): |
|
372 | 370 | # text starts with blank line, don't ignore the first line |
|
373 | 371 | return textwrap.dedent(text) |
|
374 | 372 | |
|
375 | 373 | # split first line |
|
376 | 374 | splits = text.split('\n',1) |
|
377 | 375 | if len(splits) == 1: |
|
378 | 376 | # only one line |
|
379 | 377 | return textwrap.dedent(text) |
|
380 | 378 | |
|
381 | 379 | first, rest = splits |
|
382 | 380 | # dedent everything but the first line |
|
383 | 381 | rest = textwrap.dedent(rest) |
|
384 | 382 | return '\n'.join([first, rest]) |
|
385 | 383 | |
|
386 | 384 | |
|
387 | 385 | def wrap_paragraphs(text, ncols=80): |
|
388 | 386 | """Wrap multiple paragraphs to fit a specified width. |
|
389 | 387 | |
|
390 | 388 | This is equivalent to textwrap.wrap, but with support for multiple |
|
391 | 389 | paragraphs, as separated by empty lines. |
|
392 | 390 | |
|
393 | 391 | Returns |
|
394 | 392 | ------- |
|
395 | 393 | list of complete paragraphs, wrapped to fill `ncols` columns. |
|
396 | 394 | """ |
|
397 | 395 | warnings.warn( |
|
398 | 396 | "`wrap_paragraphs` is Pending Deprecation since IPython 8.17." |
|
399 | 397 | "It is considered fro removal in in future version. " |
|
400 | 398 | "Please open an issue if you believe it should be kept.", |
|
401 | 399 | stacklevel=2, |
|
402 | 400 | category=PendingDeprecationWarning, |
|
403 | 401 | ) |
|
404 | 402 | paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE) |
|
405 | 403 | text = dedent(text).strip() |
|
406 | 404 | paragraphs = paragraph_re.split(text)[::2] # every other entry is space |
|
407 | 405 | out_ps = [] |
|
408 | 406 | indent_re = re.compile(r'\n\s+', re.MULTILINE) |
|
409 | 407 | for p in paragraphs: |
|
410 | 408 | # presume indentation that survives dedent is meaningful formatting, |
|
411 | 409 | # so don't fill unless text is flush. |
|
412 | 410 | if indent_re.search(p) is None: |
|
413 | 411 | # wrap paragraph |
|
414 | 412 | p = textwrap.fill(p, ncols) |
|
415 | 413 | out_ps.append(p) |
|
416 | 414 | return out_ps |
|
417 | 415 | |
|
418 | 416 | |
|
419 | 417 | def strip_email_quotes(text): |
|
420 | 418 | """Strip leading email quotation characters ('>'). |
|
421 | 419 | |
|
422 | 420 | Removes any combination of leading '>' interspersed with whitespace that |
|
423 | 421 | appears *identically* in all lines of the input text. |
|
424 | 422 | |
|
425 | 423 | Parameters |
|
426 | 424 | ---------- |
|
427 | 425 | text : str |
|
428 | 426 | |
|
429 | 427 | Examples |
|
430 | 428 | -------- |
|
431 | 429 | |
|
432 | 430 | Simple uses:: |
|
433 | 431 | |
|
434 | 432 | In [2]: strip_email_quotes('> > text') |
|
435 | 433 | Out[2]: 'text' |
|
436 | 434 | |
|
437 | 435 | In [3]: strip_email_quotes('> > text\\n> > more') |
|
438 | 436 | Out[3]: 'text\\nmore' |
|
439 | 437 | |
|
440 | 438 | Note how only the common prefix that appears in all lines is stripped:: |
|
441 | 439 | |
|
442 | 440 | In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') |
|
443 | 441 | Out[4]: '> text\\n> more\\nmore...' |
|
444 | 442 | |
|
445 | 443 | So if any line has no quote marks ('>'), then none are stripped from any |
|
446 | 444 | of them :: |
|
447 | 445 | |
|
448 | 446 | In [5]: strip_email_quotes('> > text\\n> > more\\nlast different') |
|
449 | 447 | Out[5]: '> > text\\n> > more\\nlast different' |
|
450 | 448 | """ |
|
451 | 449 | lines = text.splitlines() |
|
452 | 450 | strip_len = 0 |
|
453 | 451 | |
|
454 | 452 | for characters in zip(*lines): |
|
455 | 453 | # Check if all characters in this position are the same |
|
456 | 454 | if len(set(characters)) > 1: |
|
457 | 455 | break |
|
458 | 456 | prefix_char = characters[0] |
|
459 | 457 | |
|
460 | 458 | if prefix_char in string.whitespace or prefix_char == ">": |
|
461 | 459 | strip_len += 1 |
|
462 | 460 | else: |
|
463 | 461 | break |
|
464 | 462 | |
|
465 | 463 | text = "\n".join([ln[strip_len:] for ln in lines]) |
|
466 | 464 | return text |
|
467 | 465 | |
|
468 | 466 | |
|
469 | 467 | def strip_ansi(source): |
|
470 | 468 | """ |
|
471 | 469 | Remove ansi escape codes from text. |
|
472 | 470 | |
|
473 | 471 | Parameters |
|
474 | 472 | ---------- |
|
475 | 473 | source : str |
|
476 | 474 | Source to remove the ansi from |
|
477 | 475 | """ |
|
478 | 476 | warnings.warn( |
|
479 | 477 | "`strip_ansi` is Pending Deprecation since IPython 8.17." |
|
480 | 478 | "It is considered fro removal in in future version. " |
|
481 | 479 | "Please open an issue if you believe it should be kept.", |
|
482 | 480 | stacklevel=2, |
|
483 | 481 | category=PendingDeprecationWarning, |
|
484 | 482 | ) |
|
485 | 483 | |
|
486 | 484 | return re.sub(r'\033\[(\d|;)+?m', '', source) |
|
487 | 485 | |
|
488 | 486 | |
|
489 | 487 | class EvalFormatter(Formatter): |
|
490 | 488 | """A String Formatter that allows evaluation of simple expressions. |
|
491 | 489 | |
|
492 | 490 | Note that this version interprets a `:` as specifying a format string (as per |
|
493 | 491 | standard string formatting), so if slicing is required, you must explicitly |
|
494 | 492 | create a slice. |
|
495 | 493 | |
|
496 | 494 | This is to be used in templating cases, such as the parallel batch |
|
497 | 495 | script templates, where simple arithmetic on arguments is useful. |
|
498 | 496 | |
|
499 | 497 | Examples |
|
500 | 498 | -------- |
|
501 | 499 | :: |
|
502 | 500 | |
|
503 | 501 | In [1]: f = EvalFormatter() |
|
504 | 502 | In [2]: f.format('{n//4}', n=8) |
|
505 | 503 | Out[2]: '2' |
|
506 | 504 | |
|
507 | 505 | In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello") |
|
508 | 506 | Out[3]: 'll' |
|
509 | 507 | """ |
|
510 | 508 | def get_field(self, name, args, kwargs): |
|
511 | 509 | v = eval(name, kwargs) |
|
512 | 510 | return v, name |
|
513 | 511 | |
|
514 | 512 | #XXX: As of Python 3.4, the format string parsing no longer splits on a colon |
|
515 | 513 | # inside [], so EvalFormatter can handle slicing. Once we only support 3.4 and |
|
516 | 514 | # above, it should be possible to remove FullEvalFormatter. |
|
517 | 515 | |
|
518 | 516 | class FullEvalFormatter(Formatter): |
|
519 | 517 | """A String Formatter that allows evaluation of simple expressions. |
|
520 | 518 | |
|
521 | 519 | Any time a format key is not found in the kwargs, |
|
522 | 520 | it will be tried as an expression in the kwargs namespace. |
|
523 | 521 | |
|
524 | 522 | Note that this version allows slicing using [1:2], so you cannot specify |
|
525 | 523 | a format string. Use :class:`EvalFormatter` to permit format strings. |
|
526 | 524 | |
|
527 | 525 | Examples |
|
528 | 526 | -------- |
|
529 | 527 | :: |
|
530 | 528 | |
|
531 | 529 | In [1]: f = FullEvalFormatter() |
|
532 | 530 | In [2]: f.format('{n//4}', n=8) |
|
533 | 531 | Out[2]: '2' |
|
534 | 532 | |
|
535 | 533 | In [3]: f.format('{list(range(5))[2:4]}') |
|
536 | 534 | Out[3]: '[2, 3]' |
|
537 | 535 | |
|
538 | 536 | In [4]: f.format('{3*2}') |
|
539 | 537 | Out[4]: '6' |
|
540 | 538 | """ |
|
541 | 539 | # copied from Formatter._vformat with minor changes to allow eval |
|
542 | 540 | # and replace the format_spec code with slicing |
|
543 | def vformat(self, format_string:str, args, kwargs)->str: | |
|
541 | def vformat(self, format_string: str, args, kwargs) -> str: | |
|
544 | 542 | result = [] |
|
545 | for literal_text, field_name, format_spec, conversion in \ | |
|
546 | self.parse(format_string): | |
|
547 | ||
|
543 | conversion: Optional[str] | |
|
544 | for literal_text, field_name, format_spec, conversion in self.parse( | |
|
545 | format_string | |
|
546 | ): | |
|
548 | 547 | # output the literal text |
|
549 | 548 | if literal_text: |
|
550 | 549 | result.append(literal_text) |
|
551 | 550 | |
|
552 | 551 | # if there's a field, output it |
|
553 | 552 | if field_name is not None: |
|
554 | 553 | # this is some markup, find the object and do |
|
555 | 554 | # the formatting |
|
556 | 555 | |
|
557 | 556 | if format_spec: |
|
558 | 557 | # override format spec, to allow slicing: |
|
559 | 558 | field_name = ':'.join([field_name, format_spec]) |
|
560 | 559 | |
|
561 | 560 | # eval the contents of the field for the object |
|
562 | 561 | # to be formatted |
|
563 | 562 | obj = eval(field_name, kwargs) |
|
564 | 563 | |
|
565 | 564 | # do any conversion on the resulting object |
|
566 | obj = self.convert_field(obj, conversion) | |
|
565 | # type issue in typeshed, fined in https://github.com/python/typeshed/pull/11377 | |
|
566 | obj = self.convert_field(obj, conversion) # type: ignore[arg-type] | |
|
567 | 567 | |
|
568 | 568 | # format the object and append to the result |
|
569 | 569 | result.append(self.format_field(obj, '')) |
|
570 | 570 | |
|
571 | 571 | return ''.join(result) |
|
572 | 572 | |
|
573 | 573 | |
|
574 | 574 | class DollarFormatter(FullEvalFormatter): |
|
575 | 575 | """Formatter allowing Itpl style $foo replacement, for names and attribute |
|
576 | 576 | access only. Standard {foo} replacement also works, and allows full |
|
577 | 577 | evaluation of its arguments. |
|
578 | 578 | |
|
579 | 579 | Examples |
|
580 | 580 | -------- |
|
581 | 581 | :: |
|
582 | 582 | |
|
583 | 583 | In [1]: f = DollarFormatter() |
|
584 | 584 | In [2]: f.format('{n//4}', n=8) |
|
585 | 585 | Out[2]: '2' |
|
586 | 586 | |
|
587 | 587 | In [3]: f.format('23 * 76 is $result', result=23*76) |
|
588 | 588 | Out[3]: '23 * 76 is 1748' |
|
589 | 589 | |
|
590 | 590 | In [4]: f.format('$a or {b}', a=1, b=2) |
|
591 | 591 | Out[4]: '1 or 2' |
|
592 | 592 | """ |
|
593 | 593 | _dollar_pattern_ignore_single_quote = re.compile(r"(.*?)\$(\$?[\w\.]+)(?=([^']*'[^']*')*[^']*$)") |
|
594 | 594 | def parse(self, fmt_string): |
|
595 | 595 | for literal_txt, field_name, format_spec, conversion \ |
|
596 | 596 | in Formatter.parse(self, fmt_string): |
|
597 | 597 | |
|
598 | 598 | # Find $foo patterns in the literal text. |
|
599 | 599 | continue_from = 0 |
|
600 | 600 | txt = "" |
|
601 | 601 | for m in self._dollar_pattern_ignore_single_quote.finditer(literal_txt): |
|
602 | 602 | new_txt, new_field = m.group(1,2) |
|
603 | 603 | # $$foo --> $foo |
|
604 | 604 | if new_field.startswith("$"): |
|
605 | 605 | txt += new_txt + new_field |
|
606 | 606 | else: |
|
607 | 607 | yield (txt + new_txt, new_field, "", None) |
|
608 | 608 | txt = "" |
|
609 | 609 | continue_from = m.end() |
|
610 | 610 | |
|
611 | 611 | # Re-yield the {foo} style pattern |
|
612 | 612 | yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) |
|
613 | 613 | |
|
614 | 614 | def __repr__(self): |
|
615 | 615 | return "<DollarFormatter>" |
|
616 | 616 | |
|
617 | 617 | #----------------------------------------------------------------------------- |
|
618 | 618 | # Utils to columnize a list of string |
|
619 | 619 | #----------------------------------------------------------------------------- |
|
620 | 620 | |
|
621 | 621 | def _col_chunks(l, max_rows, row_first=False): |
|
622 | 622 | """Yield successive max_rows-sized column chunks from l.""" |
|
623 | 623 | if row_first: |
|
624 | 624 | ncols = (len(l) // max_rows) + (len(l) % max_rows > 0) |
|
625 | 625 | for i in range(ncols): |
|
626 | 626 | yield [l[j] for j in range(i, len(l), ncols)] |
|
627 | 627 | else: |
|
628 | 628 | for i in range(0, len(l), max_rows): |
|
629 | 629 | yield l[i:(i + max_rows)] |
|
630 | 630 | |
|
631 | 631 | |
|
632 | 632 | def _find_optimal(rlist, row_first: bool, separator_size: int, displaywidth: int): |
|
633 | 633 | """Calculate optimal info to columnize a list of string""" |
|
634 | 634 | for max_rows in range(1, len(rlist) + 1): |
|
635 | 635 | col_widths = list(map(max, _col_chunks(rlist, max_rows, row_first))) |
|
636 | 636 | sumlength = sum(col_widths) |
|
637 | 637 | ncols = len(col_widths) |
|
638 | 638 | if sumlength + separator_size * (ncols - 1) <= displaywidth: |
|
639 | 639 | break |
|
640 | 640 | return {'num_columns': ncols, |
|
641 | 641 | 'optimal_separator_width': (displaywidth - sumlength) // (ncols - 1) if (ncols - 1) else 0, |
|
642 | 642 | 'max_rows': max_rows, |
|
643 | 643 | 'column_widths': col_widths |
|
644 | 644 | } |
|
645 | 645 | |
|
646 | 646 | |
|
647 | 647 | def _get_or_default(mylist, i, default=None): |
|
648 | 648 | """return list item number, or default if don't exist""" |
|
649 | 649 | if i >= len(mylist): |
|
650 | 650 | return default |
|
651 | 651 | else : |
|
652 | 652 | return mylist[i] |
|
653 | 653 | |
|
654 | 654 | |
|
655 | 655 | def compute_item_matrix( |
|
656 | 656 | items, row_first: bool = False, empty=None, *, separator_size=2, displaywidth=80 |
|
657 | 657 | ) -> Tuple[List[List[int]], Dict[str, int]]: |
|
658 | 658 | """Returns a nested list, and info to columnize items |
|
659 | 659 | |
|
660 | 660 | Parameters |
|
661 | 661 | ---------- |
|
662 | 662 | items |
|
663 | 663 | list of strings to columize |
|
664 | 664 | row_first : (default False) |
|
665 | 665 | Whether to compute columns for a row-first matrix instead of |
|
666 | 666 | column-first (default). |
|
667 | 667 | empty : (default None) |
|
668 | 668 | default value to fill list if needed |
|
669 | 669 | separator_size : int (default=2) |
|
670 | 670 | How much characters will be used as a separation between each columns. |
|
671 | 671 | displaywidth : int (default=80) |
|
672 | 672 | The width of the area onto which the columns should enter |
|
673 | 673 | |
|
674 | 674 | Returns |
|
675 | 675 | ------- |
|
676 | 676 | strings_matrix |
|
677 | 677 | nested list of string, the outer most list contains as many list as |
|
678 | 678 | rows, the innermost lists have each as many element as columns. If the |
|
679 | 679 | total number of elements in `items` does not equal the product of |
|
680 | 680 | rows*columns, the last element of some lists are filled with `None`. |
|
681 | 681 | dict_info |
|
682 | 682 | some info to make columnize easier: |
|
683 | 683 | |
|
684 | 684 | num_columns |
|
685 | 685 | number of columns |
|
686 | 686 | max_rows |
|
687 | 687 | maximum number of rows (final number may be less) |
|
688 | 688 | column_widths |
|
689 | 689 | list of with of each columns |
|
690 | 690 | optimal_separator_width |
|
691 | 691 | best separator width between columns |
|
692 | 692 | |
|
693 | 693 | Examples |
|
694 | 694 | -------- |
|
695 | 695 | :: |
|
696 | 696 | |
|
697 | 697 | In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] |
|
698 | 698 | In [2]: list, info = compute_item_matrix(l, displaywidth=12) |
|
699 | 699 | In [3]: list |
|
700 | 700 | Out[3]: [['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]] |
|
701 | 701 | In [4]: ideal = {'num_columns': 3, 'column_widths': [5, 1, 1], 'optimal_separator_width': 2, 'max_rows': 5} |
|
702 | 702 | In [5]: all((info[k] == ideal[k] for k in ideal.keys())) |
|
703 | 703 | Out[5]: True |
|
704 | 704 | """ |
|
705 | 705 | warnings.warn( |
|
706 | 706 | "`compute_item_matrix` is Pending Deprecation since IPython 8.17." |
|
707 | 707 | "It is considered fro removal in in future version. " |
|
708 | 708 | "Please open an issue if you believe it should be kept.", |
|
709 | 709 | stacklevel=2, |
|
710 | 710 | category=PendingDeprecationWarning, |
|
711 | 711 | ) |
|
712 | 712 | info = _find_optimal( |
|
713 | 713 | list(map(len, items)), |
|
714 | 714 | row_first, |
|
715 | 715 | separator_size=separator_size, |
|
716 | 716 | displaywidth=displaywidth, |
|
717 | 717 | ) |
|
718 | 718 | nrow, ncol = info["max_rows"], info["num_columns"] |
|
719 | 719 | if row_first: |
|
720 | 720 | return ([[_get_or_default(items, r * ncol + c, default=empty) for c in range(ncol)] for r in range(nrow)], info) |
|
721 | 721 | else: |
|
722 | 722 | return ([[_get_or_default(items, c * nrow + r, default=empty) for c in range(ncol)] for r in range(nrow)], info) |
|
723 | 723 | |
|
724 | 724 | |
|
725 | def columnize(items, row_first=False, separator=" ", displaywidth=80, spread=False): | |
|
725 | def columnize( | |
|
726 | items: List[str], | |
|
727 | row_first: bool = False, | |
|
728 | separator: str = " ", | |
|
729 | displaywidth: int = 80, | |
|
730 | spread: bool = False, | |
|
731 | ): | |
|
726 | 732 | """Transform a list of strings into a single string with columns. |
|
727 | 733 | |
|
728 | 734 | Parameters |
|
729 | 735 | ---------- |
|
730 | 736 | items : sequence of strings |
|
731 | 737 | The strings to process. |
|
732 | 738 | row_first : (default False) |
|
733 | 739 | Whether to compute columns for a row-first matrix instead of |
|
734 | 740 | column-first (default). |
|
735 | 741 | separator : str, optional [default is two spaces] |
|
736 | 742 | The string that separates columns. |
|
737 | 743 | displaywidth : int, optional [default is 80] |
|
738 | 744 | Width of the display in number of characters. |
|
739 | 745 | |
|
740 | 746 | Returns |
|
741 | 747 | ------- |
|
742 | 748 | The formatted string. |
|
743 | 749 | """ |
|
744 | 750 | warnings.warn( |
|
745 | 751 | "`columnize` is Pending Deprecation since IPython 8.17." |
|
746 |
"It is considered f |
|
|
752 | "It is considered for removal in future versions. " | |
|
747 | 753 | "Please open an issue if you believe it should be kept.", |
|
748 | 754 | stacklevel=2, |
|
749 | 755 | category=PendingDeprecationWarning, |
|
750 | 756 | ) |
|
751 | 757 | if not items: |
|
752 | 758 | return "\n" |
|
753 | 759 | matrix: List[List[int]] |
|
754 | 760 | matrix, info = compute_item_matrix( |
|
755 | 761 | items, |
|
756 | 762 | row_first=row_first, |
|
757 | 763 | separator_size=len(separator), |
|
758 | 764 | displaywidth=displaywidth, |
|
759 | 765 | ) |
|
760 | 766 | if spread: |
|
761 | 767 | separator = separator.ljust(int(info["optimal_separator_width"])) |
|
762 | 768 | fmatrix: List[filter[int]] = [filter(None, x) for x in matrix] |
|
763 | 769 | sjoin = lambda x: separator.join( |
|
764 | [y.ljust(w, " ") for y, w in zip(x, info["column_widths"])] | |
|
770 | [y.ljust(w, " ") for y, w in zip(x, cast(List[int], info["column_widths"]))] | |
|
765 | 771 | ) |
|
766 | 772 | return "\n".join(map(sjoin, fmatrix)) + "\n" |
|
767 | 773 | |
|
768 | 774 | |
|
769 | 775 | def get_text_list(list_, last_sep=' and ', sep=", ", wrap_item_with=""): |
|
770 | 776 | """ |
|
771 | 777 | Return a string with a natural enumeration of items |
|
772 | 778 | |
|
773 | 779 | >>> get_text_list(['a', 'b', 'c', 'd']) |
|
774 | 780 | 'a, b, c and d' |
|
775 | 781 | >>> get_text_list(['a', 'b', 'c'], ' or ') |
|
776 | 782 | 'a, b or c' |
|
777 | 783 | >>> get_text_list(['a', 'b', 'c'], ', ') |
|
778 | 784 | 'a, b, c' |
|
779 | 785 | >>> get_text_list(['a', 'b'], ' or ') |
|
780 | 786 | 'a or b' |
|
781 | 787 | >>> get_text_list(['a']) |
|
782 | 788 | 'a' |
|
783 | 789 | >>> get_text_list([]) |
|
784 | 790 | '' |
|
785 | 791 | >>> get_text_list(['a', 'b'], wrap_item_with="`") |
|
786 | 792 | '`a` and `b`' |
|
787 | 793 | >>> get_text_list(['a', 'b', 'c', 'd'], " = ", sep=" + ") |
|
788 | 794 | 'a + b + c = d' |
|
789 | 795 | """ |
|
790 | 796 | if len(list_) == 0: |
|
791 | 797 | return '' |
|
792 | 798 | if wrap_item_with: |
|
793 | 799 | list_ = ['%s%s%s' % (wrap_item_with, item, wrap_item_with) for |
|
794 | 800 | item in list_] |
|
795 | 801 | if len(list_) == 1: |
|
796 | 802 | return list_[0] |
|
797 | 803 | return '%s%s%s' % ( |
|
798 | 804 | sep.join(i for i in list_[:-1]), |
|
799 | 805 | last_sep, list_[-1]) |
General Comments 0
You need to be logged in to leave comments.
Login now