##
# .python.command - Python command emulation module.
##
"""
Create and Execute Python Commands
==================================
The purpose of this module is to simplify the creation of a Python command
interface. Normally, one would want to do this if there is a *common* need
for a certain Python environment that may be, at least, partially initialized
via command line options. A notable case would be a Python environment with a
database connection whose connection parameters came from the command line. That
is, Python + command line driven configuration.
The module also provides an extended interactive console that provides backslash
commands for editing and executing temporary files. Use ``python -m
pythoncommand`` to try it out.
Simple usage::
import sys
import os
import optparse
import pythoncommand as pycmd
op = optparse.OptionParser(
"%prog [options] [script] [script arguments]",
version = '1.0',
)
op.disable_interspersed_args()
# Basically, the standard -m and -c. (Some additional ones for fun)
op.add_options(pycmd.default_optparse_options)
co, ca = op.parse_args(args[1:])
# This initializes an execution instance which gathers all the information
# about the code to be ran when ``pyexe`` is called.
pyexe = pycmd.Execution(ca,
context = getattr(co, 'python_context', ()),
loader = getattr(co, 'python_main', None),
)
# And run it. Any exceptions will be printed via print_exception.
rv = pyexe()
sys.exit(rv)
"""
import os
import sys
import re
import code
import types
import optparse
import subprocess
import contextlib
from gettext import gettext as _
from traceback import print_exception
from pkgutil import get_loader as module_loader
class single_loader(object):
"""
used for "loading" string modules(think -c)
"""
def __init__(self, source):
self.source = source
def get_filename(self, fullpath):
if fullpath == self.source:
return ']'),
dest = 'python_context',
action = 'callback',
callback = append_context,
type = 'str'
)
module = optparse.make_option(
'-m',
help = _('Python module to run as script(__main__)'),
dest = 'python_main',
action = 'callback',
callback = set_python_main,
type = 'str'
)
module.python_loader = module_loader_descriptor
command = optparse.make_option(
'-c',
help = _('Python expression to run(__main__)'),
dest = 'python_main',
action = 'callback',
callback = set_python_main,
type = 'str'
)
command.python_loader = single_loader_descriptor
default_optparse_options = [
context, module, command,
]
class ExtendedConsole(code.InteractiveConsole):
"""
Console subclass providing some convenient backslash commands.
"""
def __init__(self, *args, **kw):
import tempfile
self.mktemp = tempfile.mktemp
import shlex
self.split = shlex.split
code.InteractiveConsole.__init__(self, *args, **kw)
self.bsc_map = {}
self.temp_files = {}
self.past_buffers = []
self.register_backslash(r'\?', self.showhelp, "Show this help message.")
self.register_backslash(r'\set', self.bs_set,
"Configure environment variables. \set without arguments to show all")
self.register_backslash(r'\E', self.bs_E,
"Edit a file or a temporary script.")
self.register_backslash(r'\i', self.bs_i,
"Execute a Python script within the interpreter's context.")
self.register_backslash(r'\e', self.bs_e,
"Edit and Execute the file directly in the context.")
self.register_backslash(r'\x', self.bs_x,
"Execute the Python command within this process.")
def interact(self, *args, **kw):
self.showhelp(None, None)
return super().interact(*args,**kw)
def showtraceback(self):
e, v, tb = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = e, v, tb
print_exception(e, v, tb.tb_next or tb)
def register_backslash(self, bscmd, meth, doc):
self.bsc_map[bscmd] = (meth, doc)
def execslash(self, line):
"""
If push() gets a line that starts with a backslash, execute
the command that the backslash sequence corresponds to.
"""
cmd = line.split(None, 1)
cmd.append('')
bsc = self.bsc_map.get(cmd[0])
if bsc is None:
self.write("ERROR: unknown backslash command: %s%s"%(cmd, os.linesep))
else:
return bsc[0](cmd[0], cmd[1])
def showhelp(self, cmd, arg):
i = list(self.bsc_map.items())
i.sort(key = lambda x: x[0])
helplines = os.linesep.join([
' %s%s%s' %(
x[0], ' ' * (8 - len(x[0])), x[1][1]
) for x in i
])
self.write("Backslash Commands:%s%s%s" %(
os.linesep*2, helplines, os.linesep*2
))
def bs_set(self, cmd, arg):
"""
Set a value in the interpreter's environment.
"""
if arg:
for x in self.split(arg):
if '=' in x:
k, v = x.split('=', 1)
os.environ[k] = v
self.write("%s=%s%s" %(k, v, os.linesep))
elif x:
self.write("%s=%s%s" %(x, os.environ.get(x, ''), os.linesep))
else:
for k,v in os.environ.items():
self.write("%s=%s%s" %(k, v, os.linesep))
def resolve_path(self, path, dont_create = False):
"""
Get the path of the given string; if the path is not
absolute and does not contain path separators, identify
it as a temporary file.
"""
if not os.path.isabs(path) and not os.path.sep in path:
# clean it up to avoid typos
path = path.strip().lower()
tmppath = self.temp_files.get(path)
if tmppath is None:
if dont_create is False:
tmppath = self.mktemp(
suffix = '.py',
prefix = '_console_%s_' %(path,)
)
self.temp_files[path] = tmppath
else:
return path
return tmppath
return path
def execfile(self, filepath):
src = open(filepath)
try:
try:
co = compile(src.read(), filepath, 'exec')
except SyntaxError:
co = None
print_exception(*sys.exc_info())
finally:
src.close()
if co is not None:
try:
exec(co, self.locals, self.locals)
except:
e, v, tb = sys.exc_info()
print_exception(e, v, tb.tb_next or tb)
def editfiles(self, filepaths):
sp = list(filepaths)
# ;)
sp.insert(0, os.environ.get('EDITOR', 'vi'))
return subprocess.call(sp)
def bs_i(self, cmd, arg):
'execute the files'
for x in self.split(arg) or ('',):
p = self.resolve_path(x, dont_create = True)
self.execfile(p)
def bs_E(self, cmd, arg):
'edit the files, but *only* edit them'
self.editfiles([self.resolve_path(x) for x in self.split(arg) or ('',)])
def bs_e(self, cmd, arg):
'edit *and* execute the files'
filepaths = [self.resolve_path(x) for x in self.split(arg) or ('',)]
self.editfiles(filepaths)
for x in filepaths:
self.execfile(x)
def bs_x(self, cmd, arg):
rv = -1
if len(cmd) > 1:
a = self.split(arg)
a.insert(0, '\\x')
try:
rv = command(argv = a)
except SystemExit as se:
rv = se.code
self.write("[Return Value: %d]%s" %(rv, os.linesep))
def push(self, line):
# Has to be a ps1 context.
if not self.buffer and line.startswith('\\'):
try:
self.execslash(line)
except:
# print the exception, but don't raise.
e, v, tb = sys.exc_info()
print_exception(e, v, tb.tb_next or tb)
else:
return code.InteractiveConsole.push(self, line)
@contextlib.contextmanager
def postmortem(funcpath):
if not funcpath:
yield None
else:
pm = funcpath.split('.')
attr = pm.pop(-1)
modpath = '.'.join(pm)
try:
m = __import__(modpath, fromlist = modpath)
pmobject = getattr(m, attr, None)
except ValueError:
pmobject = None
sys.stderr.write(
"%sERROR: no object at %r for postmortem%s"%(
os.linesep, funcpath, os.linesep
)
)
try:
yield None
except:
try:
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
pmobject()
except:
sys.stderr.write(
"[Exception raised by Postmortem]" + os.linesep
)
print_exception(*sys.exc_info())
raise
class Execution(object):
"""
Given argv and context make an execution instance that, when called, will
execute the configured Python code.
This class provides the ability to identify what the main part of the
execution of the configured Python code. For instance, shall it execute a
console, the file that the first argument points to, a -m option module
appended to the python_context option value, or the code given within -c?
"""
def __init__(self,
args, context = (),
main = None,
loader = None,
stdin = sys.stdin
):
"""
args
The arguments passed to the script; usually sys.argv after being
processed by optparse(ca).
context
A list of loader descriptors that will be used to establish the
context of __main__ module.
main
Overload to explicitly state what main is. None will cause the
class to attempt to fill in the attribute using 'args' and other
system objects like sys.stdin.
"""
self.args = args
self.context = context and list(context) or ()
if main is not None:
self.main = main
elif loader is not None:
# Main explicitly stated, resolve the path and the loader
path, ldesc = loader
ltitle, rloader, xpath = ldesc
l = rloader(path)
if l is None:
raise ImportError(
"%s %r does not exist or cannot be read" %(
ltitle, path
)
)
self.main = (path, l)
# If there are args, but no main, run the first arg.
elif args:
fp = self.args[0]
f = open(fp)
try:
l = file_loader(fp, fileobj = f)
finally:
f.close()
self.main = (self.args[0], l)
self.args = self.args[1:]
# There is no main, no loader, and no args.
# If stdin is not a tty, use stdin as the main file.
elif not stdin.isatty():
l = file_loader('