-
-
Save lonetwin/5902720 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# The MIT License (MIT) | |
# | |
# Copyright (c) 2015 Steven Fernandez | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
"""lonetwin's pimped-up pythonrc | |
This file will be executed when the Python interactive shell is started, if | |
$PYTHONSTARTUP is in your environment and points to this file. | |
You could also simply make this file executable and call it directly. | |
This file create an InteractiveConsole instance, which provides: | |
* colored prompts and pretty printing | |
* intelligent tab completion:¹ | |
- with preceding text | |
+ names in the current namespace | |
+ for objects, their attributes/methods | |
+ for strings with a '/', pathname completion | |
- without preceding text four spaces | |
* shortcut to open your $EDITOR with the last executed command | |
(the '\e' command) | |
* temporary escape to $SHELL or ability to execute a shell command and | |
capturing the output in to the '_' variable (the '!' command) | |
* execution history | |
* convenient printing of doc stings (the '?' command) | |
Some ideas borrowed from: | |
* http://eseth.org/2008/pimping-pythonrc.html | |
(which co-incidentally reused something I wrote back in 2005 !! Ain't | |
sharing great ?) | |
* http://igotgenes.blogspot.in/2009/01/tab-completion-and-history-in-python.html | |
If you have any other good ideas please feel free to leave a comment. | |
¹ Since python 3.4 the default interpreter also has tab completion enabled | |
however it does not do pathname completion | |
""" | |
try: | |
import builtins | |
except ImportError: | |
import __builtin__ as builtins | |
import atexit | |
import glob | |
import keyword | |
import os | |
import pkgutil | |
import pprint | |
import re | |
import readline | |
import rlcompleter | |
import signal | |
import subprocess | |
import sys | |
from code import InteractiveConsole | |
from collections import namedtuple | |
from tempfile import mkstemp | |
HISTFILE = os.path.expanduser("~/.python_history") | |
HISTSIZE = 1000 | |
EDITOR = os.environ.get('EDITOR', 'vi') | |
SHELL = os.environ.get('SHELL', '$SHELL') | |
def create_color_func(code): | |
def color_func(text, bold=True, readline_workaround=False): | |
code_str = '1;{}'.format(code) if bold else code | |
# - reason for readline_workaround: http://bugs.python.org/issue20359 | |
if readline_workaround: | |
return "\001\033[{}m\002{}\001\033[0m\002".format(code_str, text) | |
else: | |
return "\033[{}m{}\033[0m".format(code_str, text) | |
return color_func | |
# add any colors you might need. | |
red = create_color_func(31) | |
green = create_color_func(32) | |
yellow = create_color_func(33) | |
blue = create_color_func(34) | |
purple = create_color_func(35) | |
cyan = create_color_func(36) | |
class ImprovedConsole(InteractiveConsole, object): | |
EDIT_CMD = '\e' | |
SH_EXEC = '!' | |
DOC_CMD = '?' | |
HELP_CMD = '\h' | |
MODLIST = frozenset(name for _, name, _ in pkgutil.iter_modules()) | |
def __init__(self, tab=' ', *args, **kwargs): | |
self.session_history = [] # This holds the last executed statements | |
self.buffer = [] # This holds the statement to be executed | |
self.tab = tab | |
super(ImprovedConsole, self).__init__(*args, **kwargs) | |
self._init_readline() | |
self._init_prompt() | |
self._init_pprint() | |
def _init_readline(self): | |
"""Activates history and tab completion | |
""" | |
# - init history | |
if os.path.exists(HISTFILE): | |
readline.read_history_file(HISTFILE) | |
readline.set_history_length(HISTSIZE) | |
atexit.register(lambda :readline.write_history_file(HISTFILE)) | |
# - turn on tab completion | |
readline.parse_and_bind('tab: complete') | |
# - other useful stuff | |
readline.parse_and_bind('set skip-completed-text on') | |
readline.set_completer(self._improved_rlcompleter()) | |
def _init_prompt(self): | |
"""Activates color on the prompt based on python version. | |
Also adds the hosts IP if running on a remote host over a | |
ssh connection. | |
""" | |
prompt_color = green if sys.version_info.major == 2 else yellow | |
sys.ps1 = prompt_color('>>> ', readline_workaround=True) | |
sys.ps2 = red('... ', readline_workaround=True) | |
# - if we are over a remote connection, modify the ps1 | |
if os.environ.get('SSH_CONNECTION'): | |
this_host = os.environ['SSH_CONNECTION'].split()[-2] | |
sys.ps1 = prompt_color('[{}]>>> '.format(this_host), readline_workaround=True) | |
sys.ps2 = red('[{}]... '.format(this_host), readline_workaround=True) | |
def _init_pprint(self): | |
"""Activates pretty-printing of output values. | |
""" | |
try: | |
rows, cols = subprocess.check_output('stty size', shell=True).strip().split() | |
except: | |
cols = 80 | |
def pprint_callback(value): | |
if value is not None: | |
builtins._ = value | |
formatted = pprint.pformat(value, width=cols) | |
if issubclass(type(value), dict): | |
formatted = re.sub(r'([ {][^{:]+?: )+?', lambda m: purple(m.group()), formatted) | |
print(formatted) | |
else: | |
print(blue(formatted)) | |
sys.displayhook = pprint_callback | |
def _improved_rlcompleter(self): | |
"""Enhances the default rlcompleter | |
The function enhances the default rlcompleter by also doing | |
pathname completion and module name completion for import | |
statements. Additionally, it inserts a tab instead of attempting | |
completion if there is no preceding text. | |
""" | |
completer = rlcompleter.Completer(namespace=self.locals) | |
# - remove / from the delimiters to help identify possibility for path completion | |
readline.set_completer_delims(readline.get_completer_delims().replace('/', '')) | |
def complete_wrapper(text, state): | |
line = readline.get_line_buffer().strip() | |
if line == '': | |
return None if state > 0 else self.tab | |
if state == 0: | |
if line.startswith('import') or line.startswith('from'): | |
completer.matches = [ name for name in self.MODLIST if name.startswith(text) ] | |
else: | |
match = completer.complete(text, state) | |
if match is None and '/' in text: | |
completer.matches = glob.glob(text+'*') | |
try: | |
match = completer.matches[state] | |
return '{}{}'.format(match, ' ' if keyword.iskeyword(match) else '') | |
except IndexError: | |
return None | |
return complete_wrapper | |
def raw_input(self, *args): | |
"""Read the input and delegate if necessary. | |
""" | |
line = InteractiveConsole.raw_input(self, *args) | |
if line == self.HELP_CMD: | |
print(HELP) | |
line = '' | |
elif line == self.EDIT_CMD: | |
line = self._process_edit_cmd() | |
elif line.startswith(self.SH_EXEC): | |
line = self._process_sh_cmd(line.strip(self.SH_EXEC)) | |
elif line.endswith(self.DOC_CMD): | |
line = line.strip(self.DOC_CMD + '.(') | |
if not line: | |
line = 'dir()' | |
elif keyword.iskeyword(line): | |
line = 'help("{}")'.format(line) | |
else: | |
line = 'print({}.__doc__)'.format(line) | |
return line | |
def write(self, data): | |
"""Write out errors to stderr | |
""" | |
sys.stderr.write(red(data)) | |
def resetbuffer(self): | |
self.session_history.extend(self.buffer) | |
return super(ImprovedConsole, self).resetbuffer() | |
def _process_edit_cmd(self): | |
# - setup the edit buffer | |
fd, filename = mkstemp('.py') | |
lines = '\n'.join('# {}'.format(line.strip('\n')) for line in self.session_history) | |
os.write(fd, lines.encode('utf-8')) | |
os.close(fd) | |
# - shell out to the editor | |
os.system('{} {}'.format(EDITOR, filename)) | |
# - process commands | |
lines = open(filename) | |
os.unlink(filename) | |
for stmt in lines: | |
self.write(cyan("... {}".format(stmt))) | |
line = stmt.strip('\n') | |
if not line.strip().startswith('#'): | |
self.push(line) | |
readline.add_history(line) | |
return '' | |
def _process_sh_cmd(self, cmd): | |
cmd_exec = namedtuple('CmdExec', ['out', 'err', 'rc']) | |
if cmd: | |
cmd = cmd.format(**self.locals) | |
try: | |
process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) | |
except: | |
self.showtraceback() | |
else: | |
out, err = process.communicate() | |
rc = process.returncode | |
print ('{}'.format(red(err.decode('utf-8') | |
if err else green(out.decode('utf-8'), bold=False)))) | |
builtins._ = cmd_exec(out, err, rc) | |
del cmd_exec | |
else: | |
if os.environ.get('SSH_CONNECTION'): | |
# I use the bash function similar to the one below in my | |
# .bashrc to directly open a python prompt on remote | |
# systems I log on to. | |
# function rpython { ssh -t $1 -- "python" } | |
# Unfortunately, suspending this ssh session, does not place me | |
# in a shell, so I need to create one: | |
os.system(os.environ.get('SHELL', '/bin/bash')) | |
else: | |
os.kill(os.getpid(), signal.SIGSTOP) | |
return '' | |
# Welcome message | |
HELP = cyan("""\ | |
Welcome to lonetwin's pimped up python prompt | |
( available at https://gist.github.com/lonetwin/5902720 ) | |
You've got color, tab completion, pretty-printing, an editable input buffer | |
(via the '\e' command) and shell command execution (via the '!' command). | |
* A tab with preceding text will attempt auto-completion of keywords, name in | |
the current namespace, attributes and methods. If the preceding text has a | |
'/' filename completion will be attempted. Without preceding text four spaces | |
will be inserted. | |
* History will be saved in {HISTFILE} when you exit. | |
* The '\e' command will open {EDITOR} with the history for the current | |
session. On closing the editor any lines not starting with '#' will be | |
executed. | |
* The '!' command without anything following it will suspend this process, use | |
fg to get back. | |
- If the '!' command is followed by any text, the text will be executed in | |
{SHELL} and the output/error will be displayed. | |
- You may pass strings from the global namespace to the command line using | |
the `.format()` syntax assuming the globals are passed to format as kwargs. | |
- Additionally '_' will contain a named tuple representing the | |
(<stdout>, <stderror>, <return_code>) for the execution of the command. | |
for example: | |
>>> filename='/does/not/exist' | |
>>> !ls {{filename}} | |
ls: cannot access /does/not/exist: No such file or directory | |
>>> _ | |
CmdExec(out='', err='ls: cannot access /does/not/exist: No such file or directory\n', rc=2) | |
* Simply typing out a defined name followed by a '?' will print out the | |
object's __doc__ attribute if one exists. (eg: []? / str? / os.getcwd? ) | |
""".format(**globals())) | |
# - create our pimped out console | |
pymp = ImprovedConsole() | |
banner="Welcome to the ImprovedConsole. Type in \h for list of features" | |
# - fire it up ! | |
while True: | |
try: | |
pymp.interact(banner=banner) | |
except: | |
import traceback | |
print(red("I'm sorry, ImprovedConsole could not handle that !\n" | |
"Please report an error with this traceback, I would really appreciate that !")) | |
traceback.print_exc() | |
print(red("I shall try to restore the crashed session.\n" | |
"If the crash occurs again, please exit the session")) | |
banner=blue("Your crashed session has been restored") | |
else: | |
break | |
# Exit the Python shell on exiting the InteractiveConsole | |
sys.exit() |
@deenes thanks for reporting that issue ! Sorry I didn't see your comment earlier since comments on gists do not send out notifications AFAICT. Anyways, the issue you reported appears to be a tricky one. The only way I can get this to work is to modify the initialization of ImprovedConsole
to accept the locals()
. Something like this (on line 313 currently):
pymp = ImprovedConsole(locals=locals())
This however, also pollutes the interpreter namespace with everything inside the .pythonrc.py
namespace. The reason for this behavior is because when using this pythonrc, the actual __main__
is the pythonrc namespace rather than the interpreter environment you are in.
I'll try to fix / workaround this if possible but as of right now it seems unlikely.
This gist will not longer be updated. Development will continue at https://github.com/lonetwin/pythonrc
Hi, thanks for your very nice pythonrc, I have used it already for a while. Now I realized that it gives an ImportError on using timeit. For example, try to run this minimal example: http://stackoverflow.com/a/12404635/854988, while testing without the pythonrc it works fine.