creeper-adventure/.venv/lib/python3.8/site-packages/pyflyby/_py.py
2022-03-31 20:20:07 -05:00

2046 lines
74 KiB
Python

# pyflyby/_py.py
# Copyright (C) 2014, 2015, 2018, 2019 Karl Chen.
# License: MIT http://opensource.org/licenses/MIT
r"""
The `py` program (part of the pyflyby project) is a command-line multitool for
running python code, with heuristic intention guessing, automatic importing,
and debugging support.
Invocation summary
==================
.. code::
py [--file] filename.py arg1 arg2 Execute a file
py [--eval] 'function(arg1, arg2)' Evaluate an expression/statement
py [--apply] function arg1 arg2 Call function(arg1, arg2)
py [--module] modname arg1 arg2 Run a module
py --map function arg1 arg2 Call function(arg1); function(arg2)
py -i 'function(arg1, arg2)' Run file/code/etc, then run IPython
py --debug 'function(arg1, arg2)' Debug file/code/etc
py --debug PID Attach debugger to PID
py function? Get help for a function or module
py function?? Get source of a function or module
py Start IPython with autoimporter
py nb Start IPython Notebook with autoimporter
Features
========
* Heuristic action mode guessing: If none of --file, --eval, --apply,
--module, or --map is specified, then guess what to do, choosing one of
these actions:
* Execute (run) a file
* Evaluate concatenated arguments
* Run a module
* Call (apply) a function
* Evaluate first argument
* Automatic importing: All action modes (except run_module) automatically
import as needed.
* Heuristic argument evaluation: By default, `py --eval`, `py --apply`, and
`py --map` guess whether the arguments should be interpreted as
expressions or literal strings. A "--" by itself will designate subsequent
args as strings. A "-" by itself will be replaced by the contents of
stdin as a string.
* Merged eval/exec: Code is eval()ed or exec()ed as appropriate.
* Result printing: By default, results are pretty-printed if not None.
* Heuristic flags: "print" can be used as a function or a statement.
* Matplotlib/pylab integration: show() is called if appropriate to block on
plots.
* Enter debugger upon unhandled exception. (This functionality is enabled
by default when stdout is a tty. Use --postmortem=no to never use the
postmortem debugger. Use --postmortem=yes enable even if stdout is not a
tty. If the postmortem debugger is enabled but /dev/tty is not available,
then if an exception occurs, py will email instructions for attaching a
debugger.)
* Control-\\ (SIGQUIT) enters debugger while running (and allows continuing).
* New builtin functions such as "debugger()".
Warning
=======
`py` is intended as an interactive tool. When writing shell aliases for
interactive use, the `--safe` option can be useful. When writing scripts,
it's better to avoid all heuristic guessing; use regular `python -c ...`, or
better yet, a full-fledged python program (and run tidy-imports).
Options
=======
.. code::
Global options valid before code argument:
--args=string Interpret all arguments as literal strings.
(The "--" argument also specifies remaining arguments to be
literal strings.)
--args=eval Evaluate all arguments as expressions.
--args=auto (Default) Heuristically guess whether to evaluate arguments
as literal strings or expressions.
--output=silent Don't print the result of evaluation.
--output=str Print str(result).
--output=repr Print repr(result).
--output=pprint Print pprint.pformat(result).
--output=repr-if-not-none
Print repr(result), but only if result is not None.
--output=pprint-if-not-none
Print pprint.pformat(result), but only if result is not None.
--output=interactive
(Default) Print result.__interactive_display__() if defined,
else pprint if result is not None.
--output=exit Raise SystemExit(result).
--safe Equivalent to --args=strings and PYFLYBY_PATH=EMPTY.
--quiet, --q Log only error messages to stderr; omit info and warnings.
--interactive, --i
Run an IPython shell after completion
--debug, --d Run the target code file etc under the debugger. If a PID is
given, then instead attach a debugger to the target PID.
--verbose Turn on verbose messages from pyflyby.
Pseudo-actions valid before, after, or without code argument:
--version Print pyflyby version or version of a module.
--help, --h, --? Print this help or help for a function or module.
--source, --?? Print source code for a function or module.
Examples
========
Start IPython with pyflyby autoimporter enabled::
$ py
Start IPython/Jupyter Notebook with pyflyby autoimporter enabled::
$ py nb
Find the ASCII value of the letter "j" (apply builtin function)::
$ py ord j
[PYFLYBY] ord('j')
106
Decode a base64-encoded string (apply autoimported function)::
$ py b64decode aGVsbG8=
[PYFLYBY] from base64 import b64decode
[PYFLYBY] b64decode('aGVsbG8=', altchars=None)
b'hello'
Find the day of the week of some date (apply function in module)::
$ py calendar.weekday 2014 7 18
[PYFLYBY] import calendar
[PYFLYBY] calendar.weekday(2014, 7, 18)
4
Using named arguments::
$ py calendar.weekday --day=16 --month=7 --year=2014
[PYFLYBY] import calendar
[PYFLYBY] calendar.weekday(2014, 7, 16)
2
Using short named arguments::
$ py calendar.weekday -m 7 -d 15 -y 2014
[PYFLYBY] import calendar
[PYFLYBY] calendar.weekday(2014, 7, 15)
1
Invert a matrix (evaluate expression, with autoimporting)::
$ py 'matrix("1 3 3; 1 4 3; 1 3 4").I'
[PYFLYBY] from numpy import matrix
[PYFLYBY] matrix("1 3 3; 1 4 3; 1 3 4").I
matrix([[ 7., -3., -3.],
[-1., 1., 0.],
[-1., 0., 1.]])
Plot cosine (evaluate expression, with autoimporting)::
$ py 'plot(cos(arange(30)))'
[PYFLYBY] from numpy import arange
[PYFLYBY] from numpy import cos
[PYFLYBY] from matplotlib.pyplot import plot
[PYFLYBY] plot(cos(arange(30)))
<plot>
Command-line calculator (multiple arguments)::
$ py 3 / 4
0.75
Command-line calculator (single arguments)::
$ py '(5+7j) \** 12'
(65602966976-150532462080j)
Rationalize a decimal (apply bound method)::
$ py 2.5.as_integer_ratio
[PYFLYBY] 2.5.as_integer_ratio()
(5, 2)
Rationalize a decimal (apply unbound method)::
$ py float.as_integer_ratio 2.5
[PYFLYBY] float.as_integer_ratio(2.5)
(5, 2)
Rationalize decimals (map/apply)::
$ py --map float.as_integer_ratio 2.5 3.5
[PYFLYBY] float.as_integer_ratio(2.5)
(5, 2)
[PYFLYBY] float.as_integer_ratio(3.5)
(7, 2)
Square numbers (map lambda)::
$ py --map 'lambda x: x \**2' 3 4 5
[PYFLYBY] (lambda x: x \**2)(3)
9
[PYFLYBY] (lambda x: x \**2)(4)
16
[PYFLYBY] (lambda x: x \**2)(5)
25
Find length of string (using "-" for stdin)::
$ echo hello | py len -
[PYFLYBY] len('hello\\n')
6
Run stdin as code::
$ echo 'print(sys.argv[1:])' | py - hello world
[PYFLYBY] import sys
['hello', 'world']
Run libc functions::
$ py --quiet --output=none 'CDLL("libc.so.6").printf' %03d 7
007
Download web page::
$ py --print 'requests.get(sys.argv[1]).text' http://example.com
Get function help::
$ py b64decode?
[PYFLYBY] from base64 import b64decode
Python signature::
>> b64decode(s, altchars=None, validate=False)
Command-line signature::
$ py b64decode s [altchars [validate]]
$ py b64decode --s=... [--altchars=...] [--validate=...]
...
Get module help::
$ py pandas?
[PYFLYBY] import pandas
Version:
0.13.1
Filename:
/usr/local/lib/python2.7/site-packages/pandas/__init__.pyc
Docstring:
pandas - a powerful data analysis and manipulation library for Python
...
"""
from __future__ import (absolute_import, division, print_function,
with_statement)
from functools import total_ordering
from pyflyby._util import cmp
usage = """
py --- command-line python multitool with automatic importing
$ py [--file] filename.py arg1 arg2 Execute file
$ py [--apply] function arg1 arg2 Call function
$ py [--eval] 'function(arg1, arg2)' Evaluate code
$ py [--module] modname arg1 arg2 Run a module
$ py --debug file/code... args... Debug code
$ py --debug PID Attach debugger to PID
$ py IPython shell
""".strip()
# TODO: add --tidy-imports, etc
# TODO: new --action="concat_eval eval apply" etc. specifying multiple
# actions means try each of them in that order. then --safe can exclude
# concat-eval, and users can customize which action modes are included.
# --apply would be equivalent to --action=apply.
# TODO: plug-in system. 'py foo' should attempt something that the
# user/vendor can add to the system. leading candidate: use entry_point
# system (http://stackoverflow.com/a/774859). other candidates: maybe use
# python namespace like pyflyby.vendor.foo or pyflyby.commands.foo or
# pyflyby.magics.foo, or maybe a config file.
# TODO: note additional features in documentation feature list
# TODO: somehow do the right thing with glob.glob vs glob, pprint.pprint vs
# pprint, etc. Ideas:
# - make --apply special case detect modules and take module.module
# - enhance auto_import() to keep track of the context while finding missing imports
# - enhance auto_import() to scan for calls after importing
# TODO: pipe help/source output (all output?) through $PYFLYBY_PAGER (default "less -FRX").
# TODO: unparse ast node for info logging
# https://hg.python.org/cpython/log/tip/Tools/parser/unparse.py
# TODO: run_module should detect if the module doesn't check __name__ and
# therefore is unlikely to be meaningful to use with run_module.
# TODO: make sure run_modules etc work correctly with modules under namespace
# packages.
# TODO: detect deeper ImportError, e.g. suppose user accesses module1; module1
# imports badmodule, which can't be imported successfully and raises
# ImportError; we should get that ImportError instead of trying other things
# or turning it into a string. probably do this by changing the place where
# we import modules to first get the loader, then if import fails, raise a
# subclass of ImportError.
# TODO: provide a way to omit newline in output. maybe --output=write.
# TODO: make sure 'py -c' matches behavior of 'python -c' w.r.t. sys.modules["__main__"]
# $ py -c 'x=3;import sys; print sys.modules["__main__"].__dict__["x"]'
# mimic runpy.{_TempModule,_run_code}
# TODO: refactor this module - maybe to _heuristic.py
# TODO: add --profile, --runsnake
import ast
from contextlib import contextmanager
import inspect
import os
import re
import six
from six.moves import builtins
import sys
import types
from types import FunctionType, MethodType
from pyflyby._autoimp import auto_import, find_missing_imports
from pyflyby._cmdline import print_version_and_exit, syntax
from pyflyby._dbg import (add_debug_functions_to_builtins,
attach_debugger, debug_statement,
debugger, enable_faulthandler,
enable_signal_handler_debugger,
enable_sigterm_handler,
remote_print_stack)
from pyflyby._file import Filename, UnsafeFilenameError, which
from pyflyby._flags import CompilerFlags
from pyflyby._idents import is_identifier
from pyflyby._interactive import (get_ipython_terminal_app_with_autoimporter,
run_ipython_line_magic,
start_ipython_with_autoimporter)
from pyflyby._log import logger
from pyflyby._modules import ModuleHandle
from pyflyby._parse import PythonBlock
from pyflyby._util import indent, prefixes
# Default compiler flags (feature flags) used for all user code. We include
# "print_function" here, but we also use auto_flags=True, which means
# print_function may be flipped off if the code contains print statements.
FLAGS = CompilerFlags(["absolute_import", "with_statement", "division",
"print_function"])
def _get_argspec(arg, _recurse=False):
from inspect import getargspec, ArgSpec
if isinstance(arg, FunctionType):
return getargspec(arg)
elif isinstance(arg, MethodType):
argspec = getargspec(arg)
if arg.__self__ is not None:
# For bound methods, ignore the "self" argument.
return ArgSpec(argspec.args[1:], *argspec[1:])
return argspec
elif isinstance(arg, type):
if arg.__new__ is not object.__new__:
argspec = _get_argspec(arg.__new__)
return ArgSpec(argspec.args[1:], *argspec[1:])
else:
argspec = _get_argspec(arg.__init__)
return ArgSpec(argspec.args[1:], *argspec[1:])
# Old style class. Should only run in Python 2. types.ClassType doesn't
# exist in Python 3.
elif isinstance(arg, getattr(types, 'ClassType', type)):
argspec = _get_argspec(arg.__init__)
return ArgSpec(argspec.args[1:], *argspec[1:])
elif _recurse and hasattr(arg, '__call__'):
return _get_argspec(arg.__call__, _recurse=False)
elif callable(arg):
# Unknown, probably a built-in method.
return ArgSpec((), "args", "kwargs", None)
raise TypeError(
"_get_argspec: unexpected %s" % (type(arg).__name__,))
def _requires_parens_as_function(function_name):
"""
Returns whether the given string of a callable would require parentheses
around it to call it.
>>> _requires_parens_as_function("foo.bar[4]")
False
>>> _requires_parens_as_function("foo+bar")
True
>>> _requires_parens_as_function("(foo+bar)()")
False
>>> _requires_parens_as_function("(foo+bar)")
False
>>> _requires_parens_as_function("(foo)+(bar)")
True
:type function_name:
``str``
:rtype:
``bool``
"""
# TODO: this might be obsolete if we use unparse instead of keeping original
# user formatting (or alternatively, unparse should use something like this).
function_name = PythonBlock(function_name, flags=FLAGS)
node = function_name.expression_ast_node
if not node:
# Couldn't parse? Just assume we do need parens for now. Or should
# we raise an exception here?
return True
body = node.body
# Is it something that doesn't need parentheses?
if isinstance(body, (ast.Name, ast.Attribute, ast.Call, ast.Subscript)):
return False
# Does it already have parentheses?
n = str(function_name)
if n.startswith("(") and n.endswith(")"):
# It has parentheses, superficially. Make sure it's not something
# like "(foo)+(bar)".
flags = int(FLAGS) | ast.PyCF_ONLY_AST
try:
tnode = compile(n[1:-1], "<unknown>", "eval", flags)
except SyntaxError:
return True
if ast.dump(tnode) == ast.dump(node):
return False
else:
return True
return True
def _format_call_spec(function_name, argspec):
callspec = inspect.formatargspec(*argspec)
if _requires_parens_as_function(function_name):
return "(%s)%s" % (function_name, callspec)
else:
return "%s%s" % (function_name, callspec)
# TODO: move to util module
try:
from shlex import quote as shquote # python3.3+
except ImportError:
# Backport of shlex.quote from python3.3
_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
def shquote(s):
"""Return a shell-escaped version of the string *s*."""
if not s:
return "''"
if _find_unsafe(s) is None:
return s
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
def _build_function_usage_string(function_name, argspec, prefix):
usage = []
# TODO: colorize
usage.append("Python signature:")
usage.append(" >"+">> " + _format_call_spec(function_name, argspec))
usage.append("")
usage.append("Command-line signature:")
if not argspec.args and argspec.varargs and argspec.keywords:
# We have no information about the arguments. It's probably a
# built-in where getargspec failed.
usage.append(" $ %s%s ...\n" % (prefix, function_name))
return "\n".join(usage)
defaults = argspec.defaults or ()
first_with_default = len(argspec.args) - len(defaults)
# Show first alternative of command-line syntax.
syntax1 = " $ %s%s" % (prefix, shquote(function_name),)
for i, arg in enumerate(argspec.args):
if i >= first_with_default:
syntax1 += " [%s" % (arg,)
else:
syntax1 += " %s" % (arg,)
if argspec.varargs:
syntax1 += " %s..." % argspec.varargs
syntax1 += "]" * len(defaults)
if argspec.keywords:
syntax1 += " [--...]"
usage.append(syntax1)
# usage.append("or:")
syntax2 = " $ %s%s" % (prefix, shquote(function_name),)
for i, arg in enumerate(argspec.args):
if i >= first_with_default:
syntax2 += " [--%s=...]" % (arg,)
else:
syntax2 += " --%s=..." % (arg,)
if argspec.varargs:
syntax2 += " %s..." % argspec.varargs
if argspec.keywords:
syntax2 += " [--...]"
usage.append(syntax2)
usage.append("")
return "\n".join(usage)
class ParseError(Exception):
pass
class _ParseInterruptedWantHelp(Exception):
pass
class _ParseInterruptedWantSource(Exception):
pass
class UserExpr(object):
"""
An expression from user input, and its evaluated value.
The expression can come from a string literal or other raw value, or a
string that is evaluated as an expression, or heuristically chosen.
>>> ns = _Namespace()
Heuristic auto-evaluation::
>>> UserExpr('5+2', ns, "auto").value
7
>>> UserExpr('5j+2', ns, "auto").value
(2+5j)
>>> UserExpr('base64.b64decode("SGFsbG93ZWVu")', ns, "auto").value
[PYFLYBY] import base64
b'Halloween'
Returning an unparsable argument as a string::
>>> UserExpr('Victory Loop', ns, "auto").value
'Victory Loop'
Returning an undefined (and not auto-importable) argument as a string::
>>> UserExpr('Willowbrook29817621+5', ns, "auto").value
'Willowbrook29817621+5'
Explicit literal string::
>>> UserExpr("2+3", ns, "raw_value").value
'2+3'
>>> UserExpr("'2+3'", ns, "raw_value").value
"'2+3'"
Other raw values::
>>> UserExpr(sys.exit, ns, "raw_value").value
<built-in function exit>
"""
def __init__(self, arg, namespace, arg_mode, source=None):
"""
Construct a new UserExpr.
:type arg:
``str`` if ``arg_mode`` is "eval" or "auto"; anything if ``arg_mode``
is "raw_value"
:param arg:
Input user argument.
:type namespace:
`_Namespace`
:type arg_mode:
``str``
:param arg_mode:
If ``"raw_value"``, then return ``arg`` unchanged. If ``"eval"``, then
always evaluate ``arg``. If ``"auto"``, then heuristically evaluate
if appropriate.
"""
if arg_mode == "string":
arg_mode = "raw_value"
self._namespace = namespace
self._original_arg_mode = arg_mode
self._original_arg = arg
if arg_mode == "raw_value":
# self.inferred_arg_mode = "raw_value"
# self.original_source = None
if source is None:
source = PythonBlock(repr(self._original_arg))
else:
source = PythonBlock(source)
self.source = source
self.value = self._original_arg
elif arg_mode == "eval":
if source is not None:
raise ValueError(
"UserExpr(): source argument not allowed for eval")
# self.inferred_arg_mode = "eval"
self._original_arg_as_source = PythonBlock(arg, flags=FLAGS)
# self.original_source = self._original_arg_as_source
elif arg_mode == "auto":
if source is not None:
raise ValueError(
"UserExpr(): source argument not allowed for auto")
if not isinstance(arg, six.string_types):
raise ValueError(
"UserExpr(): arg must be a string if arg_mode='auto'")
self._original_arg_as_source = PythonBlock(arg, flags=FLAGS)
else:
raise ValueError("UserExpr(): bad arg_mode=%r" % (arg_mode,))
def _infer_and_evaluate(self):
if self._original_arg_mode == "raw_value":
pass
elif self._original_arg_mode == "eval":
block = self._original_arg_as_source
if not (str(block).strip()):
raise ValueError("empty input")
self.value = self._namespace.auto_eval(block)
self.source = self._original_arg_as_source #.pretty_print() # TODO
elif self._original_arg_mode == "auto":
block = self._original_arg_as_source
ERROR = object()
if not (str(block).strip()):
value = ERROR
elif not block.parsable_as_expression:
value = ERROR
else:
try:
value = self._namespace.auto_eval(block)
except UnimportableNameError:
value = ERROR
if value is ERROR:
# self.inferred_arg_mode = "raw_value"
self.value = self._original_arg
# self.original_source = None
self.source = PythonBlock(repr(self.value))
else:
# self.inferred_arg_mode = "eval"
self.value = value
# self.original_source = block
self.source = block #.pretty_print() # TODO
else:
raise AssertionError("internal error")
self._infer_and_evaluate = lambda: None
def __getattr__(self, k):
self._infer_and_evaluate()
return object.__getattribute__(self, k)
def __str__(self):
return str(self._original_arg)
def _parse_auto_apply_args(argspec, commandline_args, namespace, arg_mode="auto"):
"""
Parse command-line arguments heuristically. Arguments that can be
evaluated are evaluated; otherwise they are treated as strings.
:returns:
``args``, ``kwargs``
"""
# This is implemented manually instead of using optparse or argparse. We
# do so because neither supports dynamic keyword arguments well. Optparse
# doesn't support parsing known arguments only, and argparse doesn't
# support turning off interspersed positional arguments.
def make_expr(arg, arg_mode=arg_mode):
return UserExpr(arg, namespace, arg_mode)
# Create a map from argname to default value.
defaults = argspec.defaults or ()
argname2default = {}
for argname, default in zip(argspec.args[len(argspec.args)-len(defaults):],
defaults):
argname2default[argname] = make_expr(default, "raw_value")
# Create a map from prefix to arguments with that prefix. E.g. {"foo":
# ["foobar", "foobaz"]}
prefix2argname = {}
for argname in argspec.args:
for prefix in prefixes(argname):
prefix2argname.setdefault(prefix, []).append(argname)
# Enumerate over input arguments.
got_pos_args = []
got_keyword_args = {}
args = list(commandline_args)
while args:
arg = args.pop(0)
if arg in ["--?", "-?", "?"]:
raise _ParseInterruptedWantHelp
elif arg in ["--??", "-??", "??"]:
raise _ParseInterruptedWantSource
elif arg.startswith("-"):
if arg == "-":
# Read from stdin and stuff into next argument as a string.
data = sys.stdin.read()
got_pos_args.append(make_expr(data, "string"))
continue
elif arg == "--":
# Treat remaining arguments as strings.
got_pos_args.extend([make_expr(x, "string") for x in args])
del args[:]
continue
elif arg.startswith("--"):
argname = arg[2:]
else:
argname = arg[1:]
argname, equalsign, value = argname.partition("=")
argname = argname.replace("-", "_")
if not is_identifier(argname):
raise ParseError("Invalid option name %s" % (argname,))
matched_argnames = prefix2argname.get(argname, [])
if len(matched_argnames) == 1:
argname, = matched_argnames
elif len(matched_argnames) == 0:
if equalsign == "":
if argname in ["help", "h"]:
raise _ParseInterruptedWantHelp
if argname in ["source"]:
raise _ParseInterruptedWantSource
if not argspec.keywords:
raise ParseError("Unknown option name %s" % (argname,))
elif len(matched_argnames) > 1:
raise ParseError(
"Ambiguous %s: could mean one of: %s"
% (argname,
", ".join("--%s"%s for s in matched_argnames)))
else:
raise AssertionError
if not value:
try:
value = args.pop(0)
except IndexError:
raise ParseError("Missing argument to %s" % (arg,))
if value.startswith("--"):
raise ParseError(
"Missing argument to %s. "
"If you really want to use %r as the argument to %s, "
"then use %s=%s."
% (arg, value, arg, arg, value))
got_keyword_args[argname] = make_expr(value)
else:
got_pos_args.append(make_expr(arg))
parsed_args = []
parsed_kwargs = {}
for i, argname in enumerate(argspec.args):
if i < len(got_pos_args):
if argname in got_keyword_args:
raise ParseError(
"%s specified both as positional argument (%s) "
"and keyword argument (%s)"
% (argname, got_pos_args[i], got_keyword_args[argname]))
expr = got_pos_args[i]
else:
try:
expr = got_keyword_args.pop(argname)
except KeyError:
try:
expr = argname2default[argname]
except KeyError:
raise ParseError(
"missing required argument %s" % (argname,))
try:
value = expr.value
except Exception as e:
raise ParseError(
"Error parsing value for --%s=%s: %s: %s"
% (argname, expr, type(e).__name__, e))
parsed_args.append(value)
if len(got_pos_args) > len(argspec.args):
if argspec.varargs:
for expr in got_pos_args[len(argspec.args):]:
try:
value = expr.value
except Exception as e:
raise ParseError(
"Error parsing value for *%s: %s: %s: %s"
% (argspec.varargs, expr, type(e).__name__, e))
parsed_args.append(value)
else:
max_nargs = len(argspec.args)
if argspec.defaults:
expected = "%d-%d" % (max_nargs-len(argspec.defaults),max_nargs)
else:
expected = "%d" % (max_nargs,)
raise ParseError(
"Too many positional arguments. "
"Expected %s positional argument(s): %s. Got %d args: %s"
% (expected, ", ".join(argspec.args),
len(got_pos_args), " ".join(map(str, got_pos_args))))
for argname, expr in sorted(got_keyword_args.items()):
try:
parsed_kwargs[argname] = expr.value
except Exception as e:
raise ParseError(
"Error parsing value for --%s=%s: %s: %s"
% (argname, expr, type(e).__name__, e))
return parsed_args, parsed_kwargs
def _format_call(function_name, argspec, args, kwargs):
# TODO: print original unparsed arg strings
defaults = argspec.defaults or ()
first_with_default = len(argspec.args) - len(defaults)
argparts = []
for i in range(max(len(args), len(argspec.args))):
if i >= first_with_default and len(args) <= len(argspec.args):
argparts.append("%s=%r" % (argspec.args[i], args[i]))
else:
argparts.append(repr(args[i]))
for k, v in sorted(kwargs.items()):
argparts.append("%s=%r" % (k, v))
if _requires_parens_as_function(function_name):
function_name = "(%s)" % (function_name,)
r = "%s(%s)" % (function_name, ", ".join(argparts))
return r
class UnimportableNameError(NameError):
pass
class NotAFunctionError(Exception):
pass
def _get_help(expr, verbosity=1):
"""
Construct a help string.
:type expr:
`UserExpr`
:param expr:
Object to generate help for.
:rtype:
``str``
"""
# TODO: colorize headers
result = ""
obj = expr.value
name = str(expr.source)
if callable(obj):
argspec = _get_argspec(obj)
prefix = os.path.basename(sys.orig_argv[0]) + " "
result += _build_function_usage_string(name, argspec, prefix)
if verbosity == 0:
include_filename = False
include_doc = False
include_source = False
elif verbosity == 1:
include_filename = True
include_doc = True
include_source = False
elif verbosity == 2:
include_filename = True
include_doc = False
include_source = True
else:
raise ValueError("invalid verbosity=%r" % (verbosity,))
try:
version = obj.__version__
except Exception:
pass
else:
result += "\nVersion:\n %s\n" % (version,)
if include_filename:
try:
filename = inspect.getfile(obj)
except Exception:
pass
else:
result += "\nFilename:\n %s\n" % (filename,)
if include_source:
try:
source = inspect.getsource(obj)
except Exception:
source = ""
if source:
# TODO: colorize source
result += "\nSource:\n%s\n" % (indent(source, " "))
else:
source = "(Not available)"
include_doc = True
if include_doc:
doc = (inspect.getdoc(obj) or "").strip() or "(No docstring)"
result += "\nDocstring:\n%s" % (indent(doc, " "))
return result
_enable_postmortem_debugger = None
def _handle_user_exception(exc_info=None):
"""
Handle an exception in user code.
"""
# TODO: Make tracebacks show user code being executed. IPython does that
# by stuffing linecache.cache, and also advising linecache.checkcache. We
# can either advise it ourselves also, or re-use IPython.core.compilerop.
# Probably better to advise ourselves. Add "<pyflyby-input-md5[:12]>" to
# linecache. Perhaps do it in a context manager that removes from
# linecache when done. Advise checkcache to protect any "<pyflyby-*>".
# Do it for all code compiled from here, including args, debug_statement,
# etc.
if exc_info is None:
exc_info = sys.exc_info()
if exc_info[2].tb_next:
exc_info = (exc_info[0], exc_info[1],
exc_info[2].tb_next) # skip this traceback
# If ``_enable_postmortem_debugger`` is enabled, then debug the exception.
# By default, this is enabled run running in a tty.
# We check isatty(1) here because we want 'py ... | cat' to never go into
# the debugger. Note that debugger() also checks whether /dev/tty is
# usable (and if not, waits for attach).
if _enable_postmortem_debugger:
# *** Run debugger. ***
debugger(exc_info)
# TODO: consider using print_verbose_tb(*exc_info)
print("Traceback (most recent call last):", file=sys.stderr)
import traceback
traceback.print_tb(exc_info[2])
print("%s: %s" % (exc_info[0].__name__, exc_info[1]), file=sys.stderr)
raise SystemExit(1)
def auto_apply(function, commandline_args, namespace, arg_mode=None,
debug=False):
"""
Call ``function`` on command-line arguments. Arguments can be positional
or keyword arguments like "--foo=bar". Arguments are by default
heuristically evaluated.
:type function:
``UserExpr``
:param function:
Function to apply.
:type commandline_args:
``list`` of ``str``
:param commandline_args:
Arguments to ``function`` as strings.
:param arg_mode:
How to interpret ``commandline_args``. If ``"string"``, then treat them
as literal strings. If ``"eval"``, then evaluate all arguments as
expressions. If ``"auto"`` (the default), then heuristically decide
whether to treat as expressions or strings.
"""
if not isinstance(function, UserExpr):
raise TypeError
if not callable(function.value):
raise NotAFunctionError("Not a function", function.value)
arg_mode = _interpret_arg_mode(arg_mode, default="auto")
# Parse command-line arguments.
argspec = _get_argspec(function.value)
try:
args, kwargs = _parse_auto_apply_args(argspec, commandline_args,
namespace, arg_mode=arg_mode)
except _ParseInterruptedWantHelp:
usage = _get_help(function, verbosity=1)
print(usage)
raise SystemExit(0)
except _ParseInterruptedWantSource:
usage = _get_help(function, verbosity=2)
print(usage)
raise SystemExit(0)
except ParseError as e:
# Failed parsing command-line arguments. Print usage.
logger.error(e)
usage = _get_help(function, verbosity=(1 if logger.info_enabled else 0))
sys.stderr.write("\n" + usage)
raise SystemExit(1)
# Log what we're doing.
logger.info("%s", _format_call(function.source, argspec, args, kwargs))
# *** Call the function. ***
f = function.value
try:
if debug:
result = debug_statement("f(*args, **kwargs)")
else:
result = f(*args, **kwargs)
return result
except SystemExit:
raise
# TODO: handle "quit" by user here specially instead of returning None.
# May need to reimplement pdb.runeval() so we can catch BdbQuit.
except:
# Handle exception in user code.
_handle_user_exception()
@total_ordering
class LoggedList(object):
"""
List that logs which members have not yet been accessed (nor removed).
"""
_ACCESSED = object()
def __init__(self, items):
self._items = list(items)
self._unaccessed = list(self._items)
def append(self, x):
self._unaccessed.append(self._ACCESSED)
self._items.append(x)
def count(self):
return self._items.count()
def extend(self, new_items):
new_items = list(new_items)
self._unaccessed.extend([self._ACCESSED] * len(new_items))
self._items.extend(new_items)
def index(self, x, *start_stop):
index = self._items.index(x, *start_stop) # may raise ValueError
self._unaccessed[index] = self._ACCESSED
return index
def insert(self, index, x):
self._unaccessed.insert(index, self._ACCESSED)
self._items.insert(index, x)
def pop(self, index):
self._unaccessed.pop(index)
return self._items.pop(index)
def remove(self, x):
index = self._items.index(x)
self.pop(index)
def reverse(self):
self._items.reverse()
self._unaccessed.reverse()
def sort(self):
indexes = range(len(self._items))
indexes.sort(key=self._items.__getitem__) # argsort
self._items = [self._items[i] for i in indexes]
self._unaccessed = [self._unaccessed[i] for i in indexes]
def __add__(self, other):
return self._items + other
def __contains__(self, x):
try:
self.index(x)
return True
except ValueError:
return False
def __delitem__(self, x):
del self._items[x]
del self._unaccessed[x]
def __eq__(self, other):
if not isinstance(other, LoggedList):
return NotImplemented
return self._items == other._items
def __ne__(self, other):
return not (self == other)
# The rest are defined by total_ordering
def __lt__(self, other):
if not isinstance(other, LoggedList):
return NotImplemented
return self._items < other._items
def __cmp__(self, x):
return cmp(self._items, x)
def __getitem__(self, idx):
result = self._items[idx]
if isinstance(idx, slice):
self._unaccessed[idx] = [self._ACCESSED]*len(result)
else:
self._unaccessed[idx] = self._ACCESSED
return result
def __hash__(self):
raise TypeError("unhashable type: 'LoggedList'")
def __iadd__(self, x):
self.extend(x)
def __imul__(self, n):
self._items *= n
self._unaccessed *= n
def __iter__(self):
# Todo: detect mutation while iterating.
for i, x in enumerate(self._items):
self._unaccessed[i] = self._ACCESSED
yield x
def __len__(self):
return len(self._items)
def __mul__(self, n):
return self._items * n
def __reduce__(self):
return
def __repr__(self):
self._unaccessed[:] = [self._ACCESSED]*len(self._unaccessed)
return repr(self._items)
def __reversed__(self):
# Todo: detect mutation while iterating.
for i in reversed(range(len(self._items))):
self._unaccessed[i] = self._ACCESSED
yield self._items[i]
def __rmul__(self, n):
return self._items * n
def __setitem__(self, idx, value):
self._items[idx] = value
if isinstance(idx, slice):
self._unaccessed[idx] = [self._ACCESSED]*len(value)
else:
self._unaccessed[idx] = value
def __str__(self):
self._unaccessed[:] = [self._ACCESSED]*len(self._unaccessed)
return str(self._items)
@property
def unaccessed(self):
return [x for x in self._unaccessed if x is not self._ACCESSED]
@contextmanager
def SysArgvCtx(*args):
"""
Context manager that:
* Temporarily sets sys.argv = args.
* At end of context, complains if any args were never accessed.
"""
# There should always be at least one arg, since the first one is
# the program name.
if not args:
raise ValueError("Missing args")
nargs = len(args) - 1
# Create a list proxy that will log accesses.
argv = LoggedList(args)
# Don't consider first argument to be interesting.
argv[0]
orig_argv = list(sys.argv)
try:
sys.argv = argv
# Run context code.
yield
# Complain if there are unaccessed arguments.
unaccessed = argv.unaccessed
if not unaccessed:
pass
else:
if nargs == 1:
msg = ("You specified a command-line argument, but your code didn't use it: %s"
% (unaccessed[0],))
elif len(unaccessed) == nargs:
msg = ("You specified %d command-line arguments, but your code didn't use them: %s"
% (len(unaccessed), " ".join(unaccessed)))
else:
msg = ("You specified %d command-line arguments, but your code didn't use %d of them: %s"
% (nargs, len(unaccessed), " ".join(unaccessed)))
msg2 = "\nIf this is intentional, access 'sys.argv[:]' somewhere in your code."
logger.error(msg + msg2)
raise SystemExit(1)
finally:
sys.argv = orig_argv
def _as_filename_if_seems_like_filename(arg):
"""
If ``arg`` seems like a filename, then return it as one.
>>> bool(_as_filename_if_seems_like_filename("foo.py"))
True
>>> bool(_as_filename_if_seems_like_filename("%foo.py"))
False
>>> bool(_as_filename_if_seems_like_filename("foo(bar)"))
False
>>> bool(_as_filename_if_seems_like_filename("/foo/bar/baz.quux-660470"))
True
>>> bool(_as_filename_if_seems_like_filename("../foo/bar-24084866"))
True
:type arg:
``str``
:rtype:
``Filename``
"""
try:
filename = Filename(arg)
except UnsafeFilenameError:
# If the filename isn't a "safe" filename, then don't treat it as one,
# and don't even check whether it exists. This means that for an
# argument like "foo(bar)" or "lambda x:x*y" we won't even check
# existence. This is both a performance optimization and a safety
# valve to avoid unsafe filenames being created to intercept expressions.
return None
# If the argument "looks" like a filename and is unlikely to be a python
# expression, then assume it is a filename. We assume so regardless of
# whether the file actually exists; if it turns out to not exist, we'll
# complain later.
if arg.startswith("/") or arg.startswith("./") or arg.startswith("../"):
return filename
if filename.ext == ".py":
# TODO: .pyc, .pyo
return which(arg) or filename
# Even if it doesn't obviously look like a filename, but it does exist as
# a filename, then use it as one.
if filename.exists:
return filename
# If it's a plain name and we can find an executable on $PATH, then use
# that.
if re.match("^[a-zA-Z0-9_-]+$", arg):
filename = which(arg)
if not filename:
return None
if not _has_python_shebang(filename):
logger.debug("Found %s but it doesn't seem like a python script",
filename)
return None
return filename
return None
def _has_python_shebang(filename):
"""
Return whether the first line contains #!...python...
Used for confirming that an executable script found via which() is
actually supposed to be a python script.
Note that this test is only needed for scripts found via which(), since
otherwise the shebang is not necessary.
"""
filename = Filename(filename)
with open(str(filename), 'rb') as f:
line = f.readline(1024)
return line.startswith(b"#!") and b"python" in line
def _interpret_arg_mode(arg, default="auto"):
"""
>>> _interpret_arg_mode("Str")
'string'
"""
if arg is None:
arg = default
if arg == "auto" or arg == "eval" or arg == "string":
return arg # optimization for interned strings
rarg = str(arg).strip().lower()
if rarg in ["eval", "evaluate", "exprs", "expr", "expressions", "expression", "e"]:
return "eval"
elif rarg in ["strings", "string", "str", "strs", "literal", "literals", "s"]:
return "string"
elif rarg in ["auto", "automatic", "a"]:
return "auto"
elif rarg == "error":
# Intentionally not documented to user
return "error"
else:
raise ValueError(
"Invalid arg_mode=%r; expected one of eval/string/auto"
% (arg,))
def _interpret_output_mode(arg, default="interactive"):
"""
>>> _interpret_output_mode('Repr_If_Not_None')
'repr-if-not-none'
"""
if arg is None:
arg = default
rarg = str(arg).strip().lower().replace("-", "").replace("_", "")
if rarg in ["none", "no", "n", "silent"]:
return "silent"
elif rarg in ["interactive", "i"]:
return "interactive"
elif rarg in ["print", "p", "string", "str"]:
return "str"
elif rarg in ["repr", "r"]:
return "repr"
elif rarg in ["pprint", "pp"]:
return "pprint"
elif rarg in ["reprifnotnone", "reprunlessnone", "rn"]:
return "repr-if-not-none"
elif rarg in ["pprintifnotnone", "pprintunlessnone", "ppn"]:
return "pprint-if-not-none"
elif rarg in ["systemexit", "exit", "raise"]:
return "exit"
else:
raise ValueError(
"Invalid output=%r; expected one of "
"silent/interactive/str/repr/pprint/repr-if-not-none/pprint-if-not-none/exit"
% (arg,))
def print_result(result, output_mode):
output_mode = _interpret_output_mode(output_mode)
if output_mode == "silent":
return
if output_mode == "interactive":
# TODO: support IPython output stuff (text/plain)
try:
idisp = result.__interactive_display__
except Exception:
output_mode = "pprint-if-not-none"
else:
result = idisp()
output_mode = "print-if-not-none"
# Fall through.
if output_mode == "str":
print(str(result))
elif output_mode == "repr":
print(repr(result))
elif output_mode == "pprint":
import pprint
pprint.pprint(result) # or equivalently, print pprint.pformat(result)
elif output_mode == "repr-if-not-none":
if result is not None:
print(repr(result))
elif output_mode == "print-if-not-none":
if result is not None:
print(result)
elif output_mode == "pprint-if-not-none":
if result is not None:
import pprint
pprint.pprint(result)
elif output_mode == "exit":
# TODO: only raise at end after pre_exit
raise SystemExit(result)
else:
raise AssertionError("unexpected output_mode=%r" % (output_mode,))
class _Namespace(object):
def __init__(self):
self.globals = {"__name__": "__main__",
"__builtin__": builtins,
"__builtins__": builtins}
self.autoimported = {}
def auto_import(self, arg):
return auto_import(arg, [self.globals], autoimported=self.autoimported)
def auto_eval(self, block, mode=None, info=False, auto_import=True,
debug=False):
"""
Evaluate ``block`` with auto-importing.
"""
# Equivalent to::
# auto_eval(arg, mode=mode, flags=FLAGS, globals=self.globals)
# but better logging and error raising.
block = PythonBlock(block, flags=FLAGS, auto_flags=True)
if auto_import and not self.auto_import(block):
missing = find_missing_imports(block, [self.globals])
mstr = ", ".join(repr(str(x)) for x in missing)
if len(missing) == 1:
msg = "name %s is not defined and not importable" % mstr
elif len(missing) > 1:
msg = "names %s are not defined and not importable" % mstr
else:
raise AssertionError
raise UnimportableNameError(msg)
if info:
logger.info(block)
try:
# TODO: enter text into linecache
if debug:
return debug_statement(block, self.globals)
else:
code = block.compile(mode=mode)
return eval(code, self.globals, self.globals)
except SystemExit:
raise
except:
_handle_user_exception()
class _PyMain(object):
def __init__(self, args):
self.main_args = args
self.namespace = _Namespace()
self.result = None
self.ipython_app = None
def exec_stdin(self, cmd_args):
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
output_mode = _interpret_output_mode(self.output_mode, default="silent")
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
for a in cmd_args]
with SysArgvCtx(*cmd_args):
result = self.namespace.auto_eval(Filename.STDIN, debug=self.debug)
print_result(result, output_mode)
self.result = result
def eval(self, cmd, cmd_args):
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
output_mode = _interpret_output_mode(self.output_mode)
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
for a in cmd_args]
with SysArgvCtx("-c", *cmd_args):
cmd = PythonBlock(cmd)
result = self.namespace.auto_eval(cmd, info=True, debug=self.debug)
# TODO: make auto_eval() plow ahead even if there are unimportable
# names, after warning
print_result(result, output_mode)
self.result = result
def execfile(self, filename_arg, cmd_args):
# TODO: pass filename to import db target_filename; unit test.
# TODO: set __file__
# TODO: support compiled (.pyc/.pyo) files
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
output_mode = _interpret_output_mode(self.output_mode)
cmd_args = [UserExpr(a, self.namespace, arg_mode).value
for a in cmd_args]
additional_msg = ""
if isinstance(filename_arg, Filename):
filename = filename_arg
elif filename_arg == "-":
filename = Filename.STDIN
elif "/" in filename_arg:
filename = Filename(filename_arg)
else:
filename = which(filename_arg)
if not filename:
filename = Filename(filename_arg)
additional_msg = (" (and didn't find %s on $PATH)"
% (filename_arg,))
elif not _has_python_shebang(filename):
additional_msg = (" (found %s but it doesn't look "
"like python source"
% (filename,))
filename = Filename(filename_arg)
if not filename.exists:
raise Exception("No such file: %s%s" % (filename, additional_msg))
with SysArgvCtx(str(filename), *cmd_args):
sys.path.insert(0, str(filename.dir))
result = self.namespace.auto_eval(filename, debug=self.debug)
print_result(result, output_mode)
self.result = result
def apply(self, function, cmd_args):
arg_mode = _interpret_arg_mode(self.arg_mode, default="auto")
output_mode = _interpret_output_mode(self.output_mode)
# Todo: what should we set argv to?
result = auto_apply(function, cmd_args, self.namespace, arg_mode,
debug=self.debug)
print_result(result, output_mode)
self.result = result
def _seems_like_runnable_module(self, arg):
if not is_identifier(arg, dotted=True):
# It's not a single (dotted) identifier.
return False
if not find_missing_imports(arg, [{}]):
# It's off of a builtin, e.g. "str.upper"
return False
m = ModuleHandle(arg)
if m.parent:
# Auto-import the parent, which is necessary in order to get the
# filename of the module. ``ModuleHandle.filename`` does this
# automatically, but we do it explicitly here so that we log
# the import of the parent module.
if not self.namespace.auto_import(str(m.parent.name)):
return False
if not m.filename:
logger.debug("Module %s doesn't have a source filename", m)
return False
# TODO: check that the source accesses __main__ (ast traversal?)
return True
def heuristic_cmd(self, cmd, cmd_args, function_name=None):
output_mode = _interpret_output_mode(self.output_mode)
# If the "command" is just a module name, then call run_module. Make
# sure we check that it's not a builtin.
if self._seems_like_runnable_module(str(cmd)):
self.heuristic_run_module(str(cmd), cmd_args)
return
# FIXME TODO heed arg_mode for non-apply case. This is tricky to
# implement; will require assigning some proxy class to sys.argv
# that's more sophisticated than just logging.
with SysArgvCtx("-c", *cmd_args):
# Log the expression before we evaluate it, unless we're likely to
# log another substantially similar line. (We can only guess
# heuristically whether it's interesting enough to log it. And we
# can't know whether the result will be callable until we evaluate
# it.)
info = not re.match("^[a-zA-Z0-9_.]+$", function_name)
result = self.namespace.auto_eval(cmd, info=info, debug=self.debug)
if callable(result):
function = UserExpr(
result, self.namespace, "raw_value", function_name)
result = auto_apply(function, cmd_args, self.namespace,
self.arg_mode, debug=self.debug)
print_result(result, output_mode)
self.result = result
sys.argv[:] # mark as accessed
else:
if not info:
# We guessed wrong earlier and didn't log yet; log now.
logger.info(cmd)
print_result(result, output_mode)
self.result = result
unaccessed = sys.argv.unaccessed
if unaccessed:
logger.error(
"%s is not callable. Unexpected argument(s): %s",
result, " ".join(unaccessed))
sys.argv[:] # don't complain again
def run_module(self, module, args):
arg_mode = _interpret_arg_mode(self.arg_mode, default="string")
if arg_mode != "string":
raise NotImplementedError(
"run_module only supports string arguments")
module = ModuleHandle(module)
logger.info("python -m %s", ' '.join([str(module.name)] + args))
# Imitate 'python -m'.
# TODO: include only the traceback below runpy.run_module
# os.execvp(sys.executable, [sys.argv[0], "-m", modname] + args)
sys.argv = [str(module.filename)] + args
import runpy
if self.debug:
# TODO: break closer to user code
debugger()
try:
runpy.run_module(str(module.name), run_name="__main__")
except SystemExit:
raise
except:
_handle_user_exception()
def heuristic_run_module(self, module, args):
module = ModuleHandle(module)
# If the user ran 'py numpy --version', then print the numpy
# version, i.e. same as 'py --version numpy'. This has the
# downside of shadowing a possible "--version" feature
# implemented by the module itself. However, this is probably
# not a big deal, because (1) a full-featured program that
# supports --version would normally have a driver script and
# not rely on 'python -m foo'; (2) it would probably do
# something similar anyway; (3) the user can do 'py -m foo
# --version' if necessary.
if len(args)==1 and args[0] in ["--version", "-version"]:
self.print_version(module)
return
if len(args)==1 and args[0] in ["--help", "-help", "--h", "-h",
"--?", "-?", "?"]:
expr = UserExpr(module.module, None, "raw_value",
source=str(module.name))
usage = _get_help(expr, 1)
print(usage)
return
if len(args)==1 and args[0] in ["--source", "-source",
"--??", "-??", "??"]:
expr = UserExpr(module.module, None, "raw_value",
source=str(module.name))
usage = _get_help(expr, 2)
print(usage)
return
# TODO: check if module checks __main__
self.run_module(module, args)
def print_version(self, arg):
if not arg:
print_version_and_exit()
return
if isinstance(arg, (ModuleHandle, types.ModuleType)):
module = ModuleHandle(arg).module
else:
module = self.namespace.auto_eval(arg, mode="eval")
if not isinstance(module, types.ModuleType):
raise TypeError("print_version(): got a %s instead of a module"
% (type(module).__name__,))
try:
version = module.__version__
except AttributeError:
raise AttributeError(
"Module %s does not have a __version__ attribute"
% (module.__name__,))
print(version)
def print_help(self, objname, verbosity=1):
objname = objname and objname.strip()
if not objname:
print(__doc__)
return
expr = UserExpr(objname, self.namespace, "eval")
usage = _get_help(expr, verbosity)
print(usage)
def create_ipython_app(self):
"""
Create an IPython application and initialize it, but don't start it.
"""
assert self.ipython_app is None
self.ipython_app = get_ipython_terminal_app_with_autoimporter()
def start_ipython(self, args=[]):
user_ns = self.namespace.globals
start_ipython_with_autoimporter(args, _user_ns=user_ns,
app=self.ipython_app)
# Don't need to do another interactive session after this one
# (i.e. make 'py --interactive' the same as 'py').
self.interactive = False
def _parse_global_opts(self):
args = list(self.main_args)
self.debug = False
self.interactive = False
self.verbosity = 1
self.arg_mode = None
self.output_mode = None
postmortem = 'auto'
while args:
arg = args[0]
if arg in ["debug", "pdb", "ipdb", "dbg"]:
argname = "debug"
elif not arg.startswith("-"):
break
elif arg.startswith("--"):
argname = arg[2:]
else:
argname = arg[1:]
argname, equalsign, value = argname.partition("=")
def popvalue():
if equalsign:
return value
else:
try:
return args.pop(0)
except IndexError:
raise ValueError("expected argument to %s" % arg)
def novalue():
if equalsign:
raise ValueError("unexpected argument %s" % arg)
if argname in ["interactive", "i"]:
novalue()
self.interactive = True
del args[0]
# Create and initialize the IPython app now (but don't start
# it yet). We'll use it later. The reason to initialize it
# now is that the code that we're running might check if it's
# running in interactive mode based on whether an IPython app
# has been initialized. Some user code even initializes
# things differently at module import time based on this.
self.create_ipython_app()
elif argname in ["debug", "pdb", "ipdb", "dbg", "d"]:
novalue()
self.debug = True
del args[0]
if argname == "verbose":
novalue()
logger.set_level("DEBUG")
del args[0]
continue
if argname in ["quiet", "q"]:
novalue()
logger.set_level("ERROR")
del args[0]
continue
if argname in ["safe"]:
del args[0]
novalue()
self.arg_mode = _interpret_arg_mode("string")
# TODO: make this less hacky, something like
# self.import_db = ""
# TODO: also turn off which() behavior
os.environ["PYFLYBY_PATH"] = "EMPTY"
os.environ["PYFLYBY_KNOWN_IMPORTS_PATH"] = ""
os.environ["PYFLYBY_MANDATORY_IMPORTS_PATH"] = ""
continue
if argname in ["arguments", "argument", "args", "arg",
"arg_mode", "arg-mode", "argmode"]:
# Interpret --args=eval|string|auto.
# Note that if the user didn't specify --args, then we
# intentionally leave ``opts.arg_mode`` set to ``None`` for now,
# because the default varies per action.
del args[0]
self.arg_mode = _interpret_arg_mode(popvalue())
continue
if argname in ["output", "output_mode", "output-mode",
"out", "outmode", "out_mode", "out-mode", "o"]:
del args[0]
self.output_mode = _interpret_output_mode(popvalue())
continue
if argname in ["print", "pprint", "silent", "repr"]:
del args[0]
novalue()
self.output_mode = _interpret_output_mode(argname)
continue
if argname in ["postmortem"]:
del args[0]
v = (value or "").lower().strip()
if v in ["yes", "y", "always", "true", "t", "1", "enable", ""]:
postmortem = True
elif v in ["no", "n", "never", "false", "f", "0", "disable"]:
postmortem = False
elif v in ["auto", "automatic", "default", "if-tty"]:
postmortem = "auto"
else:
raise ValueError(
"unexpected %s=%s. "
"Try --postmortem=yes or --postmortem=no."
% (argname, value))
continue
if argname in ["no-postmortem", "np"]:
del args[0]
novalue()
postmortem = False
continue
break
self.args = args
if postmortem == "auto":
postmortem = os.isatty(1)
global _enable_postmortem_debugger
_enable_postmortem_debugger = postmortem
def _enable_debug_tools(self):
# Enable a bunch of debugging tools.
enable_faulthandler()
enable_signal_handler_debugger()
enable_sigterm_handler()
add_debug_functions_to_builtins()
def run(self):
# Parse global options.
sys.orig_argv = list(sys.argv)
self._parse_global_opts()
self._enable_debug_tools()
self._run_action()
self._pre_exit()
def _run_action(self):
args = self.args
if not args or args[0] == "-":
if os.isatty(0):
# The user specified no arguments (or only a "-") and stdin is a
# tty. Run ipython with autoimporter enabled, i.e. equivalent to
# autoipython. Note that we directly start IPython in the same
# process, instead of using subprocess.call(['autoipython']),
# because the latter messes with Control-C handling.
# TODO: add 'py shell' and make this an alias.
# TODO: if IPython isn't installed, then do our own
# interactive REPL with code.InteractiveConsole, readline, and
# autoimporter.
self.start_ipython()
return
else:
# Emulate python args.
cmd_args = args or [""]
# Execute code from stdin, with auto importing.
self.exec_stdin(cmd_args)
return
# Consider --action=arg, --action arg, -action=arg, -action arg,
# %action arg.
arg0 = args.pop(0)
if not arg0.strip():
raise ValueError("got empty string as first argument")
if arg0.startswith("--"):
action, equalsign, cmdarg = arg0[2:].partition("=")
elif arg0.startswith("-"):
action, equalsign, cmdarg = arg0[1:].partition("=")
elif arg0.startswith("%"):
action, equalsign, cmdarg = arg0[1:], None, None
elif len(arg0) > 1 or arg0 == "?":
action, equalsign, cmdarg = arg0, None, None
else:
action, equalsign, cmdarg = None, None, None
def popcmdarg():
if equalsign:
return cmdarg
else:
try:
return args.pop(0)
except IndexError:
raise ValueError("expected argument to %s" % arg0)
def nocmdarg():
if equalsign:
raise ValueError("unexpected argument %s" % arg0)
if action in ["eval", "c", "e"]:
# Evaluate a command from the command-line, with auto importing.
# Supports expressions as well as statements.
# Note: Regular python supports "python -cfoo" as equivalent to
# "python -c foo". For now, we intentionally don't support that.
cmd = popcmdarg()
self.eval(cmd, args)
elif action in ["file", "execfile", "execf", "runfile", "run", "f",
"python"]:
# Execute a file named on the command-line, with auto importing.
cmd = popcmdarg()
self.execfile(cmd, args)
elif action in ["apply", "call"]:
# Call a function named on the command-line, with auto importing and
# auto evaluation of arguments.
function_name = popcmdarg()
function = UserExpr(function_name, self.namespace, "eval")
self.apply(function, args)
elif action in ["map"]:
# Call function on each argument.
# TODO: instead of making this a standalone mode, change this to a
# flag that can eval/apply/exec/etc. Set "_" to each argument.
# E.g. py --map 'print _' obj1 obj2
# py --map _.foo obj1 obj2
# py --map '_**2' 3 4 5
# when using heuristic mode, "lock in" the action mode on the
# first argument.
function_name = popcmdarg()
function = UserExpr(function_name, self.namespace, "eval")
if args and args[0] == '--':
for arg in args[1:]:
self.apply(function, ['--', arg])
else:
for arg in args:
self.apply(function, [arg])
elif action in ["xargs"]:
# TODO: read lines from stdin and map. default arg_mode=string
raise NotImplementedError("TODO: xargs")
elif action in ["module", "m", "runmodule", "run_module", "run-module"]:
# Exactly like `python -m'. Intentionally does NOT do auto
# importing within the module, because modules should not be
# sloppy; they should instead be tidied to have the correct
# imports.
modname = popcmdarg()
self.run_module(modname, args)
elif arg0.startswith("-m"):
# Support "py -mbase64" in addition to "py -m base64".
modname = arg0[2:]
self.run_module(modname, args)
elif action in ["attach"]:
pid = int(popcmdarg())
nocmdarg()
attach_debugger(pid)
elif action in ["stack", "stack_trace", "stacktrace", "backtrace", "bt"]:
pid = int(popcmdarg())
nocmdarg()
print("Stack trace for process %s:" % (pid,))
remote_print_stack(pid)
elif action in ["ipython", "ip"]:
# Start IPython.
self.start_ipython(args)
elif action in ["notebook", "nb"]:
# Start IPython notebook.
nocmdarg()
self.start_ipython(["notebook"] + args)
elif action in ["kernel"]:
# Start IPython kernel.
nocmdarg()
self.start_ipython(["kernel"] + args)
elif action in ["qtconsole", "qt"]:
# Start IPython qtconsole.
nocmdarg()
self.start_ipython(["qtconsole"] + args)
elif action in ["console"]:
# Start IPython console (with new kernel).
nocmdarg()
self.start_ipython(["console"] + args)
elif action in ["existing"]:
# Start IPython console (with existing kernel).
if equalsign:
args.insert(0, cmdarg)
self.start_ipython(["console", "--existing"] + args)
elif action in ["nbconvert"]:
# Start IPython nbconvert. (autoimporter is irrelevant.)
if equalsign:
args.insert(0, cmdarg)
start_ipython_with_autoimporter(["nbconvert"] + args)
elif action in ["timeit"]:
# TODO: make --timeit and --time flags which work with any mode
# and heuristic, instead of only eval.
# TODO: fallback if IPython isn't available. above todo probably
# requires not using IPython anyway.
nocmdarg()
run_ipython_line_magic("%timeit " + ' '.join(args))
elif action in ["time"]:
# TODO: make --timeit and --time flags which work with any mode
# and heuristic, instead of only eval.
# TODO: fallback if IPython isn't available. above todo probably
# requires not using IPython anyway.
nocmdarg()
run_ipython_line_magic("%time " + ' '.join(args))
elif action in ["version"]:
if equalsign:
args.insert(0, cmdarg)
self.print_version(args[0] if args else None)
elif action in ["help", "h", "?"]:
if equalsign:
args.insert(0, cmdarg)
self.print_help(args[0] if args else None, verbosity=1)
elif action in ["pinfo"]:
self.print_help(popcmdarg(), verbosity=1)
elif action in ["source", "pinfo2", "??"]:
self.print_help(popcmdarg(), verbosity=2)
elif arg0.startswith("-"):
# Unknown argument.
msg = "Unknown option %s" % (arg0,)
if arg0.startswith("-c"):
msg += "; do you mean -c %s?" % (arg0[2:])
syntax(msg, usage=usage)
elif arg0.startswith("??"):
# TODO: check number of args
self.print_help(arg0[2:], verbosity=2)
elif arg0.endswith("??"):
# TODO: check number of args
self.print_help(arg0[:-2], verbosity=2)
elif arg0.startswith("?"):
# TODO: check number of args
self.print_help(arg0[1:], verbosity=1)
elif arg0.endswith("?"):
# TODO: check number of args
self.print_help(arg0[:-1], verbosity=1)
elif arg0.startswith("%"):
run_ipython_line_magic(' '.join([arg0]+args))
# Heuristically choose the behavior automatically based on what the
# argument looks like.
else:
filename = _as_filename_if_seems_like_filename(arg0)
if filename:
# Implied --execfile.
self.execfile(filename, args)
return
if not args and arg0.isdigit():
if self.debug:
attach_debugger(int(arg0, 10))
return
else:
logger.error(
"Use py -d %s if you want to attach a debugger", arg0)
raise SystemExit(1)
# Implied --eval.
# When given multiple arguments, first see if the args can be
# concatenated and parsed as a single python program/expression.
# But don't try this if any arguments look like options, empty
# string or whitespace, etc.
# TODO: refactor
if (args and
self.arg_mode == None and
not any(re.match(r"\s*$|-[a-zA-Z-]", a) for a in args)):
cmd = PythonBlock(" ".join([arg0]+args),
flags=FLAGS, auto_flags=True)
if cmd.parsable and self.namespace.auto_import(cmd):
with SysArgvCtx("-c"):
output_mode = _interpret_output_mode(self.output_mode)
result = self.namespace.auto_eval(
cmd, info=True, auto_import=False)
print_result(result, output_mode)
self.result = result
return
# else fall through
# Heuristic based on first arg: try run_module, apply, or exec/eval.
cmd = PythonBlock(arg0, flags=FLAGS, auto_flags=True)
if not cmd.parsable:
logger.error(
"Could not interpret as filename or expression: %s",
arg0)
syntax(usage=usage)
self.heuristic_cmd(cmd, args, function_name=arg0)
def _pre_exit(self):
self._pre_exit_matplotlib_show()
self._pre_exit_interactive_shell()
def _pre_exit_matplotlib_show(self):
"""
If matplotlib.pyplot (pylab) is loaded, then call the show() function.
This will cause the program to block until all figures are closed.
Without this, a command like 'py plot(...)' would exit immediately.
"""
if self.interactive:
return
try:
pyplot = sys.modules["matplotlib.pyplot"]
except KeyError:
return
pyplot.show()
def _pre_exit_interactive_shell(self):
if self.interactive:
assert self.ipython_app is not None
self.namespace.globals["_"] = self.result
self.start_ipython()
def py_main(args=None):
if args is None:
args = sys.argv[1:]
_PyMain(args).run()