pyflyby 1.10.4__cp311-cp311-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyflyby/__init__.py +61 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2228 -0
- pyflyby/_cmdline.py +591 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1383 -0
- pyflyby/_dynimp.py +154 -0
- pyflyby/_fast_iter_modules.cpython-311-darwin.so +0 -0
- pyflyby/_file.py +771 -0
- pyflyby/_flags.py +230 -0
- pyflyby/_format.py +186 -0
- pyflyby/_idents.py +227 -0
- pyflyby/_import_sorting.py +165 -0
- pyflyby/_importclns.py +658 -0
- pyflyby/_importdb.py +535 -0
- pyflyby/_imports2s.py +643 -0
- pyflyby/_importstmt.py +723 -0
- pyflyby/_interactive.py +2113 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +107 -0
- pyflyby/_modules.py +646 -0
- pyflyby/_parse.py +1396 -0
- pyflyby/_py.py +2165 -0
- pyflyby/_saveframe.py +1145 -0
- pyflyby/_saveframe_reader.py +471 -0
- pyflyby/_util.py +458 -0
- pyflyby/_version.py +8 -0
- pyflyby/autoimport.py +20 -0
- pyflyby/etc/pyflyby/canonical.py +10 -0
- pyflyby/etc/pyflyby/common.py +27 -0
- pyflyby/etc/pyflyby/forget.py +10 -0
- pyflyby/etc/pyflyby/mandatory.py +10 -0
- pyflyby/etc/pyflyby/numpy.py +156 -0
- pyflyby/etc/pyflyby/std.py +335 -0
- pyflyby/importdb.py +19 -0
- pyflyby/libexec/pyflyby/colordiff +34 -0
- pyflyby/libexec/pyflyby/diff-colorize +148 -0
- pyflyby/share/emacs/site-lisp/pyflyby.el +112 -0
- pyflyby-1.10.4.data/scripts/collect-exports +76 -0
- pyflyby-1.10.4.data/scripts/collect-imports +58 -0
- pyflyby-1.10.4.data/scripts/find-import +38 -0
- pyflyby-1.10.4.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.10.4.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.10.4.data/scripts/reformat-imports +27 -0
- pyflyby-1.10.4.data/scripts/replace-star-imports +37 -0
- pyflyby-1.10.4.data/scripts/saveframe +299 -0
- pyflyby-1.10.4.data/scripts/tidy-imports +170 -0
- pyflyby-1.10.4.data/scripts/transform-imports +47 -0
- pyflyby-1.10.4.dist-info/METADATA +605 -0
- pyflyby-1.10.4.dist-info/RECORD +53 -0
- pyflyby-1.10.4.dist-info/WHEEL +6 -0
- pyflyby-1.10.4.dist-info/entry_points.txt +4 -0
- pyflyby-1.10.4.dist-info/licenses/LICENSE.txt +19 -0
pyflyby/_py.py
ADDED
|
@@ -0,0 +1,2165 @@
|
|
|
1
|
+
# pyflyby/_py.py
|
|
2
|
+
# Copyright (C) 2014, 2015, 2018, 2019 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
|
|
5
|
+
r"""
|
|
6
|
+
The `py` program (part of the pyflyby project) is a command-line multitool for
|
|
7
|
+
running python code, with heuristic intention guessing, automatic importing,
|
|
8
|
+
and debugging support.
|
|
9
|
+
|
|
10
|
+
Invocation summary
|
|
11
|
+
==================
|
|
12
|
+
|
|
13
|
+
.. code::
|
|
14
|
+
|
|
15
|
+
py [--file] filename.py arg1 arg2 Execute a file
|
|
16
|
+
py [--eval] 'function(arg1, arg2)' Evaluate an expression/statement
|
|
17
|
+
py [--apply] function arg1 arg2 Call function(arg1, arg2)
|
|
18
|
+
py [--module] modname arg1 arg2 Run a module
|
|
19
|
+
|
|
20
|
+
py --map function arg1 arg2 Call function(arg1); function(arg2)
|
|
21
|
+
|
|
22
|
+
py -i 'function(arg1, arg2)' Run file/code/etc, then run IPython
|
|
23
|
+
py --debug 'function(arg1, arg2)' Debug file/code/etc
|
|
24
|
+
py --debug PID Attach debugger to PID
|
|
25
|
+
|
|
26
|
+
py function? Get help for a function or module
|
|
27
|
+
py function?? Get source of a function or module
|
|
28
|
+
|
|
29
|
+
py Start IPython with autoimporter
|
|
30
|
+
py nb Start IPython Notebook with autoimporter
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
py [--add-deprecated-builtins] Inject "breakpoint", "debug_exception",
|
|
34
|
+
"debug_statement", "waitpoint" into
|
|
35
|
+
builtins. This is deprecated, and
|
|
36
|
+
present for backward compatibilty
|
|
37
|
+
but will be removed in the future.
|
|
38
|
+
|
|
39
|
+
Features
|
|
40
|
+
========
|
|
41
|
+
|
|
42
|
+
* Heuristic action mode guessing: If none of --file, --eval, --apply,
|
|
43
|
+
--module, or --map is specified, then guess what to do, choosing one of
|
|
44
|
+
these actions:
|
|
45
|
+
|
|
46
|
+
* Execute (run) a file
|
|
47
|
+
* Evaluate concatenated arguments
|
|
48
|
+
* Run a module
|
|
49
|
+
* Call (apply) a function
|
|
50
|
+
* Evaluate first argument
|
|
51
|
+
|
|
52
|
+
* Automatic importing: All action modes (except run_module) automatically
|
|
53
|
+
import as needed.
|
|
54
|
+
|
|
55
|
+
* Heuristic argument evaluation: By default, `py --eval`, `py --apply`, and
|
|
56
|
+
`py --map` guess whether the arguments should be interpreted as
|
|
57
|
+
expressions or literal strings. A "--" by itself will designate subsequent
|
|
58
|
+
args as strings. A "-" by itself will be replaced by the contents of
|
|
59
|
+
stdin as a string.
|
|
60
|
+
|
|
61
|
+
* Merged eval/exec: Code is eval()ed or exec()ed as appropriate.
|
|
62
|
+
|
|
63
|
+
* Result printing: By default, results are pretty-printed if not None.
|
|
64
|
+
|
|
65
|
+
* Heuristic flags: "print" can be used as a function or a statement.
|
|
66
|
+
|
|
67
|
+
* Matplotlib/pylab integration: show() is called if appropriate to block on
|
|
68
|
+
plots.
|
|
69
|
+
|
|
70
|
+
* Enter debugger upon unhandled exception. (This functionality is enabled
|
|
71
|
+
by default when stdout is a tty. Use --postmortem=no to never use the
|
|
72
|
+
postmortem debugger. Use --postmortem=yes enable even if stdout is not a
|
|
73
|
+
tty. If the postmortem debugger is enabled but /dev/tty is not available,
|
|
74
|
+
then if an exception occurs, py will email instructions for attaching a
|
|
75
|
+
debugger.)
|
|
76
|
+
|
|
77
|
+
* Control-\\ (SIGQUIT) enters debugger while running (and allows continuing).
|
|
78
|
+
|
|
79
|
+
* New builtin functions such as "debugger()".
|
|
80
|
+
|
|
81
|
+
Warning
|
|
82
|
+
=======
|
|
83
|
+
`py` is intended as an interactive tool. When writing shell aliases for
|
|
84
|
+
interactive use, the `--safe` option can be useful. When writing scripts,
|
|
85
|
+
it's better to avoid all heuristic guessing; use regular `python -c ...`, or
|
|
86
|
+
better yet, a full-fledged python program (and run tidy-imports).
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Options
|
|
90
|
+
=======
|
|
91
|
+
|
|
92
|
+
.. code::
|
|
93
|
+
|
|
94
|
+
Global options valid before code argument:
|
|
95
|
+
|
|
96
|
+
--args=string Interpret all arguments as literal strings.
|
|
97
|
+
(The "--" argument also specifies remaining arguments to be
|
|
98
|
+
literal strings.)
|
|
99
|
+
--args=eval Evaluate all arguments as expressions.
|
|
100
|
+
--args=auto (Default) Heuristically guess whether to evaluate arguments
|
|
101
|
+
as literal strings or expressions.
|
|
102
|
+
--output=silent Don't print the result of evaluation.
|
|
103
|
+
--output=str Print str(result).
|
|
104
|
+
--output=repr Print repr(result).
|
|
105
|
+
--output=pprint Print pprint.pformat(result).
|
|
106
|
+
--output=repr-if-not-none
|
|
107
|
+
Print repr(result), but only if result is not None.
|
|
108
|
+
--output=pprint-if-not-none
|
|
109
|
+
Print pprint.pformat(result), but only if result is not None.
|
|
110
|
+
--output=interactive
|
|
111
|
+
(Default) Print result.__interactive_display__() if defined,
|
|
112
|
+
else pprint if result is not None.
|
|
113
|
+
--output=exit Raise SystemExit(result).
|
|
114
|
+
--safe Equivalent to --args=strings and PYFLYBY_PATH=EMPTY.
|
|
115
|
+
--quiet, --q Log only error messages to stderr; omit info and warnings.
|
|
116
|
+
--interactive, --i
|
|
117
|
+
Run an IPython shell after completion
|
|
118
|
+
--debug, --d Run the target code file etc under the debugger. If a PID is
|
|
119
|
+
given, then instead attach a debugger to the target PID.
|
|
120
|
+
--verbose Turn on verbose messages from pyflyby.
|
|
121
|
+
|
|
122
|
+
Pseudo-actions valid before, after, or without code argument:
|
|
123
|
+
|
|
124
|
+
--version Print pyflyby version or version of a module.
|
|
125
|
+
--help, --h, --? Print this help or help for a function or module.
|
|
126
|
+
--source, --?? Print source code for a function or module.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Examples
|
|
130
|
+
========
|
|
131
|
+
|
|
132
|
+
Start IPython with pyflyby autoimporter enabled::
|
|
133
|
+
|
|
134
|
+
$ py
|
|
135
|
+
|
|
136
|
+
Start IPython/Jupyter Notebook with pyflyby autoimporter enabled::
|
|
137
|
+
|
|
138
|
+
$ py nb
|
|
139
|
+
|
|
140
|
+
Find the ASCII value of the letter "j" (apply builtin function)::
|
|
141
|
+
|
|
142
|
+
$ py ord j
|
|
143
|
+
[PYFLYBY] ord('j')
|
|
144
|
+
106
|
|
145
|
+
|
|
146
|
+
Decode a base64-encoded string (apply autoimported function)::
|
|
147
|
+
|
|
148
|
+
$ py b64decode aGVsbG8=
|
|
149
|
+
[PYFLYBY] from base64 import b64decode
|
|
150
|
+
[PYFLYBY] b64decode('aGVsbG8=', altchars=None)
|
|
151
|
+
b'hello'
|
|
152
|
+
|
|
153
|
+
Find the day of the week of some date (apply function in module)::
|
|
154
|
+
|
|
155
|
+
$ py calendar.weekday 2014 7 18
|
|
156
|
+
[PYFLYBY] import calendar
|
|
157
|
+
[PYFLYBY] calendar.weekday(2014, 7, 18)
|
|
158
|
+
4
|
|
159
|
+
|
|
160
|
+
Using named arguments::
|
|
161
|
+
|
|
162
|
+
$ py calendar.weekday --day=16 --month=7 --year=2014
|
|
163
|
+
[PYFLYBY] import calendar
|
|
164
|
+
[PYFLYBY] calendar.weekday(2014, 7, 16)
|
|
165
|
+
2
|
|
166
|
+
|
|
167
|
+
Using short named arguments::
|
|
168
|
+
|
|
169
|
+
$ py calendar.weekday -m 7 -d 15 -y 2014
|
|
170
|
+
[PYFLYBY] import calendar
|
|
171
|
+
[PYFLYBY] calendar.weekday(2014, 7, 15)
|
|
172
|
+
1
|
|
173
|
+
|
|
174
|
+
Invert a matrix (evaluate expression, with autoimporting)::
|
|
175
|
+
|
|
176
|
+
$ py 'matrix("1 3 3; 1 4 3; 1 3 4").I'
|
|
177
|
+
[PYFLYBY] from numpy import matrix
|
|
178
|
+
[PYFLYBY] matrix("1 3 3; 1 4 3; 1 3 4").I
|
|
179
|
+
matrix([[ 7., -3., -3.],
|
|
180
|
+
[-1., 1., 0.],
|
|
181
|
+
[-1., 0., 1.]])
|
|
182
|
+
|
|
183
|
+
Plot cosine (evaluate expression, with autoimporting)::
|
|
184
|
+
|
|
185
|
+
$ py 'plot(cos(arange(30)))'
|
|
186
|
+
[PYFLYBY] from numpy import arange
|
|
187
|
+
[PYFLYBY] from numpy import cos
|
|
188
|
+
[PYFLYBY] from matplotlib.pyplot import plot
|
|
189
|
+
[PYFLYBY] plot(cos(arange(30)))
|
|
190
|
+
<plot>
|
|
191
|
+
|
|
192
|
+
Command-line calculator (multiple arguments)::
|
|
193
|
+
|
|
194
|
+
$ py 3 / 4
|
|
195
|
+
0.75
|
|
196
|
+
|
|
197
|
+
Command-line calculator (single arguments)::
|
|
198
|
+
|
|
199
|
+
$ py '(5+7j) \** 12'
|
|
200
|
+
(65602966976-150532462080j)
|
|
201
|
+
|
|
202
|
+
Rationalize a decimal (apply bound method)::
|
|
203
|
+
|
|
204
|
+
$ py 2.5.as_integer_ratio
|
|
205
|
+
[PYFLYBY] 2.5.as_integer_ratio()
|
|
206
|
+
(5, 2)
|
|
207
|
+
|
|
208
|
+
Rationalize a decimal (apply unbound method)::
|
|
209
|
+
|
|
210
|
+
$ py float.as_integer_ratio 2.5
|
|
211
|
+
[PYFLYBY] float.as_integer_ratio(2.5)
|
|
212
|
+
(5, 2)
|
|
213
|
+
|
|
214
|
+
Rationalize decimals (map/apply)::
|
|
215
|
+
|
|
216
|
+
$ py --map float.as_integer_ratio 2.5 3.5
|
|
217
|
+
[PYFLYBY] float.as_integer_ratio(2.5)
|
|
218
|
+
(5, 2)
|
|
219
|
+
[PYFLYBY] float.as_integer_ratio(3.5)
|
|
220
|
+
(7, 2)
|
|
221
|
+
|
|
222
|
+
Square numbers (map lambda)::
|
|
223
|
+
|
|
224
|
+
$ py --map 'lambda x: x \**2' 3 4 5
|
|
225
|
+
[PYFLYBY] (lambda x: x \**2)(3)
|
|
226
|
+
9
|
|
227
|
+
[PYFLYBY] (lambda x: x \**2)(4)
|
|
228
|
+
16
|
|
229
|
+
[PYFLYBY] (lambda x: x \**2)(5)
|
|
230
|
+
25
|
|
231
|
+
|
|
232
|
+
Find length of string (using "-" for stdin)::
|
|
233
|
+
|
|
234
|
+
$ echo hello | py len -
|
|
235
|
+
[PYFLYBY] len('hello\\n')
|
|
236
|
+
6
|
|
237
|
+
|
|
238
|
+
Run stdin as code::
|
|
239
|
+
|
|
240
|
+
$ echo 'print(sys.argv[1:])' | py - hello world
|
|
241
|
+
[PYFLYBY] import sys
|
|
242
|
+
['hello', 'world']
|
|
243
|
+
|
|
244
|
+
Run libc functions::
|
|
245
|
+
|
|
246
|
+
$ py --quiet --output=none 'CDLL("libc.so.6").printf' %03d 7
|
|
247
|
+
007
|
|
248
|
+
|
|
249
|
+
Download web page::
|
|
250
|
+
|
|
251
|
+
$ py --print 'requests.get(sys.argv[1]).text' http://example.com
|
|
252
|
+
|
|
253
|
+
Get function help::
|
|
254
|
+
|
|
255
|
+
$ py b64decode?
|
|
256
|
+
[PYFLYBY] from base64 import b64decode
|
|
257
|
+
Python signature::
|
|
258
|
+
|
|
259
|
+
>> b64decode(s, altchars=None, validate=False)
|
|
260
|
+
|
|
261
|
+
Command-line signature::
|
|
262
|
+
|
|
263
|
+
$ py b64decode s [altchars [validate]]
|
|
264
|
+
$ py b64decode --s=... [--altchars=...] [--validate=...]
|
|
265
|
+
...
|
|
266
|
+
|
|
267
|
+
Get module help::
|
|
268
|
+
|
|
269
|
+
$ py pandas?
|
|
270
|
+
[PYFLYBY] import pandas
|
|
271
|
+
Version:
|
|
272
|
+
0.13.1
|
|
273
|
+
Filename:
|
|
274
|
+
/usr/local/lib/python2.7/site-packages/pandas/__init__.pyc
|
|
275
|
+
Docstring:
|
|
276
|
+
pandas - a powerful data analysis and manipulation library for Python
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
from contextlib import contextmanager
|
|
282
|
+
from functools import total_ordering
|
|
283
|
+
from pathlib import Path
|
|
284
|
+
from shlex import quote as shquote
|
|
285
|
+
from types import FunctionType, MethodType, ModuleType
|
|
286
|
+
from typing import Any
|
|
287
|
+
import ast
|
|
288
|
+
import builtins
|
|
289
|
+
import inspect
|
|
290
|
+
import os
|
|
291
|
+
import re
|
|
292
|
+
import sys
|
|
293
|
+
import types
|
|
294
|
+
import warnings
|
|
295
|
+
|
|
296
|
+
from pyflyby._autoimp import auto_import, find_missing_imports
|
|
297
|
+
from pyflyby._cmdline import print_version_and_exit, syntax
|
|
298
|
+
from pyflyby._dbg import (add_debug_functions_to_builtins,
|
|
299
|
+
attach_debugger, debug_statement,
|
|
300
|
+
debugger, enable_faulthandler,
|
|
301
|
+
enable_signal_handler_debugger,
|
|
302
|
+
enable_sigterm_handler,
|
|
303
|
+
remote_print_stack)
|
|
304
|
+
from pyflyby._file import Filename, UnsafeFilenameError, which
|
|
305
|
+
from pyflyby._flags import CompilerFlags
|
|
306
|
+
from pyflyby._idents import is_identifier
|
|
307
|
+
from pyflyby._interactive import (get_ipython_terminal_app_with_autoimporter,
|
|
308
|
+
run_ipython_line_magic,
|
|
309
|
+
start_ipython_with_autoimporter)
|
|
310
|
+
from pyflyby._log import logger
|
|
311
|
+
from pyflyby._modules import ModuleHandle
|
|
312
|
+
from pyflyby._parse import PythonBlock
|
|
313
|
+
from pyflyby._util import indent, prefixes, cmp
|
|
314
|
+
|
|
315
|
+
# TODO: add --tidy-imports, etc
|
|
316
|
+
|
|
317
|
+
# TODO: new --action="concat_eval eval apply" etc. specifying multiple
|
|
318
|
+
# actions means try each of them in that order. then --safe can exclude
|
|
319
|
+
# concat-eval, and users can customize which action modes are included.
|
|
320
|
+
# --apply would be equivalent to --action=apply.
|
|
321
|
+
|
|
322
|
+
# TODO: plug-in system. 'py foo' should attempt something that the
|
|
323
|
+
# user/vendor can add to the system. leading candidate: use entry_point
|
|
324
|
+
# system (http://stackoverflow.com/a/774859). other candidates: maybe use
|
|
325
|
+
# python namespace like pyflyby.vendor.foo or pyflyby.commands.foo or
|
|
326
|
+
# pyflyby.magics.foo, or maybe a config file.
|
|
327
|
+
# TODO: note additional features in documentation feature list
|
|
328
|
+
|
|
329
|
+
# TODO: somehow do the right thing with glob.glob vs glob, pprint.pprint vs
|
|
330
|
+
# pprint, etc. Ideas:
|
|
331
|
+
# - make --apply special case detect modules and take module.module
|
|
332
|
+
# - enhance auto_import() to keep track of the context while finding missing imports
|
|
333
|
+
# - enhance auto_import() to scan for calls after importing
|
|
334
|
+
|
|
335
|
+
# TODO: pipe help/source output (all output?) through $PYFLYBY_PAGER (default "less -FRX").
|
|
336
|
+
|
|
337
|
+
# TODO: unparse ast node for info logging
|
|
338
|
+
# https://hg.python.org/cpython/log/tip/Tools/parser/unparse.py
|
|
339
|
+
|
|
340
|
+
# TODO: run_module should detect if the module doesn't check __name__ and
|
|
341
|
+
# therefore is unlikely to be meaningful to use with run_module.
|
|
342
|
+
|
|
343
|
+
# TODO: make sure run_modules etc work correctly with modules under namespace
|
|
344
|
+
# packages.
|
|
345
|
+
|
|
346
|
+
# TODO: detect deeper ImportError, e.g. suppose user accesses module1; module1
|
|
347
|
+
# imports badmodule, which can't be imported successfully and raises
|
|
348
|
+
# ImportError; we should get that ImportError instead of trying other things
|
|
349
|
+
# or turning it into a string. probably do this by changing the place where
|
|
350
|
+
# we import modules to first get the loader, then if import fails, raise a
|
|
351
|
+
# subclass of ImportError.
|
|
352
|
+
|
|
353
|
+
# TODO: provide a way to omit newline in output. maybe --output=write.
|
|
354
|
+
|
|
355
|
+
# TODO: make sure 'py -c' matches behavior of 'python -c' w.r.t. sys.modules["__main__"]
|
|
356
|
+
# $ py -c 'x=3;import sys; print sys.modules["__main__"].__dict__["x"]'
|
|
357
|
+
# mimic runpy.{_TempModule,_run_code}
|
|
358
|
+
|
|
359
|
+
# TODO: refactor this module - maybe to _heuristic.py
|
|
360
|
+
|
|
361
|
+
# TODO: add --profile, --runsnake
|
|
362
|
+
usage = """
|
|
363
|
+
py --- command-line python multitool with automatic importing
|
|
364
|
+
|
|
365
|
+
$ py [--file] filename.py arg1 arg2 Execute file
|
|
366
|
+
$ py [--apply] function arg1 arg2 Call function
|
|
367
|
+
$ py [--eval] 'function(arg1, arg2)' Evaluate code
|
|
368
|
+
$ py [--module] modname arg1 arg2 Run a module
|
|
369
|
+
|
|
370
|
+
$ py --debug file/code... args... Debug code
|
|
371
|
+
$ py --debug PID Attach debugger to PID
|
|
372
|
+
|
|
373
|
+
$ py IPython shell
|
|
374
|
+
""".strip()
|
|
375
|
+
|
|
376
|
+
# Default compiler flags (feature flags) used for all user code. We include
|
|
377
|
+
# "print_function" here, but we also use auto_flags=True, which means
|
|
378
|
+
# print_function may be flipped off if the code contains print statements.
|
|
379
|
+
FLAGS = CompilerFlags(["absolute_import", "with_statement", "division",
|
|
380
|
+
"print_function"])
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _get_argspec(arg:Any) -> inspect.FullArgSpec:
|
|
384
|
+
from inspect import getfullargspec as getargspec, FullArgSpec as ArgSpec
|
|
385
|
+
if isinstance(arg, FunctionType):
|
|
386
|
+
return getargspec(arg)
|
|
387
|
+
elif isinstance(arg, MethodType):
|
|
388
|
+
argspec = getargspec(arg)
|
|
389
|
+
if arg.__self__ is not None:
|
|
390
|
+
# For bound methods, ignore the "self" argument.
|
|
391
|
+
return ArgSpec(argspec.args[1:], *argspec[1:])
|
|
392
|
+
return argspec
|
|
393
|
+
elif isinstance(arg, type):
|
|
394
|
+
if arg.__new__ is not object.__new__:
|
|
395
|
+
argspec = _get_argspec(arg.__new__)
|
|
396
|
+
return ArgSpec(argspec.args[1:], *argspec[1:])
|
|
397
|
+
else:
|
|
398
|
+
argspec = _get_argspec(arg.__init__)
|
|
399
|
+
return ArgSpec(argspec.args[1:], *argspec[1:])
|
|
400
|
+
elif callable(arg):
|
|
401
|
+
# Unknown, probably a built-in method.
|
|
402
|
+
return ArgSpec([], "args", "kwargs", None, [], None, {})
|
|
403
|
+
raise TypeError(
|
|
404
|
+
"_get_argspec: unexpected %s" % (type(arg).__name__,))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _requires_parens_as_function(function_name:str) -> bool:
|
|
408
|
+
"""
|
|
409
|
+
Returns whether the given string of a callable would require parentheses
|
|
410
|
+
around it to call it.
|
|
411
|
+
|
|
412
|
+
>>> _requires_parens_as_function("foo.bar[4]")
|
|
413
|
+
False
|
|
414
|
+
|
|
415
|
+
>>> _requires_parens_as_function("foo+bar")
|
|
416
|
+
True
|
|
417
|
+
|
|
418
|
+
>>> _requires_parens_as_function("(foo+bar)()")
|
|
419
|
+
False
|
|
420
|
+
|
|
421
|
+
>>> _requires_parens_as_function("(foo+bar)")
|
|
422
|
+
False
|
|
423
|
+
|
|
424
|
+
>>> _requires_parens_as_function("(foo)+(bar)")
|
|
425
|
+
True
|
|
426
|
+
|
|
427
|
+
:type function_name:
|
|
428
|
+
``str``
|
|
429
|
+
:rtype:
|
|
430
|
+
``bool``
|
|
431
|
+
"""
|
|
432
|
+
# TODO: this might be obsolete if we use unparse instead of keeping original
|
|
433
|
+
# user formatting (or alternatively, unparse should use something like this).
|
|
434
|
+
|
|
435
|
+
assert isinstance(function_name, str)
|
|
436
|
+
|
|
437
|
+
block = PythonBlock(function_name, flags=FLAGS)
|
|
438
|
+
node = block.expression_ast_node
|
|
439
|
+
if not node:
|
|
440
|
+
# Couldn't parse? Just assume we do need parens for now. Or should
|
|
441
|
+
# we raise an exception here?
|
|
442
|
+
return True
|
|
443
|
+
body = node.body
|
|
444
|
+
# Is it something that doesn't need parentheses?
|
|
445
|
+
if isinstance(body, (ast.Name, ast.Attribute, ast.Call, ast.Subscript)):
|
|
446
|
+
return False
|
|
447
|
+
# Does it already have parentheses?
|
|
448
|
+
n = str(function_name)
|
|
449
|
+
if n.startswith("(") and n.endswith(")"):
|
|
450
|
+
# It has parentheses, superficially. Make sure it's not something
|
|
451
|
+
# like "(foo)+(bar)".
|
|
452
|
+
flags = int(FLAGS) | ast.PyCF_ONLY_AST
|
|
453
|
+
try:
|
|
454
|
+
tnode = compile(n[1:-1], "<unknown>", "eval", flags)
|
|
455
|
+
except SyntaxError:
|
|
456
|
+
return True
|
|
457
|
+
if ast.dump(tnode) == ast.dump(node):
|
|
458
|
+
return False
|
|
459
|
+
else:
|
|
460
|
+
return True
|
|
461
|
+
return True
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _format_call_spec(function_name:str, obj:Any)-> str:
|
|
465
|
+
# using signature() is not strictly identical
|
|
466
|
+
# as it might look at __text_signature__ and/or respect possitional only
|
|
467
|
+
# forward slash:
|
|
468
|
+
# >>> def foo(a, /, b)
|
|
469
|
+
# whcih formatargspec did not do.
|
|
470
|
+
# argspec = _get_argspec(obj)
|
|
471
|
+
# old_callspect = inspect.formatargspec(*argspec)
|
|
472
|
+
# assert old_callspect == callspec , f"{old_callspect} !={callspec}"
|
|
473
|
+
callspec = str(inspect.signature(obj))
|
|
474
|
+
if _requires_parens_as_function(function_name):
|
|
475
|
+
return "(%s)%s" % (function_name, callspec)
|
|
476
|
+
else:
|
|
477
|
+
return "%s%s" % (function_name, callspec)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _build_function_usage_string(function_name:str, obj:Any, prefix:str):
|
|
483
|
+
argspec = _get_argspec(obj)
|
|
484
|
+
|
|
485
|
+
usage = []
|
|
486
|
+
# TODO: colorize
|
|
487
|
+
usage.append("Python signature:")
|
|
488
|
+
usage.append(" >"+">> " + _format_call_spec(function_name, obj))
|
|
489
|
+
usage.append("")
|
|
490
|
+
usage.append("Command-line signature:")
|
|
491
|
+
keywords = argspec.varkw
|
|
492
|
+
if not argspec.args and argspec.varargs and keywords:
|
|
493
|
+
# We have no information about the arguments. It's probably a
|
|
494
|
+
# built-in where getargspec failed.
|
|
495
|
+
usage.append(" $ %s%s ...\n" % (prefix, function_name))
|
|
496
|
+
return "\n".join(usage)
|
|
497
|
+
defaults = argspec.defaults or ()
|
|
498
|
+
first_with_default = len(argspec.args) - len(defaults)
|
|
499
|
+
# Show first alternative of command-line syntax.
|
|
500
|
+
syntax1 = " $ %s%s" % (prefix, shquote(function_name),)
|
|
501
|
+
for i, arg in enumerate(argspec.args):
|
|
502
|
+
if i >= first_with_default:
|
|
503
|
+
syntax1 += " [%s" % (arg,)
|
|
504
|
+
else:
|
|
505
|
+
syntax1 += " %s" % (arg,)
|
|
506
|
+
if argspec.varargs:
|
|
507
|
+
syntax1 += " %s..." % argspec.varargs
|
|
508
|
+
syntax1 += "]" * len(defaults)
|
|
509
|
+
for arg in argspec.kwonlyargs:
|
|
510
|
+
if argspec.kwonlydefaults and arg in argspec.kwonlydefaults:
|
|
511
|
+
syntax1 += " [--%s=...]" % (arg,)
|
|
512
|
+
else:
|
|
513
|
+
syntax1 += " --%s=..." % (arg,)
|
|
514
|
+
if keywords:
|
|
515
|
+
syntax1 += " [--...]"
|
|
516
|
+
usage.append(syntax1)
|
|
517
|
+
# usage.append("or:")
|
|
518
|
+
syntax2 = " $ %s%s" % (prefix, shquote(function_name),)
|
|
519
|
+
for i, arg in enumerate(argspec.args):
|
|
520
|
+
if i >= first_with_default:
|
|
521
|
+
syntax2 += " [--%s=...]" % (arg,)
|
|
522
|
+
else:
|
|
523
|
+
syntax2 += " --%s=..." % (arg,)
|
|
524
|
+
for arg in argspec.kwonlyargs:
|
|
525
|
+
if argspec.kwonlydefaults and arg in argspec.kwonlydefaults:
|
|
526
|
+
syntax2 += " [--%s=...]" % (arg,)
|
|
527
|
+
else:
|
|
528
|
+
syntax2 += " --%s=..." % (arg,)
|
|
529
|
+
if argspec.varargs:
|
|
530
|
+
syntax2 += " %s..." % argspec.varargs
|
|
531
|
+
if keywords:
|
|
532
|
+
syntax2 += " [--...]"
|
|
533
|
+
usage.append(syntax2)
|
|
534
|
+
usage.append("")
|
|
535
|
+
return "\n".join(usage)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class ParseError(Exception):
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class _ParseInterruptedWantHelp(Exception):
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class _ParseInterruptedWantSource(Exception):
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class UserExpr:
|
|
551
|
+
"""
|
|
552
|
+
An expression from user input, and its evaluated value.
|
|
553
|
+
|
|
554
|
+
The expression can come from a string literal or other raw value, or a
|
|
555
|
+
string that is evaluated as an expression, or heuristically chosen.
|
|
556
|
+
|
|
557
|
+
>>> ns = _Namespace()
|
|
558
|
+
|
|
559
|
+
Heuristic auto-evaluation::
|
|
560
|
+
|
|
561
|
+
>>> UserExpr('5+2', ns, "auto").value
|
|
562
|
+
7
|
|
563
|
+
|
|
564
|
+
>>> UserExpr('5j+2', ns, "auto").value
|
|
565
|
+
(2+5j)
|
|
566
|
+
|
|
567
|
+
>>> UserExpr('base64.b64decode("SGFsbG93ZWVu")', ns, "auto").value
|
|
568
|
+
[PYFLYBY] import base64
|
|
569
|
+
b'Halloween'
|
|
570
|
+
|
|
571
|
+
Returning an unparsable argument as a string::
|
|
572
|
+
|
|
573
|
+
>>> UserExpr('Victory Loop', ns, "auto").value
|
|
574
|
+
'Victory Loop'
|
|
575
|
+
|
|
576
|
+
Returning an undefined (and not auto-importable) argument as a string::
|
|
577
|
+
|
|
578
|
+
>>> UserExpr('Willowbrook29817621+5', ns, "auto").value
|
|
579
|
+
'Willowbrook29817621+5'
|
|
580
|
+
|
|
581
|
+
Explicit literal string::
|
|
582
|
+
|
|
583
|
+
>>> UserExpr("2+3", ns, "raw_value").value
|
|
584
|
+
'2+3'
|
|
585
|
+
|
|
586
|
+
>>> UserExpr("'2+3'", ns, "raw_value").value
|
|
587
|
+
"'2+3'"
|
|
588
|
+
|
|
589
|
+
Other raw values::
|
|
590
|
+
|
|
591
|
+
>>> UserExpr(sys.exit, ns, "raw_value").value
|
|
592
|
+
<built-in function exit>
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
def __init__(self, arg, namespace, arg_mode, source=None):
|
|
596
|
+
"""
|
|
597
|
+
Construct a new UserExpr.
|
|
598
|
+
|
|
599
|
+
:type arg:
|
|
600
|
+
``str`` if ``arg_mode`` is "eval" or "auto"; anything if ``arg_mode``
|
|
601
|
+
is "raw_value"
|
|
602
|
+
:param arg:
|
|
603
|
+
Input user argument.
|
|
604
|
+
:type namespace:
|
|
605
|
+
`_Namespace`
|
|
606
|
+
:type arg_mode:
|
|
607
|
+
``str``
|
|
608
|
+
:param arg_mode:
|
|
609
|
+
If ``"raw_value"``, then return ``arg`` unchanged. If ``"eval"``, then
|
|
610
|
+
always evaluate ``arg``. If ``"auto"``, then heuristically evaluate
|
|
611
|
+
if appropriate.
|
|
612
|
+
"""
|
|
613
|
+
if arg_mode == "string":
|
|
614
|
+
arg_mode = "raw_value"
|
|
615
|
+
self._namespace = namespace
|
|
616
|
+
self._original_arg_mode = arg_mode
|
|
617
|
+
self._original_arg = arg
|
|
618
|
+
if arg_mode == "raw_value":
|
|
619
|
+
# self.inferred_arg_mode = "raw_value"
|
|
620
|
+
# self.original_source = None
|
|
621
|
+
if source is None:
|
|
622
|
+
source = PythonBlock(repr(self._original_arg))
|
|
623
|
+
else:
|
|
624
|
+
source = PythonBlock(source)
|
|
625
|
+
self.source = source
|
|
626
|
+
self.value = self._original_arg
|
|
627
|
+
elif arg_mode == "eval":
|
|
628
|
+
if source is not None:
|
|
629
|
+
raise ValueError(
|
|
630
|
+
"UserExpr(): source argument not allowed for eval")
|
|
631
|
+
# self.inferred_arg_mode = "eval"
|
|
632
|
+
self._original_arg_as_source = PythonBlock(arg, flags=FLAGS)
|
|
633
|
+
# self.original_source = self._original_arg_as_source
|
|
634
|
+
elif arg_mode == "auto":
|
|
635
|
+
if source is not None:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
"UserExpr(): source argument not allowed for auto")
|
|
638
|
+
if not isinstance(arg, str):
|
|
639
|
+
raise ValueError(
|
|
640
|
+
"UserExpr(): arg must be a string if arg_mode='auto'")
|
|
641
|
+
self._original_arg_as_source = PythonBlock(arg, flags=FLAGS)
|
|
642
|
+
else:
|
|
643
|
+
raise ValueError("UserExpr(): bad arg_mode=%r" % (arg_mode,))
|
|
644
|
+
|
|
645
|
+
def _infer_and_evaluate(self) -> None:
|
|
646
|
+
if self._original_arg_mode == "raw_value":
|
|
647
|
+
pass
|
|
648
|
+
elif self._original_arg_mode == "eval":
|
|
649
|
+
block = self._original_arg_as_source
|
|
650
|
+
if not (str(block).strip()):
|
|
651
|
+
raise ValueError("empty input")
|
|
652
|
+
self.value = self._namespace.auto_eval(block)
|
|
653
|
+
self.source = self._original_arg_as_source #.pretty_print() # TODO
|
|
654
|
+
elif self._original_arg_mode == "auto":
|
|
655
|
+
block = self._original_arg_as_source
|
|
656
|
+
ERROR = object()
|
|
657
|
+
if not (str(block).strip()):
|
|
658
|
+
value = ERROR
|
|
659
|
+
elif not block.parsable_as_expression:
|
|
660
|
+
value = ERROR
|
|
661
|
+
else:
|
|
662
|
+
try:
|
|
663
|
+
value = self._namespace.auto_eval(block)
|
|
664
|
+
except UnimportableNameError:
|
|
665
|
+
value = ERROR
|
|
666
|
+
if value is ERROR:
|
|
667
|
+
# self.inferred_arg_mode = "raw_value"
|
|
668
|
+
self.value = self._original_arg
|
|
669
|
+
# self.original_source = None
|
|
670
|
+
self.source = PythonBlock(repr(self.value))
|
|
671
|
+
else:
|
|
672
|
+
# self.inferred_arg_mode = "eval"
|
|
673
|
+
self.value = value
|
|
674
|
+
# self.original_source = block
|
|
675
|
+
self.source = block #.pretty_print() # TODO
|
|
676
|
+
else:
|
|
677
|
+
raise AssertionError("internal error")
|
|
678
|
+
self._infer_and_evaluate = lambda: None
|
|
679
|
+
|
|
680
|
+
def __getattr__(self, k):
|
|
681
|
+
self._infer_and_evaluate()
|
|
682
|
+
return object.__getattribute__(self, k)
|
|
683
|
+
|
|
684
|
+
def __str__(self):
|
|
685
|
+
return str(self._original_arg)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _parse_auto_apply_args(argspec, commandline_args, namespace, arg_mode="auto"):
|
|
689
|
+
"""
|
|
690
|
+
Parse command-line arguments heuristically. Arguments that can be
|
|
691
|
+
evaluated are evaluated; otherwise they are treated as strings.
|
|
692
|
+
|
|
693
|
+
:returns:
|
|
694
|
+
``args``, ``kwargs``
|
|
695
|
+
"""
|
|
696
|
+
# This is implemented manually instead of using optparse or argparse. We
|
|
697
|
+
# do so because neither supports dynamic keyword arguments well. Optparse
|
|
698
|
+
# doesn't support parsing known arguments only, and argparse doesn't
|
|
699
|
+
# support turning off interspersed positional arguments.
|
|
700
|
+
def make_expr(arg, arg_mode=arg_mode):
|
|
701
|
+
return UserExpr(arg, namespace, arg_mode)
|
|
702
|
+
|
|
703
|
+
# Create a map from argname to default value.
|
|
704
|
+
defaults = argspec.defaults or ()
|
|
705
|
+
argname2default = {}
|
|
706
|
+
for argname, default in zip(argspec.args[len(argspec.args)-len(defaults):],
|
|
707
|
+
defaults):
|
|
708
|
+
argname2default[argname] = make_expr(default, "raw_value")
|
|
709
|
+
if argspec.kwonlydefaults:
|
|
710
|
+
for argname, default in argspec.kwonlydefaults.items():
|
|
711
|
+
argname2default[argname] = make_expr(default, "raw_value")
|
|
712
|
+
# Create a map from prefix to arguments with that prefix. E.g. {"foo":
|
|
713
|
+
# ["foobar", "foobaz"]}
|
|
714
|
+
prefix2argname = {}
|
|
715
|
+
for argname in argspec.args:
|
|
716
|
+
for prefix in prefixes(argname):
|
|
717
|
+
prefix2argname.setdefault(prefix, []).append(argname)
|
|
718
|
+
for argname in argspec.kwonlyargs:
|
|
719
|
+
for prefix in prefixes(argname):
|
|
720
|
+
prefix2argname.setdefault(prefix, []).append(argname)
|
|
721
|
+
# Enumerate over input arguments.
|
|
722
|
+
got_pos_args = []
|
|
723
|
+
got_keyword_args = {}
|
|
724
|
+
args = list(commandline_args)
|
|
725
|
+
while args:
|
|
726
|
+
arg = args.pop(0)
|
|
727
|
+
if arg in ["--?", "-?", "?"]:
|
|
728
|
+
raise _ParseInterruptedWantHelp
|
|
729
|
+
elif arg in ["--??", "-??", "??"]:
|
|
730
|
+
raise _ParseInterruptedWantSource
|
|
731
|
+
elif arg.startswith("-"):
|
|
732
|
+
if arg == "-":
|
|
733
|
+
# Read from stdin and stuff into next argument as a string.
|
|
734
|
+
data = sys.stdin.read()
|
|
735
|
+
got_pos_args.append(make_expr(data, "string"))
|
|
736
|
+
continue
|
|
737
|
+
elif arg == "--":
|
|
738
|
+
# Treat remaining arguments as strings.
|
|
739
|
+
got_pos_args.extend([make_expr(x, "string") for x in args])
|
|
740
|
+
del args[:]
|
|
741
|
+
continue
|
|
742
|
+
elif arg.startswith("--"):
|
|
743
|
+
argname = arg[2:]
|
|
744
|
+
else:
|
|
745
|
+
argname = arg[1:]
|
|
746
|
+
argname, equalsign, value = argname.partition("=")
|
|
747
|
+
argname = argname.replace("-", "_")
|
|
748
|
+
if not is_identifier(argname):
|
|
749
|
+
raise ParseError("Invalid option name %s" % (argname,))
|
|
750
|
+
matched_argnames = prefix2argname.get(argname, [])
|
|
751
|
+
if len(matched_argnames) == 1:
|
|
752
|
+
argname, = matched_argnames
|
|
753
|
+
elif len(matched_argnames) == 0:
|
|
754
|
+
if equalsign == "":
|
|
755
|
+
if argname in ["help", "h"]:
|
|
756
|
+
raise _ParseInterruptedWantHelp
|
|
757
|
+
if argname in ["source"]:
|
|
758
|
+
raise _ParseInterruptedWantSource
|
|
759
|
+
if not argspec.varkw:
|
|
760
|
+
raise ParseError("Unknown option name %s" %
|
|
761
|
+
(argname,))
|
|
762
|
+
|
|
763
|
+
elif len(matched_argnames) > 1:
|
|
764
|
+
raise ParseError(
|
|
765
|
+
"Ambiguous %s: could mean one of: %s"
|
|
766
|
+
% (argname,
|
|
767
|
+
", ".join("--%s"%s for s in matched_argnames)))
|
|
768
|
+
else:
|
|
769
|
+
raise AssertionError
|
|
770
|
+
if not value:
|
|
771
|
+
try:
|
|
772
|
+
value = args.pop(0)
|
|
773
|
+
except IndexError:
|
|
774
|
+
raise ParseError("Missing argument to %s" % (arg,))
|
|
775
|
+
if value.startswith("--"):
|
|
776
|
+
raise ParseError(
|
|
777
|
+
"Missing argument to %s. "
|
|
778
|
+
"If you really want to use %r as the argument to %s, "
|
|
779
|
+
"then use %s=%s."
|
|
780
|
+
% (arg, value, arg, arg, value))
|
|
781
|
+
got_keyword_args[argname] = make_expr(value)
|
|
782
|
+
else:
|
|
783
|
+
got_pos_args.append(make_expr(arg))
|
|
784
|
+
|
|
785
|
+
parsed_args = []
|
|
786
|
+
parsed_kwargs = {}
|
|
787
|
+
for i, argname in enumerate(argspec.args):
|
|
788
|
+
if i < len(got_pos_args):
|
|
789
|
+
if argname in got_keyword_args:
|
|
790
|
+
raise ParseError(
|
|
791
|
+
"%s specified both as positional argument (%s) "
|
|
792
|
+
"and keyword argument (%s)"
|
|
793
|
+
% (argname, got_pos_args[i], got_keyword_args[argname]))
|
|
794
|
+
expr = got_pos_args[i]
|
|
795
|
+
else:
|
|
796
|
+
try:
|
|
797
|
+
expr = got_keyword_args.pop(argname)
|
|
798
|
+
except KeyError:
|
|
799
|
+
try:
|
|
800
|
+
expr = argname2default[argname]
|
|
801
|
+
except KeyError:
|
|
802
|
+
raise ParseError(
|
|
803
|
+
"missing required argument %s" % (argname,))
|
|
804
|
+
try:
|
|
805
|
+
value = expr.value
|
|
806
|
+
except Exception as e:
|
|
807
|
+
raise ParseError(
|
|
808
|
+
"Error parsing value for --%s=%s: %s: %s"
|
|
809
|
+
% (argname, expr, type(e).__name__, e))
|
|
810
|
+
parsed_args.append(value)
|
|
811
|
+
|
|
812
|
+
for argname in argspec.kwonlyargs:
|
|
813
|
+
try:
|
|
814
|
+
expr = got_keyword_args.pop(argname)
|
|
815
|
+
except KeyError:
|
|
816
|
+
try:
|
|
817
|
+
expr = argname2default[argname]
|
|
818
|
+
except KeyError:
|
|
819
|
+
raise ParseError(
|
|
820
|
+
"missing required keyword argument %s" % (argname,))
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
value = expr.value
|
|
824
|
+
except Exception as e:
|
|
825
|
+
raise ParseError(
|
|
826
|
+
"Error parsing value for --%s=%s: %s: %s"
|
|
827
|
+
% (argname, expr, type(e).__name__, e))
|
|
828
|
+
parsed_kwargs[argname] = value
|
|
829
|
+
|
|
830
|
+
if len(got_pos_args) > len(argspec.args):
|
|
831
|
+
if argspec.varargs:
|
|
832
|
+
for expr in got_pos_args[len(argspec.args):]:
|
|
833
|
+
try:
|
|
834
|
+
value = expr.value
|
|
835
|
+
except Exception as e:
|
|
836
|
+
raise ParseError(
|
|
837
|
+
"Error parsing value for *%s: %s: %s: %s"
|
|
838
|
+
% (argspec.varargs, expr, type(e).__name__, e))
|
|
839
|
+
parsed_args.append(value)
|
|
840
|
+
else:
|
|
841
|
+
max_nargs = len(argspec.args)
|
|
842
|
+
if argspec.defaults:
|
|
843
|
+
expected = "%d-%d" % (max_nargs-len(argspec.defaults),max_nargs)
|
|
844
|
+
else:
|
|
845
|
+
expected = "%d" % (max_nargs,)
|
|
846
|
+
raise ParseError(
|
|
847
|
+
"Too many positional arguments. "
|
|
848
|
+
"Expected %s positional argument(s): %s. Got %d args: %s"
|
|
849
|
+
% (expected, ", ".join(argspec.args),
|
|
850
|
+
len(got_pos_args), " ".join(map(str, got_pos_args))))
|
|
851
|
+
|
|
852
|
+
for argname, expr in sorted(got_keyword_args.items()):
|
|
853
|
+
try:
|
|
854
|
+
parsed_kwargs[argname] = expr.value
|
|
855
|
+
except Exception as e:
|
|
856
|
+
raise ParseError(
|
|
857
|
+
"Error parsing value for --%s=%s: %s: %s"
|
|
858
|
+
% (argname, expr, type(e).__name__, e))
|
|
859
|
+
|
|
860
|
+
return parsed_args, parsed_kwargs
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _format_call(function_name:str, argspec, args, kwargs):
|
|
864
|
+
# TODO: print original unparsed arg strings
|
|
865
|
+
defaults = argspec.defaults or ()
|
|
866
|
+
first_with_default = len(argspec.args) - len(defaults)
|
|
867
|
+
argparts = []
|
|
868
|
+
for i in range(max(len(args), len(argspec.args))):
|
|
869
|
+
if i >= first_with_default and len(args) <= len(argspec.args):
|
|
870
|
+
argparts.append("%s=%r" % (argspec.args[i], args[i]))
|
|
871
|
+
else:
|
|
872
|
+
argparts.append(repr(args[i]))
|
|
873
|
+
for k, v in sorted(kwargs.items()):
|
|
874
|
+
argparts.append("%s=%r" % (k, v))
|
|
875
|
+
if _requires_parens_as_function(function_name):
|
|
876
|
+
function_name = "(%s)" % (function_name,)
|
|
877
|
+
r = "%s(%s)" % (function_name, ", ".join(argparts))
|
|
878
|
+
return r
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class UnimportableNameError(NameError):
|
|
882
|
+
pass
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class NotAFunctionError(Exception):
|
|
886
|
+
pass
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _get_help(expr:UserExpr, verbosity:int=1) -> str:
|
|
890
|
+
"""
|
|
891
|
+
Construct a help string.
|
|
892
|
+
|
|
893
|
+
:type expr:
|
|
894
|
+
`UserExpr`
|
|
895
|
+
:param expr:
|
|
896
|
+
Object to generate help for.
|
|
897
|
+
:rtype:
|
|
898
|
+
``str``
|
|
899
|
+
"""
|
|
900
|
+
# TODO: colorize headers
|
|
901
|
+
result = ""
|
|
902
|
+
obj = expr.value
|
|
903
|
+
name:str = str(expr.source)
|
|
904
|
+
if callable(obj):
|
|
905
|
+
prefix = os.path.basename(sys.orig_argv[0]) + " "
|
|
906
|
+
result += _build_function_usage_string(name, obj, prefix)
|
|
907
|
+
if verbosity == 0:
|
|
908
|
+
include_filename = False
|
|
909
|
+
include_doc = False
|
|
910
|
+
include_source = False
|
|
911
|
+
elif verbosity == 1:
|
|
912
|
+
include_filename = True
|
|
913
|
+
include_doc = True
|
|
914
|
+
include_source = False
|
|
915
|
+
elif verbosity == 2:
|
|
916
|
+
include_filename = True
|
|
917
|
+
include_doc = False
|
|
918
|
+
include_source = True
|
|
919
|
+
else:
|
|
920
|
+
raise ValueError("invalid verbosity=%r" % (verbosity,))
|
|
921
|
+
try:
|
|
922
|
+
version = obj.__version__
|
|
923
|
+
except Exception:
|
|
924
|
+
pass
|
|
925
|
+
else:
|
|
926
|
+
result += "\nVersion:\n %s\n" % (version,)
|
|
927
|
+
if include_filename:
|
|
928
|
+
try:
|
|
929
|
+
filename = inspect.getfile(obj)
|
|
930
|
+
except Exception:
|
|
931
|
+
pass
|
|
932
|
+
else:
|
|
933
|
+
result += "\nFilename:\n %s\n" % (filename,)
|
|
934
|
+
if include_source:
|
|
935
|
+
try:
|
|
936
|
+
source = inspect.getsource(obj)
|
|
937
|
+
except Exception:
|
|
938
|
+
source = ""
|
|
939
|
+
if source:
|
|
940
|
+
# TODO: colorize source
|
|
941
|
+
result += "\nSource:\n%s\n" % (indent(source, " "))
|
|
942
|
+
else:
|
|
943
|
+
source = "(Not available)"
|
|
944
|
+
include_doc = True
|
|
945
|
+
if include_doc:
|
|
946
|
+
doc = (inspect.getdoc(obj) or "").strip() or "(No docstring)"
|
|
947
|
+
result += "\nDocstring:\n%s" % (indent(doc, " "))
|
|
948
|
+
return result
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
_enable_postmortem_debugger = None
|
|
952
|
+
|
|
953
|
+
def _handle_user_exception(exc_info=None):
|
|
954
|
+
"""
|
|
955
|
+
Handle an exception in user code.
|
|
956
|
+
"""
|
|
957
|
+
# TODO: Make tracebacks show user code being executed. IPython does that
|
|
958
|
+
# by stuffing linecache.cache, and also advising linecache.checkcache. We
|
|
959
|
+
# can either advise it ourselves also, or re-use IPython.core.compilerop.
|
|
960
|
+
# Probably better to advise ourselves. Add "<pyflyby-input-md5[:12]>" to
|
|
961
|
+
# linecache. Perhaps do it in a context manager that removes from
|
|
962
|
+
# linecache when done. Advise checkcache to protect any "<pyflyby-*>".
|
|
963
|
+
# Do it for all code compiled from here, including args, debug_statement,
|
|
964
|
+
# etc.
|
|
965
|
+
if exc_info is None:
|
|
966
|
+
exc_info = sys.exc_info()
|
|
967
|
+
if exc_info[2].tb_next:
|
|
968
|
+
exc_info = (exc_info[0], exc_info[1],
|
|
969
|
+
exc_info[2].tb_next) # skip this traceback
|
|
970
|
+
# If ``_enable_postmortem_debugger`` is enabled, then debug the exception.
|
|
971
|
+
# By default, this is enabled run running in a tty.
|
|
972
|
+
# We check isatty(1) here because we want 'py ... | cat' to never go into
|
|
973
|
+
# the debugger. Note that debugger() also checks whether /dev/tty is
|
|
974
|
+
# usable (and if not, waits for attach).
|
|
975
|
+
if _enable_postmortem_debugger:
|
|
976
|
+
# *** Run debugger. ***
|
|
977
|
+
debugger(exc_info)
|
|
978
|
+
# TODO: consider using print_verbose_tb(*exc_info)
|
|
979
|
+
import traceback
|
|
980
|
+
traceback.print_exception(*exc_info)
|
|
981
|
+
raise SystemExit(1)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def auto_apply(function, commandline_args, namespace, arg_mode=None,
|
|
985
|
+
debug=False):
|
|
986
|
+
"""
|
|
987
|
+
Call ``function`` on command-line arguments. Arguments can be positional
|
|
988
|
+
or keyword arguments like "--foo=bar". Arguments are by default
|
|
989
|
+
heuristically evaluated.
|
|
990
|
+
|
|
991
|
+
:type function:
|
|
992
|
+
``UserExpr``
|
|
993
|
+
:param function:
|
|
994
|
+
Function to apply.
|
|
995
|
+
:type commandline_args:
|
|
996
|
+
``list`` of ``str``
|
|
997
|
+
:param commandline_args:
|
|
998
|
+
Arguments to ``function`` as strings.
|
|
999
|
+
:param arg_mode:
|
|
1000
|
+
How to interpret ``commandline_args``. If ``"string"``, then treat them
|
|
1001
|
+
as literal strings. If ``"eval"``, then evaluate all arguments as
|
|
1002
|
+
expressions. If ``"auto"`` (the default), then heuristically decide
|
|
1003
|
+
whether to treat as expressions or strings.
|
|
1004
|
+
"""
|
|
1005
|
+
if not isinstance(function, UserExpr):
|
|
1006
|
+
raise TypeError
|
|
1007
|
+
if not callable(function.value):
|
|
1008
|
+
raise NotAFunctionError("Not a function", function.value)
|
|
1009
|
+
arg_mode = _interpret_arg_mode(arg_mode, default="auto")
|
|
1010
|
+
# Parse command-line arguments.
|
|
1011
|
+
argspec = _get_argspec(function.value)
|
|
1012
|
+
try:
|
|
1013
|
+
args, kwargs = _parse_auto_apply_args(argspec, commandline_args,
|
|
1014
|
+
namespace, arg_mode=arg_mode)
|
|
1015
|
+
except _ParseInterruptedWantHelp:
|
|
1016
|
+
usage = _get_help(function, verbosity=1)
|
|
1017
|
+
print(usage)
|
|
1018
|
+
raise SystemExit(0)
|
|
1019
|
+
except _ParseInterruptedWantSource:
|
|
1020
|
+
usage = _get_help(function, verbosity=2)
|
|
1021
|
+
print(usage)
|
|
1022
|
+
raise SystemExit(0)
|
|
1023
|
+
except ParseError as e:
|
|
1024
|
+
# Failed parsing command-line arguments. Print usage.
|
|
1025
|
+
logger.error(e)
|
|
1026
|
+
usage = _get_help(function, verbosity=(1 if logger.info_enabled else 0))
|
|
1027
|
+
sys.stderr.write("\n" + usage)
|
|
1028
|
+
raise SystemExit(1)
|
|
1029
|
+
# Log what we're doing.
|
|
1030
|
+
|
|
1031
|
+
logger.info("%s", _format_call(str(function.source), argspec, args, kwargs))
|
|
1032
|
+
|
|
1033
|
+
# *** Call the function. ***
|
|
1034
|
+
f = function.value
|
|
1035
|
+
try:
|
|
1036
|
+
if debug:
|
|
1037
|
+
result = debug_statement("f(*args, **kwargs)")
|
|
1038
|
+
else:
|
|
1039
|
+
result = f(*args, **kwargs)
|
|
1040
|
+
return result
|
|
1041
|
+
except SystemExit:
|
|
1042
|
+
raise
|
|
1043
|
+
# TODO: handle "quit" by user here specially instead of returning None.
|
|
1044
|
+
# May need to reimplement pdb.runeval() so we can catch BdbQuit.
|
|
1045
|
+
except:
|
|
1046
|
+
# Handle exception in user code.
|
|
1047
|
+
_handle_user_exception()
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
@total_ordering
|
|
1051
|
+
class LoggedList:
|
|
1052
|
+
"""
|
|
1053
|
+
List that logs which members have not yet been accessed (nor removed).
|
|
1054
|
+
"""
|
|
1055
|
+
|
|
1056
|
+
_ACCESSED = object()
|
|
1057
|
+
|
|
1058
|
+
def __init__(self, items):
|
|
1059
|
+
self._items = list(items)
|
|
1060
|
+
self._unaccessed = list(self._items)
|
|
1061
|
+
|
|
1062
|
+
def append(self, x):
|
|
1063
|
+
self._unaccessed.append(self._ACCESSED)
|
|
1064
|
+
self._items.append(x)
|
|
1065
|
+
|
|
1066
|
+
def count(self):
|
|
1067
|
+
return self._items.count()
|
|
1068
|
+
|
|
1069
|
+
def extend(self, new_items):
|
|
1070
|
+
new_items = list(new_items)
|
|
1071
|
+
self._unaccessed.extend([self._ACCESSED] * len(new_items))
|
|
1072
|
+
self._items.extend(new_items)
|
|
1073
|
+
|
|
1074
|
+
def index(self, x, *start_stop):
|
|
1075
|
+
index = self._items.index(x, *start_stop) # may raise ValueError
|
|
1076
|
+
self._unaccessed[index] = self._ACCESSED
|
|
1077
|
+
return index
|
|
1078
|
+
|
|
1079
|
+
def insert(self, index, x):
|
|
1080
|
+
self._unaccessed.insert(index, self._ACCESSED)
|
|
1081
|
+
self._items.insert(index, x)
|
|
1082
|
+
|
|
1083
|
+
def pop(self, index):
|
|
1084
|
+
self._unaccessed.pop(index)
|
|
1085
|
+
return self._items.pop(index)
|
|
1086
|
+
|
|
1087
|
+
def remove(self, x):
|
|
1088
|
+
index = self._items.index(x)
|
|
1089
|
+
self.pop(index)
|
|
1090
|
+
|
|
1091
|
+
def reverse(self):
|
|
1092
|
+
self._items.reverse()
|
|
1093
|
+
self._unaccessed.reverse()
|
|
1094
|
+
|
|
1095
|
+
def sort(self):
|
|
1096
|
+
indexes = range(len(self._items))
|
|
1097
|
+
indexes.sort(key=self._items.__getitem__) # argsort
|
|
1098
|
+
self._items = [self._items[i] for i in indexes]
|
|
1099
|
+
self._unaccessed = [self._unaccessed[i] for i in indexes]
|
|
1100
|
+
|
|
1101
|
+
def __add__(self, other):
|
|
1102
|
+
return self._items + other
|
|
1103
|
+
|
|
1104
|
+
def __contains__(self, x):
|
|
1105
|
+
try:
|
|
1106
|
+
self.index(x)
|
|
1107
|
+
return True
|
|
1108
|
+
except ValueError:
|
|
1109
|
+
return False
|
|
1110
|
+
|
|
1111
|
+
def __delitem__(self, x):
|
|
1112
|
+
del self._items[x]
|
|
1113
|
+
del self._unaccessed[x]
|
|
1114
|
+
|
|
1115
|
+
def __eq__(self, other):
|
|
1116
|
+
if not isinstance(other, LoggedList):
|
|
1117
|
+
return NotImplemented
|
|
1118
|
+
return self._items == other._items
|
|
1119
|
+
|
|
1120
|
+
def __ne__(self, other):
|
|
1121
|
+
return not (self == other)
|
|
1122
|
+
|
|
1123
|
+
# The rest are defined by total_ordering
|
|
1124
|
+
def __lt__(self, other):
|
|
1125
|
+
if not isinstance(other, LoggedList):
|
|
1126
|
+
return NotImplemented
|
|
1127
|
+
return self._items < other._items
|
|
1128
|
+
|
|
1129
|
+
def __cmp__(self, x):
|
|
1130
|
+
return cmp(self._items, x)
|
|
1131
|
+
|
|
1132
|
+
def __getitem__(self, idx):
|
|
1133
|
+
result = self._items[idx]
|
|
1134
|
+
if isinstance(idx, slice):
|
|
1135
|
+
self._unaccessed[idx] = [self._ACCESSED]*len(result)
|
|
1136
|
+
else:
|
|
1137
|
+
self._unaccessed[idx] = self._ACCESSED
|
|
1138
|
+
return result
|
|
1139
|
+
|
|
1140
|
+
def __hash__(self):
|
|
1141
|
+
raise TypeError("unhashable type: 'LoggedList'")
|
|
1142
|
+
|
|
1143
|
+
def __iadd__(self, x):
|
|
1144
|
+
self.extend(x)
|
|
1145
|
+
|
|
1146
|
+
def __imul__(self, n):
|
|
1147
|
+
self._items *= n
|
|
1148
|
+
self._unaccessed *= n
|
|
1149
|
+
|
|
1150
|
+
def __iter__(self):
|
|
1151
|
+
# Todo: detect mutation while iterating.
|
|
1152
|
+
for i, x in enumerate(self._items):
|
|
1153
|
+
self._unaccessed[i] = self._ACCESSED
|
|
1154
|
+
yield x
|
|
1155
|
+
|
|
1156
|
+
def __len__(self):
|
|
1157
|
+
return len(self._items)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def __mul__(self, n):
|
|
1161
|
+
return self._items * n
|
|
1162
|
+
|
|
1163
|
+
def __reduce__(self):
|
|
1164
|
+
return
|
|
1165
|
+
|
|
1166
|
+
def __repr__(self):
|
|
1167
|
+
self._unaccessed[:] = [self._ACCESSED]*len(self._unaccessed)
|
|
1168
|
+
return repr(self._items)
|
|
1169
|
+
|
|
1170
|
+
def __reversed__(self):
|
|
1171
|
+
# Todo: detect mutation while iterating.
|
|
1172
|
+
for i in reversed(range(len(self._items))):
|
|
1173
|
+
self._unaccessed[i] = self._ACCESSED
|
|
1174
|
+
yield self._items[i]
|
|
1175
|
+
|
|
1176
|
+
def __rmul__(self, n):
|
|
1177
|
+
return self._items * n
|
|
1178
|
+
|
|
1179
|
+
def __setitem__(self, idx, value):
|
|
1180
|
+
self._items[idx] = value
|
|
1181
|
+
if isinstance(idx, slice):
|
|
1182
|
+
self._unaccessed[idx] = [self._ACCESSED]*len(value)
|
|
1183
|
+
else:
|
|
1184
|
+
self._unaccessed[idx] = value
|
|
1185
|
+
|
|
1186
|
+
def __str__(self):
|
|
1187
|
+
self._unaccessed[:] = [self._ACCESSED]*len(self._unaccessed)
|
|
1188
|
+
return str(self._items)
|
|
1189
|
+
|
|
1190
|
+
@property
|
|
1191
|
+
def unaccessed(self):
|
|
1192
|
+
return [x for x in self._unaccessed if x is not self._ACCESSED]
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
@contextmanager
|
|
1196
|
+
def SysArgvCtx(*args):
|
|
1197
|
+
"""
|
|
1198
|
+
Context manager that:
|
|
1199
|
+
* Temporarily sets sys.argv = args.
|
|
1200
|
+
* At end of context, complains if any args were never accessed.
|
|
1201
|
+
"""
|
|
1202
|
+
# There should always be at least one arg, since the first one is
|
|
1203
|
+
# the program name.
|
|
1204
|
+
if not args:
|
|
1205
|
+
raise ValueError("Missing args")
|
|
1206
|
+
nargs = len(args) - 1
|
|
1207
|
+
# Create a list proxy that will log accesses.
|
|
1208
|
+
argv = LoggedList(args)
|
|
1209
|
+
# Don't consider first argument to be interesting.
|
|
1210
|
+
argv[0]
|
|
1211
|
+
orig_argv = list(sys.argv)
|
|
1212
|
+
try:
|
|
1213
|
+
sys.argv = argv
|
|
1214
|
+
# Run context code.
|
|
1215
|
+
yield
|
|
1216
|
+
# Complain if there are unaccessed arguments.
|
|
1217
|
+
unaccessed = argv.unaccessed
|
|
1218
|
+
if not unaccessed:
|
|
1219
|
+
pass
|
|
1220
|
+
else:
|
|
1221
|
+
if nargs == 1:
|
|
1222
|
+
msg = ("You specified a command-line argument, but your code didn't use it: %s"
|
|
1223
|
+
% (unaccessed[0],))
|
|
1224
|
+
elif len(unaccessed) == nargs:
|
|
1225
|
+
msg = ("You specified %d command-line arguments, but your code didn't use them: %s"
|
|
1226
|
+
% (len(unaccessed), " ".join(unaccessed)))
|
|
1227
|
+
else:
|
|
1228
|
+
msg = ("You specified %d command-line arguments, but your code didn't use %d of them: %s"
|
|
1229
|
+
% (nargs, len(unaccessed), " ".join(unaccessed)))
|
|
1230
|
+
msg2 = "\nIf this is intentional, access 'sys.argv[:]' somewhere in your code."
|
|
1231
|
+
logger.error(msg + msg2)
|
|
1232
|
+
raise SystemExit(1)
|
|
1233
|
+
finally:
|
|
1234
|
+
sys.argv = orig_argv
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _as_filename_if_seems_like_filename(arg):
|
|
1238
|
+
"""
|
|
1239
|
+
If ``arg`` seems like a filename, then return it as one.
|
|
1240
|
+
|
|
1241
|
+
>>> bool(_as_filename_if_seems_like_filename("foo.py"))
|
|
1242
|
+
True
|
|
1243
|
+
|
|
1244
|
+
>>> bool(_as_filename_if_seems_like_filename("%foo.py"))
|
|
1245
|
+
False
|
|
1246
|
+
|
|
1247
|
+
>>> bool(_as_filename_if_seems_like_filename("foo(bar)"))
|
|
1248
|
+
False
|
|
1249
|
+
|
|
1250
|
+
>>> bool(_as_filename_if_seems_like_filename("/foo/bar/baz.quux-660470"))
|
|
1251
|
+
True
|
|
1252
|
+
|
|
1253
|
+
>>> bool(_as_filename_if_seems_like_filename("../foo/bar-24084866"))
|
|
1254
|
+
True
|
|
1255
|
+
|
|
1256
|
+
:type arg:
|
|
1257
|
+
``str``
|
|
1258
|
+
:rtype:
|
|
1259
|
+
``Filename``
|
|
1260
|
+
"""
|
|
1261
|
+
try:
|
|
1262
|
+
filename = Filename(arg)
|
|
1263
|
+
except UnsafeFilenameError:
|
|
1264
|
+
# If the filename isn't a "safe" filename, then don't treat it as one,
|
|
1265
|
+
# and don't even check whether it exists. This means that for an
|
|
1266
|
+
# argument like "foo(bar)" or "lambda x:x*y" we won't even check
|
|
1267
|
+
# existence. This is both a performance optimization and a safety
|
|
1268
|
+
# valve to avoid unsafe filenames being created to intercept expressions.
|
|
1269
|
+
return None
|
|
1270
|
+
# If the argument "looks" like a filename and is unlikely to be a python
|
|
1271
|
+
# expression, then assume it is a filename. We assume so regardless of
|
|
1272
|
+
# whether the file actually exists; if it turns out to not exist, we'll
|
|
1273
|
+
# complain later.
|
|
1274
|
+
if arg.startswith("/") or arg.startswith("./") or arg.startswith("../"):
|
|
1275
|
+
return filename
|
|
1276
|
+
if filename.ext == ".py":
|
|
1277
|
+
# TODO: .pyc, .pyo
|
|
1278
|
+
return which(arg) or filename
|
|
1279
|
+
# Even if it doesn't obviously look like a filename, but it does exist as
|
|
1280
|
+
# a filename, then use it as one.
|
|
1281
|
+
if filename.exists:
|
|
1282
|
+
return filename
|
|
1283
|
+
# If it's a plain name and we can find an executable on $PATH, then use
|
|
1284
|
+
# that.
|
|
1285
|
+
if re.match("^[a-zA-Z0-9_-]+$", arg):
|
|
1286
|
+
filename = which(arg)
|
|
1287
|
+
if not filename:
|
|
1288
|
+
return None
|
|
1289
|
+
if not _has_python_shebang(filename):
|
|
1290
|
+
logger.debug("Found %s but it doesn't seem like a python script",
|
|
1291
|
+
filename)
|
|
1292
|
+
return None
|
|
1293
|
+
return filename
|
|
1294
|
+
return None
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _has_python_shebang(filename):
|
|
1298
|
+
"""
|
|
1299
|
+
Return whether the first line contains #!...python...
|
|
1300
|
+
|
|
1301
|
+
Used for confirming that an executable script found via which() is
|
|
1302
|
+
actually supposed to be a python script.
|
|
1303
|
+
|
|
1304
|
+
Note that this test is only needed for scripts found via which(), since
|
|
1305
|
+
otherwise the shebang is not necessary.
|
|
1306
|
+
"""
|
|
1307
|
+
assert isinstance(filename, Filename)
|
|
1308
|
+
with open(str(filename), 'rb') as f:
|
|
1309
|
+
line = f.readline(1024)
|
|
1310
|
+
return line.startswith(b"#!") and b"python" in line
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def _interpret_arg_mode(arg, default="auto"):
|
|
1315
|
+
"""
|
|
1316
|
+
>>> _interpret_arg_mode("Str")
|
|
1317
|
+
'string'
|
|
1318
|
+
"""
|
|
1319
|
+
if arg is None:
|
|
1320
|
+
arg = default
|
|
1321
|
+
if arg == "auto" or arg == "eval" or arg == "string":
|
|
1322
|
+
return arg # optimization for interned strings
|
|
1323
|
+
rarg = str(arg).strip().lower()
|
|
1324
|
+
if rarg in ["eval", "evaluate", "exprs", "expr", "expressions", "expression", "e"]:
|
|
1325
|
+
return "eval"
|
|
1326
|
+
elif rarg in ["strings", "string", "str", "strs", "literal", "literals", "s"]:
|
|
1327
|
+
return "string"
|
|
1328
|
+
elif rarg in ["auto", "automatic", "a"]:
|
|
1329
|
+
return "auto"
|
|
1330
|
+
elif rarg == "error":
|
|
1331
|
+
# Intentionally not documented to user
|
|
1332
|
+
return "error"
|
|
1333
|
+
else:
|
|
1334
|
+
raise ValueError(
|
|
1335
|
+
"Invalid arg_mode=%r; expected one of eval/string/auto"
|
|
1336
|
+
% (arg,))
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _interpret_output_mode(arg, default="interactive"):
|
|
1340
|
+
"""
|
|
1341
|
+
>>> _interpret_output_mode('Repr_If_Not_None')
|
|
1342
|
+
'repr-if-not-none'
|
|
1343
|
+
"""
|
|
1344
|
+
if arg is None:
|
|
1345
|
+
arg = default
|
|
1346
|
+
rarg = str(arg).strip().lower().replace("-", "").replace("_", "")
|
|
1347
|
+
if rarg in ["none", "no", "n", "silent"]:
|
|
1348
|
+
return "silent"
|
|
1349
|
+
elif rarg in ["interactive", "i"]:
|
|
1350
|
+
return "interactive"
|
|
1351
|
+
elif rarg in ["print", "p", "string", "str"]:
|
|
1352
|
+
return "str"
|
|
1353
|
+
elif rarg in ["repr", "r"]:
|
|
1354
|
+
return "repr"
|
|
1355
|
+
elif rarg in ["pprint", "pp"]:
|
|
1356
|
+
return "pprint"
|
|
1357
|
+
elif rarg in ["reprifnotnone", "reprunlessnone", "rn"]:
|
|
1358
|
+
return "repr-if-not-none"
|
|
1359
|
+
elif rarg in ["pprintifnotnone", "pprintunlessnone", "ppn"]:
|
|
1360
|
+
return "pprint-if-not-none"
|
|
1361
|
+
elif rarg in ["systemexit", "exit", "raise"]:
|
|
1362
|
+
return "exit"
|
|
1363
|
+
else:
|
|
1364
|
+
raise ValueError(
|
|
1365
|
+
"Invalid output=%r; expected one of "
|
|
1366
|
+
"silent/interactive/str/repr/pprint/repr-if-not-none/pprint-if-not-none/exit"
|
|
1367
|
+
% (arg,))
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def print_result(result, output_mode):
|
|
1371
|
+
output_mode = _interpret_output_mode(output_mode)
|
|
1372
|
+
if output_mode == "silent":
|
|
1373
|
+
return
|
|
1374
|
+
if output_mode == "interactive":
|
|
1375
|
+
# TODO: support IPython output stuff (text/plain)
|
|
1376
|
+
try:
|
|
1377
|
+
idisp = result.__interactive_display__
|
|
1378
|
+
except Exception:
|
|
1379
|
+
output_mode = "pprint-if-not-none"
|
|
1380
|
+
else:
|
|
1381
|
+
result = idisp()
|
|
1382
|
+
output_mode = "print-if-not-none"
|
|
1383
|
+
# Fall through.
|
|
1384
|
+
if output_mode == "str":
|
|
1385
|
+
print(str(result))
|
|
1386
|
+
elif output_mode == "repr":
|
|
1387
|
+
print(repr(result))
|
|
1388
|
+
elif output_mode == "pprint":
|
|
1389
|
+
import pprint
|
|
1390
|
+
pprint.pprint(result) # or equivalently, print pprint.pformat(result)
|
|
1391
|
+
elif output_mode == "repr-if-not-none":
|
|
1392
|
+
if result is not None:
|
|
1393
|
+
print(repr(result))
|
|
1394
|
+
elif output_mode == "print-if-not-none":
|
|
1395
|
+
if result is not None:
|
|
1396
|
+
print(result)
|
|
1397
|
+
elif output_mode == "pprint-if-not-none":
|
|
1398
|
+
if result is not None:
|
|
1399
|
+
import pprint
|
|
1400
|
+
pprint.pprint(result)
|
|
1401
|
+
elif output_mode == "exit":
|
|
1402
|
+
# TODO: only raise at end after pre_exit
|
|
1403
|
+
raise SystemExit(result)
|
|
1404
|
+
else:
|
|
1405
|
+
raise AssertionError("unexpected output_mode=%r" % (output_mode,))
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _get_path_links(p: Path):
|
|
1409
|
+
"""Gets path links including all symlinks.
|
|
1410
|
+
|
|
1411
|
+
Adapted from `IPython.core.interactiveshell.InteractiveShell.get_path_links`.
|
|
1412
|
+
"""
|
|
1413
|
+
paths = [p]
|
|
1414
|
+
while p.is_symlink():
|
|
1415
|
+
new_path = Path(os.readlink(p))
|
|
1416
|
+
if not new_path.is_absolute():
|
|
1417
|
+
new_path = p.parent / new_path
|
|
1418
|
+
p = new_path
|
|
1419
|
+
paths.append(p)
|
|
1420
|
+
return paths
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _init_virtualenv():
|
|
1424
|
+
"""Add the current virtualenv to sys.path so the user can import modules from it.
|
|
1425
|
+
|
|
1426
|
+
A warning will appear suggesting the user installs IPython in the
|
|
1427
|
+
virtualenv, but for many cases, it probably works well enough.
|
|
1428
|
+
|
|
1429
|
+
Adapted `IPython.core.interactiveshell.InteractiveShell.init_virtualenv`.
|
|
1430
|
+
"""
|
|
1431
|
+
if 'VIRTUAL_ENV' not in os.environ:
|
|
1432
|
+
# Not in a virtualenv
|
|
1433
|
+
return
|
|
1434
|
+
elif os.environ["VIRTUAL_ENV"] == "":
|
|
1435
|
+
warnings.warn("Virtual env path set to '', please check if this is intended.")
|
|
1436
|
+
return
|
|
1437
|
+
|
|
1438
|
+
p = Path(sys.executable)
|
|
1439
|
+
p_venv = Path(os.environ["VIRTUAL_ENV"]).resolve()
|
|
1440
|
+
|
|
1441
|
+
# fallback venv detection:
|
|
1442
|
+
# stdlib venv may symlink sys.executable, so we can't use realpath.
|
|
1443
|
+
# but others can symlink *to* the venv Python, so we can't just use sys.executable.
|
|
1444
|
+
# So we just check every item in the symlink tree (generally <= 3)
|
|
1445
|
+
paths = _get_path_links(p)
|
|
1446
|
+
|
|
1447
|
+
# In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible
|
|
1448
|
+
if len(p_venv.parts) > 2 and p_venv.parts[1] == "cygdrive":
|
|
1449
|
+
drive_name = p_venv.parts[2]
|
|
1450
|
+
p_venv = (drive_name + ":/") / Path(*p_venv.parts[3:])
|
|
1451
|
+
|
|
1452
|
+
if any(p_venv == p.parents[1].resolve() for p in paths):
|
|
1453
|
+
# Our exe is inside or has access to the virtualenv, don't need to do anything.
|
|
1454
|
+
return
|
|
1455
|
+
|
|
1456
|
+
if sys.platform == "win32":
|
|
1457
|
+
virtual_env = str(Path(os.environ["VIRTUAL_ENV"], "Lib", "site-packages"))
|
|
1458
|
+
else:
|
|
1459
|
+
virtual_env_path = Path(
|
|
1460
|
+
os.environ["VIRTUAL_ENV"], "lib", "python{}.{}", "site-packages"
|
|
1461
|
+
)
|
|
1462
|
+
p_ver = sys.version_info[:2]
|
|
1463
|
+
|
|
1464
|
+
# Predict version from py[thon]-x.x in the $VIRTUAL_ENV
|
|
1465
|
+
re_m = re.search(r"\bpy(?:thon)?([23])\.(\d+)\b", os.environ["VIRTUAL_ENV"])
|
|
1466
|
+
if re_m:
|
|
1467
|
+
predicted_path = Path(str(virtual_env_path).format(*re_m.groups()))
|
|
1468
|
+
if predicted_path.exists():
|
|
1469
|
+
p_ver = re_m.groups()
|
|
1470
|
+
|
|
1471
|
+
virtual_env = str(virtual_env_path).format(*p_ver)
|
|
1472
|
+
|
|
1473
|
+
warnings.warn(
|
|
1474
|
+
"Attempting to work in a virtualenv. If you encounter problems, "
|
|
1475
|
+
"please install pyflyby inside the virtualenv."
|
|
1476
|
+
)
|
|
1477
|
+
import site
|
|
1478
|
+
sys.path.insert(0, virtual_env)
|
|
1479
|
+
site.addsitedir(virtual_env)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
class _Namespace(object):
|
|
1483
|
+
fake_main: ModuleType
|
|
1484
|
+
|
|
1485
|
+
def __init__(self):
|
|
1486
|
+
self.fake_main = ModuleType("__main__")
|
|
1487
|
+
_init_virtualenv()
|
|
1488
|
+
self.fake_main.__dict__.setdefault("__builtin__", builtins)
|
|
1489
|
+
self.fake_main.__dict__.setdefault("__builtins__", builtins)
|
|
1490
|
+
self.globals = self.fake_main.__dict__
|
|
1491
|
+
self.autoimported = {}
|
|
1492
|
+
|
|
1493
|
+
def auto_import(self, arg):
|
|
1494
|
+
return auto_import(arg, [self.globals], autoimported=self.autoimported)
|
|
1495
|
+
|
|
1496
|
+
def auto_eval(self, block, mode=None, info=False, auto_import=True,
|
|
1497
|
+
debug=False):
|
|
1498
|
+
"""
|
|
1499
|
+
Evaluate ``block`` with auto-importing.
|
|
1500
|
+
"""
|
|
1501
|
+
# Equivalent to::
|
|
1502
|
+
# auto_eval(arg, mode=mode, flags=FLAGS, globals=self.globals)
|
|
1503
|
+
# but better logging and error raising.
|
|
1504
|
+
if not isinstance(block, PythonBlock):
|
|
1505
|
+
block = PythonBlock(block, flags=FLAGS, auto_flags=True)
|
|
1506
|
+
if auto_import and not self.auto_import(block):
|
|
1507
|
+
missing = find_missing_imports(block, [self.globals])
|
|
1508
|
+
mstr = ", ".join(repr(str(x)) for x in missing)
|
|
1509
|
+
if len(missing) == 1:
|
|
1510
|
+
msg = "name %s is not defined and not importable" % mstr
|
|
1511
|
+
elif len(missing) > 1:
|
|
1512
|
+
msg = "names %s are not defined and not importable" % mstr
|
|
1513
|
+
else:
|
|
1514
|
+
raise AssertionError
|
|
1515
|
+
raise UnimportableNameError(msg)
|
|
1516
|
+
if info:
|
|
1517
|
+
logger.info(block)
|
|
1518
|
+
try:
|
|
1519
|
+
# TODO: enter text into linecache
|
|
1520
|
+
if debug:
|
|
1521
|
+
return debug_statement(block, self.globals)
|
|
1522
|
+
else:
|
|
1523
|
+
code = block.compile(mode=mode)
|
|
1524
|
+
try:
|
|
1525
|
+
main = sys.modules["__main__"]
|
|
1526
|
+
sys.modules["__main__"] = self.fake_main
|
|
1527
|
+
|
|
1528
|
+
return eval(code, self.globals, self.globals)
|
|
1529
|
+
finally:
|
|
1530
|
+
sys.modules["__main__"] = main
|
|
1531
|
+
except SystemExit:
|
|
1532
|
+
raise
|
|
1533
|
+
except:
|
|
1534
|
+
_handle_user_exception()
|
|
1535
|
+
|
|
1536
|
+
def __repr__(self):
|
|
1537
|
+
return "<{} object at 0x{:0x} \nglobals:{} \nautoimported:{}>".format(
|
|
1538
|
+
type(self).__name__, id(self), self.globals, self.autoimported
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class _PyMain(object):
|
|
1543
|
+
|
|
1544
|
+
def __init__(self, args):
|
|
1545
|
+
self.main_args = args
|
|
1546
|
+
self.namespace = _Namespace()
|
|
1547
|
+
self.result = None
|
|
1548
|
+
self.ipython_app = None
|
|
1549
|
+
|
|
1550
|
+
def exec_stdin(self, cmd_args):
|
|
1551
|
+
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
|
|
1552
|
+
output_mode = _interpret_output_mode(self.output_mode, default="silent")
|
|
1553
|
+
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
|
|
1554
|
+
for a in cmd_args]
|
|
1555
|
+
with SysArgvCtx(*cmd_args):
|
|
1556
|
+
result = self.namespace.auto_eval(Filename.STDIN, debug=self.debug)
|
|
1557
|
+
print_result(result, output_mode)
|
|
1558
|
+
self.result = result
|
|
1559
|
+
|
|
1560
|
+
def eval(self, cmd, cmd_args):
|
|
1561
|
+
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
|
|
1562
|
+
output_mode = _interpret_output_mode(self.output_mode)
|
|
1563
|
+
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
|
|
1564
|
+
for a in cmd_args]
|
|
1565
|
+
with SysArgvCtx("-c", *cmd_args):
|
|
1566
|
+
cmd = PythonBlock(cmd)
|
|
1567
|
+
result = self.namespace.auto_eval(cmd, info=True, debug=self.debug)
|
|
1568
|
+
# TODO: make auto_eval() plow ahead even if there are unimportable
|
|
1569
|
+
# names, after warning
|
|
1570
|
+
print_result(result, output_mode)
|
|
1571
|
+
self.result = result
|
|
1572
|
+
|
|
1573
|
+
def execfile(self, filename_arg, cmd_args):
|
|
1574
|
+
# TODO: pass filename to import db target_filename; unit test.
|
|
1575
|
+
# TODO: set __file__
|
|
1576
|
+
# TODO: support compiled (.pyc/.pyo) files
|
|
1577
|
+
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
|
|
1578
|
+
output_mode = _interpret_output_mode(self.output_mode)
|
|
1579
|
+
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
|
|
1580
|
+
for a in cmd_args]
|
|
1581
|
+
additional_msg = ""
|
|
1582
|
+
if isinstance(filename_arg, Filename):
|
|
1583
|
+
filename = filename_arg
|
|
1584
|
+
elif filename_arg == "-":
|
|
1585
|
+
filename = Filename.STDIN
|
|
1586
|
+
elif "/" in filename_arg:
|
|
1587
|
+
filename = Filename(filename_arg)
|
|
1588
|
+
else:
|
|
1589
|
+
filename = which(filename_arg)
|
|
1590
|
+
if not filename:
|
|
1591
|
+
filename = Filename(filename_arg)
|
|
1592
|
+
additional_msg = (" (and didn't find %s on $PATH)"
|
|
1593
|
+
% (filename_arg,))
|
|
1594
|
+
elif not _has_python_shebang(filename):
|
|
1595
|
+
additional_msg = (" (found %s but it doesn't look "
|
|
1596
|
+
"like python source"
|
|
1597
|
+
% (filename,))
|
|
1598
|
+
filename = Filename(filename_arg)
|
|
1599
|
+
if not filename.exists:
|
|
1600
|
+
raise Exception("No such file: %s%s" % (filename, additional_msg))
|
|
1601
|
+
with SysArgvCtx(str(filename), *cmd_args):
|
|
1602
|
+
sys.path.insert(0, str(filename.dir))
|
|
1603
|
+
self.namespace.globals["__file__"] = str(filename)
|
|
1604
|
+
result = self.namespace.auto_eval(filename, debug=self.debug)
|
|
1605
|
+
print_result(result, output_mode)
|
|
1606
|
+
self.result = result
|
|
1607
|
+
|
|
1608
|
+
def apply(self, function, cmd_args):
|
|
1609
|
+
arg_mode = _interpret_arg_mode(self.arg_mode, default="auto")
|
|
1610
|
+
output_mode = _interpret_output_mode(self.output_mode)
|
|
1611
|
+
# Todo: what should we set argv to?
|
|
1612
|
+
result = auto_apply(function, cmd_args, self.namespace, arg_mode,
|
|
1613
|
+
debug=self.debug)
|
|
1614
|
+
print_result(result, output_mode)
|
|
1615
|
+
self.result = result
|
|
1616
|
+
|
|
1617
|
+
def _seems_like_runnable_module(self, arg):
|
|
1618
|
+
if not is_identifier(arg, dotted=True):
|
|
1619
|
+
# It's not a single (dotted) identifier.
|
|
1620
|
+
return False
|
|
1621
|
+
if not find_missing_imports(arg, [{}]):
|
|
1622
|
+
# It's off of a builtin, e.g. "str.upper"
|
|
1623
|
+
return False
|
|
1624
|
+
m = ModuleHandle(arg)
|
|
1625
|
+
if m.parent:
|
|
1626
|
+
# Auto-import the parent, which is necessary in order to get the
|
|
1627
|
+
# filename of the module. ``ModuleHandle.filename`` does this
|
|
1628
|
+
# automatically, but we do it explicitly here so that we log
|
|
1629
|
+
# the import of the parent module.
|
|
1630
|
+
if not self.namespace.auto_import(str(m.parent.name)):
|
|
1631
|
+
return False
|
|
1632
|
+
if not m.filename:
|
|
1633
|
+
logger.debug("Module %s doesn't have a source filename", m)
|
|
1634
|
+
return False
|
|
1635
|
+
# TODO: check that the source accesses __main__ (ast traversal?)
|
|
1636
|
+
return True
|
|
1637
|
+
|
|
1638
|
+
def heuristic_cmd(self, cmd, cmd_args, function_name=None):
|
|
1639
|
+
output_mode = _interpret_output_mode(self.output_mode)
|
|
1640
|
+
# If the "command" is just a module name, then call run_module. Make
|
|
1641
|
+
# sure we check that it's not a builtin.
|
|
1642
|
+
if self._seems_like_runnable_module(str(cmd)):
|
|
1643
|
+
self.heuristic_run_module(str(cmd), cmd_args)
|
|
1644
|
+
return
|
|
1645
|
+
# FIXME TODO heed arg_mode for non-apply case. This is tricky to
|
|
1646
|
+
# implement; will require assigning some proxy class to sys.argv
|
|
1647
|
+
# that's more sophisticated than just logging.
|
|
1648
|
+
with SysArgvCtx("-c", *cmd_args):
|
|
1649
|
+
# Log the expression before we evaluate it, unless we're likely to
|
|
1650
|
+
# log another substantially similar line. (We can only guess
|
|
1651
|
+
# heuristically whether it's interesting enough to log it. And we
|
|
1652
|
+
# can't know whether the result will be callable until we evaluate
|
|
1653
|
+
# it.)
|
|
1654
|
+
info = not re.match("^[a-zA-Z0-9_.]+$", function_name)
|
|
1655
|
+
result = self.namespace.auto_eval(cmd, info=info, debug=self.debug)
|
|
1656
|
+
if callable(result):
|
|
1657
|
+
function = UserExpr(
|
|
1658
|
+
result, self.namespace, "raw_value", function_name)
|
|
1659
|
+
result = auto_apply(function, cmd_args, self.namespace,
|
|
1660
|
+
self.arg_mode, debug=self.debug)
|
|
1661
|
+
print_result(result, output_mode)
|
|
1662
|
+
self.result = result
|
|
1663
|
+
sys.argv[:] # mark as accessed
|
|
1664
|
+
else:
|
|
1665
|
+
if not info:
|
|
1666
|
+
# We guessed wrong earlier and didn't log yet; log now.
|
|
1667
|
+
logger.info(cmd)
|
|
1668
|
+
print_result(result, output_mode)
|
|
1669
|
+
self.result = result
|
|
1670
|
+
unaccessed = sys.argv.unaccessed
|
|
1671
|
+
if unaccessed:
|
|
1672
|
+
logger.error(
|
|
1673
|
+
"%s is not callable. Unexpected argument(s): %s",
|
|
1674
|
+
result, " ".join(unaccessed))
|
|
1675
|
+
sys.argv[:] # don't complain again
|
|
1676
|
+
|
|
1677
|
+
def run_module(self, module, args):
|
|
1678
|
+
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
|
|
1679
|
+
if arg_mode != "string":
|
|
1680
|
+
raise NotImplementedError(
|
|
1681
|
+
"run_module only supports string arguments")
|
|
1682
|
+
module = ModuleHandle(module)
|
|
1683
|
+
logger.info("python -m %s", ' '.join([str(module.name)] + args))
|
|
1684
|
+
# Imitate 'python -m'.
|
|
1685
|
+
# TODO: include only the traceback below runpy.run_module
|
|
1686
|
+
# os.execvp(sys.executable, [sys.argv[0], "-m", modname] + args)
|
|
1687
|
+
sys.argv = [str(module.filename)] + args
|
|
1688
|
+
import runpy
|
|
1689
|
+
if self.debug:
|
|
1690
|
+
# TODO: break closer to user code
|
|
1691
|
+
debugger()
|
|
1692
|
+
try:
|
|
1693
|
+
runpy.run_module(str(module.name), run_name="__main__")
|
|
1694
|
+
except SystemExit:
|
|
1695
|
+
raise
|
|
1696
|
+
except:
|
|
1697
|
+
_handle_user_exception()
|
|
1698
|
+
|
|
1699
|
+
def heuristic_run_module(self, module, args):
|
|
1700
|
+
module = ModuleHandle(module)
|
|
1701
|
+
# If the user ran 'py numpy --version', then print the numpy
|
|
1702
|
+
# version, i.e. same as 'py --version numpy'. This has the
|
|
1703
|
+
# downside of shadowing a possible "--version" feature
|
|
1704
|
+
# implemented by the module itself. However, this is probably
|
|
1705
|
+
# not a big deal, because (1) a full-featured program that
|
|
1706
|
+
# supports --version would normally have a driver script and
|
|
1707
|
+
# not rely on 'python -m foo'; (2) it would probably do
|
|
1708
|
+
# something similar anyway; (3) the user can do 'py -m foo
|
|
1709
|
+
# --version' if necessary.
|
|
1710
|
+
if len(args)==1 and args[0] in ["--version", "-version"]:
|
|
1711
|
+
self.print_version(module)
|
|
1712
|
+
return
|
|
1713
|
+
if len(args)==1 and args[0] in ["--help", "-help", "--h", "-h",
|
|
1714
|
+
"--?", "-?", "?"]:
|
|
1715
|
+
expr = UserExpr(module.module, None, "raw_value",
|
|
1716
|
+
source=str(module.name))
|
|
1717
|
+
usage = _get_help(expr, 1)
|
|
1718
|
+
print(usage)
|
|
1719
|
+
return
|
|
1720
|
+
if len(args)==1 and args[0] in ["--source", "-source",
|
|
1721
|
+
"--??", "-??", "??"]:
|
|
1722
|
+
expr = UserExpr(module.module, None, "raw_value",
|
|
1723
|
+
source=str(module.name))
|
|
1724
|
+
usage = _get_help(expr, 2)
|
|
1725
|
+
print(usage)
|
|
1726
|
+
return
|
|
1727
|
+
# TODO: check if module checks __main__
|
|
1728
|
+
self.run_module(module, args)
|
|
1729
|
+
|
|
1730
|
+
def print_version(self, arg):
|
|
1731
|
+
if not arg:
|
|
1732
|
+
print_version_and_exit()
|
|
1733
|
+
return
|
|
1734
|
+
if isinstance(arg, (ModuleHandle, types.ModuleType)):
|
|
1735
|
+
module = ModuleHandle(arg).module
|
|
1736
|
+
else:
|
|
1737
|
+
module = self.namespace.auto_eval(arg, mode="eval")
|
|
1738
|
+
if not isinstance(module, types.ModuleType):
|
|
1739
|
+
raise TypeError("print_version(): got a %s instead of a module"
|
|
1740
|
+
% (type(module).__name__,))
|
|
1741
|
+
try:
|
|
1742
|
+
version = module.__version__
|
|
1743
|
+
except AttributeError:
|
|
1744
|
+
raise AttributeError(
|
|
1745
|
+
"Module %s does not have a __version__ attribute"
|
|
1746
|
+
% (module.__name__,))
|
|
1747
|
+
print(version)
|
|
1748
|
+
|
|
1749
|
+
def print_help(self, objname, verbosity=1):
|
|
1750
|
+
objname = objname and objname.strip()
|
|
1751
|
+
if not objname:
|
|
1752
|
+
print(__doc__)
|
|
1753
|
+
return
|
|
1754
|
+
expr = UserExpr(objname, self.namespace, "eval")
|
|
1755
|
+
usage = _get_help(expr, verbosity)
|
|
1756
|
+
print(usage)
|
|
1757
|
+
|
|
1758
|
+
def create_ipython_app(self):
|
|
1759
|
+
"""
|
|
1760
|
+
Create an IPython application and initialize it, but don't start it.
|
|
1761
|
+
"""
|
|
1762
|
+
assert self.ipython_app is None
|
|
1763
|
+
self.ipython_app = get_ipython_terminal_app_with_autoimporter()
|
|
1764
|
+
|
|
1765
|
+
def start_ipython(self, args=[]):
|
|
1766
|
+
user_ns = self.namespace.globals
|
|
1767
|
+
start_ipython_with_autoimporter(args, _user_ns=user_ns,
|
|
1768
|
+
app=self.ipython_app)
|
|
1769
|
+
# Don't need to do another interactive session after this one
|
|
1770
|
+
# (i.e. make 'py --interactive' the same as 'py').
|
|
1771
|
+
self.interactive = False
|
|
1772
|
+
|
|
1773
|
+
def _parse_global_opts(self):
|
|
1774
|
+
args = list(self.main_args)
|
|
1775
|
+
self.add_deprecated_builtins = False
|
|
1776
|
+
self.debug = False
|
|
1777
|
+
self.interactive = False
|
|
1778
|
+
self.verbosity = 1
|
|
1779
|
+
self.arg_mode = None
|
|
1780
|
+
self.output_mode = None
|
|
1781
|
+
postmortem = 'auto'
|
|
1782
|
+
while args:
|
|
1783
|
+
arg = args[0]
|
|
1784
|
+
if arg in ["debug", "pdb", "ipdb", "dbg"]:
|
|
1785
|
+
argname = "debug"
|
|
1786
|
+
elif not arg.startswith("-"):
|
|
1787
|
+
break
|
|
1788
|
+
elif arg.startswith("--"):
|
|
1789
|
+
argname = arg[2:]
|
|
1790
|
+
else:
|
|
1791
|
+
argname = arg[1:]
|
|
1792
|
+
argname, equalsign, value = argname.partition("=")
|
|
1793
|
+
def popvalue():
|
|
1794
|
+
if equalsign:
|
|
1795
|
+
return value
|
|
1796
|
+
else:
|
|
1797
|
+
try:
|
|
1798
|
+
return args.pop(0)
|
|
1799
|
+
except IndexError:
|
|
1800
|
+
raise ValueError("expected argument to %s" % arg)
|
|
1801
|
+
def novalue():
|
|
1802
|
+
if equalsign:
|
|
1803
|
+
raise ValueError("unexpected argument %s" % arg)
|
|
1804
|
+
if argname in ["interactive", "i"]:
|
|
1805
|
+
novalue()
|
|
1806
|
+
self.interactive = True
|
|
1807
|
+
del args[0]
|
|
1808
|
+
# Create and initialize the IPython app now (but don't start
|
|
1809
|
+
# it yet). We'll use it later. The reason to initialize it
|
|
1810
|
+
# now is that the code that we're running might check if it's
|
|
1811
|
+
# running in interactive mode based on whether an IPython app
|
|
1812
|
+
# has been initialized. Some user code even initializes
|
|
1813
|
+
# things differently at module import time based on this.
|
|
1814
|
+
self.create_ipython_app()
|
|
1815
|
+
elif argname in ["debug", "pdb", "ipdb", "dbg", "d"]:
|
|
1816
|
+
novalue()
|
|
1817
|
+
self.debug = True
|
|
1818
|
+
del args[0]
|
|
1819
|
+
if argname == "verbose":
|
|
1820
|
+
novalue()
|
|
1821
|
+
logger.set_level("DEBUG")
|
|
1822
|
+
del args[0]
|
|
1823
|
+
continue
|
|
1824
|
+
if argname in ["quiet", "q"]:
|
|
1825
|
+
novalue()
|
|
1826
|
+
logger.set_level("ERROR")
|
|
1827
|
+
del args[0]
|
|
1828
|
+
continue
|
|
1829
|
+
if argname in ["safe"]:
|
|
1830
|
+
del args[0]
|
|
1831
|
+
novalue()
|
|
1832
|
+
self.arg_mode = _interpret_arg_mode("string")
|
|
1833
|
+
# TODO: make this less hacky, something like
|
|
1834
|
+
# self.import_db = ""
|
|
1835
|
+
# TODO: also turn off which() behavior
|
|
1836
|
+
os.environ["PYFLYBY_PATH"] = "EMPTY"
|
|
1837
|
+
continue
|
|
1838
|
+
if argname in ["arguments", "argument", "args", "arg",
|
|
1839
|
+
"arg_mode", "arg-mode", "argmode"]:
|
|
1840
|
+
# Interpret --args=eval|string|auto.
|
|
1841
|
+
# Note that if the user didn't specify --args, then we
|
|
1842
|
+
# intentionally leave ``opts.arg_mode`` set to ``None`` for now,
|
|
1843
|
+
# because the default varies per action.
|
|
1844
|
+
del args[0]
|
|
1845
|
+
self.arg_mode = _interpret_arg_mode(popvalue())
|
|
1846
|
+
continue
|
|
1847
|
+
if argname in ["output", "output_mode", "output-mode",
|
|
1848
|
+
"out", "outmode", "out_mode", "out-mode", "o"]:
|
|
1849
|
+
del args[0]
|
|
1850
|
+
self.output_mode = _interpret_output_mode(popvalue())
|
|
1851
|
+
continue
|
|
1852
|
+
if argname in ["print", "pprint", "silent", "repr"]:
|
|
1853
|
+
del args[0]
|
|
1854
|
+
novalue()
|
|
1855
|
+
self.output_mode = _interpret_output_mode(argname)
|
|
1856
|
+
continue
|
|
1857
|
+
if argname in ["postmortem"]:
|
|
1858
|
+
del args[0]
|
|
1859
|
+
v = (value or "").lower().strip()
|
|
1860
|
+
if v in ["yes", "y", "always", "true", "t", "1", "enable", ""]:
|
|
1861
|
+
postmortem = True
|
|
1862
|
+
elif v in ["no", "n", "never", "false", "f", "0", "disable"]:
|
|
1863
|
+
postmortem = False
|
|
1864
|
+
elif v in ["auto", "automatic", "default", "if-tty"]:
|
|
1865
|
+
postmortem = "auto"
|
|
1866
|
+
else:
|
|
1867
|
+
raise ValueError(
|
|
1868
|
+
"unexpected %s=%s. "
|
|
1869
|
+
"Try --postmortem=yes or --postmortem=no."
|
|
1870
|
+
% (argname, value))
|
|
1871
|
+
continue
|
|
1872
|
+
if argname in ["no-postmortem", "np"]:
|
|
1873
|
+
del args[0]
|
|
1874
|
+
novalue()
|
|
1875
|
+
postmortem = False
|
|
1876
|
+
continue
|
|
1877
|
+
if argname in ["add-deprecated-builtins", "add_deprecated_builtins"]:
|
|
1878
|
+
del args[0]
|
|
1879
|
+
self.add_deprecated_builtins = True
|
|
1880
|
+
continue
|
|
1881
|
+
break
|
|
1882
|
+
self.args = args
|
|
1883
|
+
if postmortem == "auto":
|
|
1884
|
+
postmortem = os.isatty(1)
|
|
1885
|
+
global _enable_postmortem_debugger
|
|
1886
|
+
_enable_postmortem_debugger = postmortem
|
|
1887
|
+
|
|
1888
|
+
def _enable_debug_tools(self, *, add_deprecated: bool):
|
|
1889
|
+
# Enable a bunch of debugging tools.
|
|
1890
|
+
enable_faulthandler()
|
|
1891
|
+
enable_signal_handler_debugger()
|
|
1892
|
+
enable_sigterm_handler()
|
|
1893
|
+
add_debug_functions_to_builtins(add_deprecated=add_deprecated)
|
|
1894
|
+
|
|
1895
|
+
def run(self):
|
|
1896
|
+
# Parse global options.
|
|
1897
|
+
sys.orig_argv = list(sys.argv)
|
|
1898
|
+
self._parse_global_opts()
|
|
1899
|
+
self._enable_debug_tools(add_deprecated=self.add_deprecated_builtins)
|
|
1900
|
+
self._run_action()
|
|
1901
|
+
self._pre_exit()
|
|
1902
|
+
|
|
1903
|
+
def _run_action(self):
|
|
1904
|
+
args = self.args
|
|
1905
|
+
if not args or args[0] == "-":
|
|
1906
|
+
if os.isatty(0):
|
|
1907
|
+
# The user specified no arguments (or only a "-") and stdin is a
|
|
1908
|
+
# tty. Run ipython with autoimporter enabled, i.e. equivalent to
|
|
1909
|
+
# autoipython. Note that we directly start IPython in the same
|
|
1910
|
+
# process, instead of using subprocess.call(['autoipython']),
|
|
1911
|
+
# because the latter messes with Control-C handling.
|
|
1912
|
+
# TODO: add 'py shell' and make this an alias.
|
|
1913
|
+
# TODO: if IPython isn't installed, then do our own
|
|
1914
|
+
# interactive REPL with code.InteractiveConsole, readline, and
|
|
1915
|
+
# autoimporter.
|
|
1916
|
+
self.start_ipython()
|
|
1917
|
+
return
|
|
1918
|
+
else:
|
|
1919
|
+
# Emulate python args.
|
|
1920
|
+
cmd_args = args or [""]
|
|
1921
|
+
# Execute code from stdin, with auto importing.
|
|
1922
|
+
self.exec_stdin(cmd_args)
|
|
1923
|
+
return
|
|
1924
|
+
|
|
1925
|
+
# Consider --action=arg, --action arg, -action=arg, -action arg,
|
|
1926
|
+
# %action arg.
|
|
1927
|
+
arg0 = args.pop(0)
|
|
1928
|
+
if not arg0.strip():
|
|
1929
|
+
raise ValueError("got empty string as first argument")
|
|
1930
|
+
if arg0.startswith("--"):
|
|
1931
|
+
action, equalsign, cmdarg = arg0[2:].partition("=")
|
|
1932
|
+
elif arg0.startswith("-"):
|
|
1933
|
+
action, equalsign, cmdarg = arg0[1:].partition("=")
|
|
1934
|
+
elif arg0.startswith("%"):
|
|
1935
|
+
action, equalsign, cmdarg = arg0[1:], None, None
|
|
1936
|
+
elif len(arg0) > 1 or arg0 == "?":
|
|
1937
|
+
action, equalsign, cmdarg = arg0, None, None
|
|
1938
|
+
else:
|
|
1939
|
+
action, equalsign, cmdarg = None, None, None
|
|
1940
|
+
def popcmdarg():
|
|
1941
|
+
if equalsign:
|
|
1942
|
+
return cmdarg
|
|
1943
|
+
else:
|
|
1944
|
+
try:
|
|
1945
|
+
return args.pop(0)
|
|
1946
|
+
except IndexError:
|
|
1947
|
+
raise ValueError("expected argument to %s" % arg0)
|
|
1948
|
+
def nocmdarg():
|
|
1949
|
+
if equalsign:
|
|
1950
|
+
raise ValueError("unexpected argument %s" % arg0)
|
|
1951
|
+
|
|
1952
|
+
if action in ["eval", "c", "e"]:
|
|
1953
|
+
# Evaluate a command from the command-line, with auto importing.
|
|
1954
|
+
# Supports expressions as well as statements.
|
|
1955
|
+
# Note: Regular python supports "python -cfoo" as equivalent to
|
|
1956
|
+
# "python -c foo". For now, we intentionally don't support that.
|
|
1957
|
+
cmd = popcmdarg()
|
|
1958
|
+
self.eval(cmd, args)
|
|
1959
|
+
elif action in ["file", "execfile", "execf", "runfile", "run", "f",
|
|
1960
|
+
"python"]:
|
|
1961
|
+
# Execute a file named on the command-line, with auto importing.
|
|
1962
|
+
cmd = popcmdarg()
|
|
1963
|
+
self.execfile(cmd, args)
|
|
1964
|
+
elif action in ["apply", "call"]:
|
|
1965
|
+
# Call a function named on the command-line, with auto importing and
|
|
1966
|
+
# auto evaluation of arguments.
|
|
1967
|
+
function_name = popcmdarg()
|
|
1968
|
+
function = UserExpr(function_name, self.namespace, "eval")
|
|
1969
|
+
self.apply(function, args)
|
|
1970
|
+
elif action in ["map"]:
|
|
1971
|
+
# Call function on each argument.
|
|
1972
|
+
# TODO: instead of making this a standalone mode, change this to a
|
|
1973
|
+
# flag that can eval/apply/exec/etc. Set "_" to each argument.
|
|
1974
|
+
# E.g. py --map 'print _' obj1 obj2
|
|
1975
|
+
# py --map _.foo obj1 obj2
|
|
1976
|
+
# py --map '_**2' 3 4 5
|
|
1977
|
+
# when using heuristic mode, "lock in" the action mode on the
|
|
1978
|
+
# first argument.
|
|
1979
|
+
function_name = popcmdarg()
|
|
1980
|
+
function = UserExpr(function_name, self.namespace, "eval")
|
|
1981
|
+
if args and args[0] == '--':
|
|
1982
|
+
for arg in args[1:]:
|
|
1983
|
+
self.apply(function, ['--', arg])
|
|
1984
|
+
else:
|
|
1985
|
+
for arg in args:
|
|
1986
|
+
self.apply(function, [arg])
|
|
1987
|
+
elif action in ["xargs"]:
|
|
1988
|
+
# TODO: read lines from stdin and map. default arg_mode=string
|
|
1989
|
+
raise NotImplementedError("TODO: xargs")
|
|
1990
|
+
elif action in ["module", "m", "runmodule", "run_module", "run-module"]:
|
|
1991
|
+
# Exactly like `python -m'. Intentionally does NOT do auto
|
|
1992
|
+
# importing within the module, because modules should not be
|
|
1993
|
+
# sloppy; they should instead be tidied to have the correct
|
|
1994
|
+
# imports.
|
|
1995
|
+
modname = popcmdarg()
|
|
1996
|
+
self.run_module(modname, args)
|
|
1997
|
+
elif arg0.startswith("-m"):
|
|
1998
|
+
# Support "py -mbase64" in addition to "py -m base64".
|
|
1999
|
+
modname = arg0[2:]
|
|
2000
|
+
self.run_module(modname, args)
|
|
2001
|
+
elif action in ["attach"]:
|
|
2002
|
+
pid = int(popcmdarg())
|
|
2003
|
+
nocmdarg()
|
|
2004
|
+
attach_debugger(pid)
|
|
2005
|
+
elif action in ["stack", "stack_trace", "stacktrace", "backtrace", "bt"]:
|
|
2006
|
+
pid = int(popcmdarg())
|
|
2007
|
+
nocmdarg()
|
|
2008
|
+
print("Stack trace for process %s:" % (pid,))
|
|
2009
|
+
remote_print_stack(pid)
|
|
2010
|
+
elif action in ["ipython", "ip"]:
|
|
2011
|
+
# Start IPython.
|
|
2012
|
+
self.start_ipython(args)
|
|
2013
|
+
elif action in ["notebook", "nb"]:
|
|
2014
|
+
# Start IPython notebook.
|
|
2015
|
+
nocmdarg()
|
|
2016
|
+
self.start_ipython(["notebook"] + args)
|
|
2017
|
+
elif action in ["kernel"]:
|
|
2018
|
+
# Start IPython kernel.
|
|
2019
|
+
nocmdarg()
|
|
2020
|
+
self.start_ipython(["kernel"] + args)
|
|
2021
|
+
elif action in ["qtconsole", "qt"]:
|
|
2022
|
+
# Start IPython qtconsole.
|
|
2023
|
+
nocmdarg()
|
|
2024
|
+
self.start_ipython(["qtconsole"] + args)
|
|
2025
|
+
elif action in ["console"]:
|
|
2026
|
+
# Start IPython console (with new kernel).
|
|
2027
|
+
nocmdarg()
|
|
2028
|
+
self.start_ipython(["console"] + args)
|
|
2029
|
+
elif action in ["existing"]:
|
|
2030
|
+
# Start IPython console (with existing kernel).
|
|
2031
|
+
if equalsign:
|
|
2032
|
+
args.insert(0, cmdarg)
|
|
2033
|
+
self.start_ipython(["console", "--existing"] + args)
|
|
2034
|
+
elif action in ["nbconvert"]:
|
|
2035
|
+
# Start IPython nbconvert. (autoimporter is irrelevant.)
|
|
2036
|
+
if equalsign:
|
|
2037
|
+
args.insert(0, cmdarg)
|
|
2038
|
+
start_ipython_with_autoimporter(["nbconvert"] + args)
|
|
2039
|
+
elif action in ["timeit"]:
|
|
2040
|
+
# TODO: make --timeit and --time flags which work with any mode
|
|
2041
|
+
# and heuristic, instead of only eval.
|
|
2042
|
+
# TODO: fallback if IPython isn't available. above todo probably
|
|
2043
|
+
# requires not using IPython anyway.
|
|
2044
|
+
nocmdarg()
|
|
2045
|
+
run_ipython_line_magic("%timeit " + ' '.join(args))
|
|
2046
|
+
elif action in ["time"]:
|
|
2047
|
+
# TODO: make --timeit and --time flags which work with any mode
|
|
2048
|
+
# and heuristic, instead of only eval.
|
|
2049
|
+
# TODO: fallback if IPython isn't available. above todo probably
|
|
2050
|
+
# requires not using IPython anyway.
|
|
2051
|
+
nocmdarg()
|
|
2052
|
+
run_ipython_line_magic("%time " + ' '.join(args))
|
|
2053
|
+
elif action in ["version"]:
|
|
2054
|
+
if equalsign:
|
|
2055
|
+
args.insert(0, cmdarg)
|
|
2056
|
+
self.print_version(args[0] if args else None)
|
|
2057
|
+
elif action in ["help", "h", "?"]:
|
|
2058
|
+
if equalsign:
|
|
2059
|
+
args.insert(0, cmdarg)
|
|
2060
|
+
self.print_help(args[0] if args else None, verbosity=1)
|
|
2061
|
+
elif action in ["pinfo"]:
|
|
2062
|
+
self.print_help(popcmdarg(), verbosity=1)
|
|
2063
|
+
elif action in ["source", "pinfo2", "??"]:
|
|
2064
|
+
self.print_help(popcmdarg(), verbosity=2)
|
|
2065
|
+
|
|
2066
|
+
elif arg0.startswith("-"):
|
|
2067
|
+
# Unknown argument.
|
|
2068
|
+
msg = "Unknown option %s" % (arg0,)
|
|
2069
|
+
if arg0.startswith("-c"):
|
|
2070
|
+
msg += "; do you mean -c %s?" % (arg0[2:])
|
|
2071
|
+
syntax(msg, usage=usage)
|
|
2072
|
+
|
|
2073
|
+
elif arg0.startswith("??"):
|
|
2074
|
+
# TODO: check number of args
|
|
2075
|
+
self.print_help(arg0[2:], verbosity=2)
|
|
2076
|
+
|
|
2077
|
+
elif arg0.endswith("??"):
|
|
2078
|
+
# TODO: check number of args
|
|
2079
|
+
self.print_help(arg0[:-2], verbosity=2)
|
|
2080
|
+
|
|
2081
|
+
elif arg0.startswith("?"):
|
|
2082
|
+
# TODO: check number of args
|
|
2083
|
+
self.print_help(arg0[1:], verbosity=1)
|
|
2084
|
+
|
|
2085
|
+
elif arg0.endswith("?"):
|
|
2086
|
+
# TODO: check number of args
|
|
2087
|
+
self.print_help(arg0[:-1], verbosity=1)
|
|
2088
|
+
|
|
2089
|
+
elif arg0.startswith("%"):
|
|
2090
|
+
run_ipython_line_magic(' '.join([arg0]+args))
|
|
2091
|
+
|
|
2092
|
+
# Heuristically choose the behavior automatically based on what the
|
|
2093
|
+
# argument looks like.
|
|
2094
|
+
else:
|
|
2095
|
+
filename = _as_filename_if_seems_like_filename(arg0)
|
|
2096
|
+
if filename:
|
|
2097
|
+
# Implied --execfile.
|
|
2098
|
+
self.execfile(filename, args)
|
|
2099
|
+
return
|
|
2100
|
+
if not args and arg0.isdigit():
|
|
2101
|
+
if self.debug:
|
|
2102
|
+
attach_debugger(int(arg0, 10))
|
|
2103
|
+
return
|
|
2104
|
+
else:
|
|
2105
|
+
logger.error(
|
|
2106
|
+
"Use py -d %s if you want to attach a debugger", arg0)
|
|
2107
|
+
raise SystemExit(1)
|
|
2108
|
+
# Implied --eval.
|
|
2109
|
+
# When given multiple arguments, first see if the args can be
|
|
2110
|
+
# concatenated and parsed as a single python program/expression.
|
|
2111
|
+
# But don't try this if any arguments look like options, empty
|
|
2112
|
+
# string or whitespace, etc.
|
|
2113
|
+
# TODO: refactor
|
|
2114
|
+
if (args and
|
|
2115
|
+
self.arg_mode == None and
|
|
2116
|
+
not any(re.match(r"\s*$|-[a-zA-Z-]", a) for a in args)):
|
|
2117
|
+
cmd = PythonBlock(" ".join([arg0]+args),
|
|
2118
|
+
flags=FLAGS, auto_flags=True)
|
|
2119
|
+
if cmd.parsable and self.namespace.auto_import(cmd):
|
|
2120
|
+
with SysArgvCtx("-c"):
|
|
2121
|
+
output_mode = _interpret_output_mode(self.output_mode)
|
|
2122
|
+
result = self.namespace.auto_eval(
|
|
2123
|
+
cmd, info=True, auto_import=False)
|
|
2124
|
+
print_result(result, output_mode)
|
|
2125
|
+
self.result = result
|
|
2126
|
+
return
|
|
2127
|
+
# else fall through
|
|
2128
|
+
# Heuristic based on first arg: try run_module, apply, or exec/eval.
|
|
2129
|
+
cmd = PythonBlock(arg0, flags=FLAGS, auto_flags=True)
|
|
2130
|
+
if not cmd.parsable:
|
|
2131
|
+
logger.error(
|
|
2132
|
+
"Could not interpret as filename or expression: %s",
|
|
2133
|
+
arg0)
|
|
2134
|
+
syntax(usage=usage)
|
|
2135
|
+
self.heuristic_cmd(cmd, args, function_name=arg0)
|
|
2136
|
+
|
|
2137
|
+
def _pre_exit(self):
|
|
2138
|
+
self._pre_exit_matplotlib_show()
|
|
2139
|
+
self._pre_exit_interactive_shell()
|
|
2140
|
+
|
|
2141
|
+
def _pre_exit_matplotlib_show(self):
|
|
2142
|
+
"""
|
|
2143
|
+
If matplotlib.pyplot (pylab) is loaded, then call the show() function.
|
|
2144
|
+
This will cause the program to block until all figures are closed.
|
|
2145
|
+
Without this, a command like 'py plot(...)' would exit immediately.
|
|
2146
|
+
"""
|
|
2147
|
+
if self.interactive:
|
|
2148
|
+
return
|
|
2149
|
+
try:
|
|
2150
|
+
pyplot = sys.modules["matplotlib.pyplot"]
|
|
2151
|
+
except KeyError:
|
|
2152
|
+
return
|
|
2153
|
+
pyplot.show()
|
|
2154
|
+
|
|
2155
|
+
def _pre_exit_interactive_shell(self):
|
|
2156
|
+
if self.interactive:
|
|
2157
|
+
assert self.ipython_app is not None
|
|
2158
|
+
self.namespace.globals["_"] = self.result
|
|
2159
|
+
self.start_ipython()
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def py_main(args=None):
|
|
2163
|
+
if args is None:
|
|
2164
|
+
args = sys.argv[1:]
|
|
2165
|
+
_PyMain(args).run()
|