prompts.py
429 lines
| 16.1 KiB
| text/x-python
|
PythonLexer
|
r0 | # -*- coding: utf-8 -*- | ||
|
r2781 | """Classes for handling input/output prompts. | ||
Authors: | ||||
* Fernando Perez | ||||
* Brian Granger | ||||
|
r5496 | * Thomas Kluyver | ||
|
r1853 | """ | ||
|
r0 | |||
|
r2781 | #----------------------------------------------------------------------------- | ||
|
r5390 | # Copyright (C) 2008-2011 The IPython Development Team | ||
|
r1875 | # Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu> | ||
|
r0 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
|
r2781 | #----------------------------------------------------------------------------- | ||
|
r0 | |||
|
r2781 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
|
r2498 | |||
|
r52 | import os | ||
|
r2498 | import re | ||
|
r52 | import socket | ||
import sys | ||||
|
r5495 | import time | ||
|
r0 | |||
|
r5724 | from string import Formatter | ||
|
r5495 | from IPython.config.configurable import Configurable | ||
|
r2043 | from IPython.core import release | ||
|
r2498 | from IPython.utils import coloransi | ||
|
r5495 | from IPython.utils.traitlets import (Unicode, Instance, Dict, Bool, Int) | ||
|
r0 | |||
|
r2781 | #----------------------------------------------------------------------------- | ||
# Color schemes for prompts | ||||
#----------------------------------------------------------------------------- | ||||
|
r0 | |||
|
r2010 | InputColors = coloransi.InputTermColors # just a shorthand | ||
Colors = coloransi.TermColors # just a shorthand | ||||
|
r0 | |||
|
r5495 | color_lists = dict(normal=Colors(), inp=InputColors(), nocolor=coloransi.NoColors()) | ||
PColNoColors = coloransi.ColorScheme( | ||||
|
r0 | 'NoColor', | ||
in_prompt = InputColors.NoColor, # Input prompt | ||||
in_number = InputColors.NoColor, # Input prompt number | ||||
in_prompt2 = InputColors.NoColor, # Continuation prompt | ||||
in_normal = InputColors.NoColor, # color off (usu. Colors.Normal) | ||||
|
r4872 | |||
|
r0 | out_prompt = Colors.NoColor, # Output prompt | ||
out_number = Colors.NoColor, # Output prompt number | ||||
normal = Colors.NoColor # color off (usu. Colors.Normal) | ||||
|
r5495 | ) | ||
|
r46 | |||
|
r0 | # make some schemes as instances so we can copy them for modification easily: | ||
|
r5495 | PColLinux = coloransi.ColorScheme( | ||
|
r0 | 'Linux', | ||
in_prompt = InputColors.Green, | ||||
in_number = InputColors.LightGreen, | ||||
in_prompt2 = InputColors.Green, | ||||
in_normal = InputColors.Normal, # color off (usu. Colors.Normal) | ||||
out_prompt = Colors.Red, | ||||
out_number = Colors.LightRed, | ||||
normal = Colors.Normal | ||||
) | ||||
|
r46 | |||
|
r0 | # Slightly modified Linux for light backgrounds | ||
|
r5495 | PColLightBG = PColLinux.copy('LightBG') | ||
|
r0 | |||
|
r5495 | PColLightBG.colors.update( | ||
|
r0 | in_prompt = InputColors.Blue, | ||
in_number = InputColors.LightBlue, | ||||
in_prompt2 = InputColors.Blue | ||||
) | ||||
#----------------------------------------------------------------------------- | ||||
|
r2781 | # Utilities | ||
#----------------------------------------------------------------------------- | ||||
|
r5495 | class LazyEvaluate(object): | ||
"""This is used for formatting strings with values that need to be updated | ||||
|
r5496 | at that time, such as the current time or working directory.""" | ||
|
r5495 | def __init__(self, func, *args, **kwargs): | ||
self.func = func | ||||
self.args = args | ||||
self.kwargs = kwargs | ||||
def __call__(self, **kwargs): | ||||
self.kwargs.update(kwargs) | ||||
return self.func(*self.args, **self.kwargs) | ||||
def __str__(self): | ||||
return str(self()) | ||||
|
r0 | def multiple_replace(dict, text): | ||
""" Replace in 'text' all occurences of any key in the given | ||||
dictionary by its corresponding value. Returns the new string.""" | ||||
# Function by Xavier Defrang, originally found at: | ||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81330 | ||||
# Create a regular expression from the dictionary keys | ||||
regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys()))) | ||||
# For each match, look-up corresponding value in dictionary | ||||
return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text) | ||||
#----------------------------------------------------------------------------- | ||||
# Special characters that can be used in prompt templates, mainly bash-like | ||||
|
r2781 | #----------------------------------------------------------------------------- | ||
|
r0 | |||
# If $HOME isn't defined (Windows), make it an absurd string so that it can | ||||
# never be expanded out into '~'. Basically anything which can never be a | ||||
# reasonable directory name will do, we just want the $HOME -> '~' operation | ||||
# to become a no-op. We pre-compute $HOME here so it's not done on every | ||||
# prompt call. | ||||
# FIXME: | ||||
# - This should be turned into a class which does proper namespace management, | ||||
# since the prompt specials need to be evaluated in a certain namespace. | ||||
# Currently it's just globals, which need to be managed manually by code | ||||
# below. | ||||
# - I also need to split up the color schemes from the prompt specials | ||||
# somehow. I don't have a clean design for that quite yet. | ||||
HOME = os.environ.get("HOME","//////:::::ZZZZZ,,,~~~") | ||||
# We precompute a few more strings here for the prompt_specials, which are | ||||
# fixed once ipython starts. This reduces the runtime overhead of computing | ||||
# prompt strings. | ||||
USER = os.environ.get("USER") | ||||
HOSTNAME = socket.gethostname() | ||||
HOSTNAME_SHORT = HOSTNAME.split(".")[0] | ||||
|
r5495 | ROOT_SYMBOL = "#" if (os.name=='nt' or os.getuid()==0) else "$" | ||
|
r0 | |||
|
r5495 | prompt_abbreviations = { | ||
|
r0 | # Prompt/history count | ||
|
r5495 | '%n' : '{color.number}' '{count}' '{color.prompt}', | ||
r'\#': '{color.number}' '{count}' '{color.prompt}', | ||||
|
r579 | # Just the prompt counter number, WITHOUT any coloring wrappers, so users | ||
# can get numbers displayed in whatever color they want. | ||||
|
r5495 | r'\N': '{count}', | ||
|
r1953 | |||
|
r0 | # Prompt/history count, with the actual digits replaced by dots. Used | ||
# mainly in continuation prompts (prompt_in2) | ||||
|
r5495 | r'\D': '{dots}', | ||
|
r2485 | |||
|
r0 | # Current time | ||
|
r5497 | r'\T' : '{time}', | ||
|
r5495 | # Current working directory | ||
r'\w': '{cwd}', | ||||
|
r0 | # Basename of current working directory. | ||
# (use os.sep to make this portable across OSes) | ||||
|
r5495 | r'\W' : '{cwd_last}', | ||
|
r0 | # These X<N> are an extension to the normal bash prompts. They return | ||
# N terms of the path, after replacing $HOME with '~' | ||||
|
r5498 | r'\X0': '{cwd_x[0]}', | ||
r'\X1': '{cwd_x[1]}', | ||||
r'\X2': '{cwd_x[2]}', | ||||
r'\X3': '{cwd_x[3]}', | ||||
r'\X4': '{cwd_x[4]}', | ||||
r'\X5': '{cwd_x[5]}', | ||||
|
r0 | # Y<N> are similar to X<N>, but they show '~' if it's the directory | ||
# N+1 in the list. Somewhat like %cN in tcsh. | ||||
|
r5498 | r'\Y0': '{cwd_y[0]}', | ||
r'\Y1': '{cwd_y[1]}', | ||||
r'\Y2': '{cwd_y[2]}', | ||||
r'\Y3': '{cwd_y[3]}', | ||||
r'\Y4': '{cwd_y[4]}', | ||||
r'\Y5': '{cwd_y[5]}', | ||||
|
r0 | # Hostname up to first . | ||
|
r579 | r'\h': HOSTNAME_SHORT, | ||
|
r0 | # Full hostname | ||
|
r579 | r'\H': HOSTNAME, | ||
|
r0 | # Username of current user | ||
|
r579 | r'\u': USER, | ||
|
r0 | # Escaped '\' | ||
'\\\\': '\\', | ||||
# Newline | ||||
|
r579 | r'\n': '\n', | ||
|
r0 | # Carriage return | ||
|
r579 | r'\r': '\r', | ||
|
r0 | # Release version | ||
|
r2043 | r'\v': release.version, | ||
|
r0 | # Root symbol ($ or #) | ||
|
r579 | r'\$': ROOT_SYMBOL, | ||
|
r0 | } | ||
#----------------------------------------------------------------------------- | ||||
|
r2781 | # More utilities | ||
#----------------------------------------------------------------------------- | ||||
|
r5498 | def cwd_filt(depth): | ||
|
r5495 | """Return the last depth elements of the current working directory. | ||
|
r763 | |||
|
r5495 | $HOME is always replaced with '~'. | ||
If depth==0, the full path is returned.""" | ||||
|
r763 | |||
|
r5495 | cwd = os.getcwd().replace(HOME,"~") | ||
out = os.sep.join(cwd.split(os.sep)[-depth:]) | ||||
return out or os.sep | ||||
|
r0 | |||
|
r5498 | def cwd_filt2(depth): | ||
|
r5495 | """Return the last depth elements of the current working directory. | ||
|
r4872 | |||
|
r5495 | $HOME is always replaced with '~'. | ||
If depth==0, the full path is returned.""" | ||||
|
r763 | |||
|
r5495 | full_cwd = os.getcwd() | ||
cwd = full_cwd.replace(HOME,"~").split(os.sep) | ||||
if '~' in cwd and len(cwd) == depth+1: | ||||
depth += 1 | ||||
drivepart = '' | ||||
if sys.platform == 'win32' and len(cwd) > depth: | ||||
drivepart = os.path.splitdrive(full_cwd)[0] | ||||
out = drivepart + '/'.join(cwd[-depth:]) | ||||
|
r0 | |||
|
r5495 | return out or os.sep | ||
|
r0 | |||
|
r5495 | #----------------------------------------------------------------------------- | ||
# Prompt classes | ||||
#----------------------------------------------------------------------------- | ||||
|
r794 | |||
|
r5495 | lazily_evaluate = {'time': LazyEvaluate(time.strftime, "%H:%M:%S"), | ||
'cwd': LazyEvaluate(os.getcwd), | ||||
'cwd_last': LazyEvaluate(lambda: os.getcwd().split(os.sep)[-1]), | ||||
'cwd_x': [LazyEvaluate(lambda: os.getcwd().replace("%s","~"))] +\ | ||||
[LazyEvaluate(cwd_filt, x) for x in range(1,6)], | ||||
'cwd_y': [LazyEvaluate(cwd_filt2, x) for x in range(6)] | ||||
} | ||||
|
r5657 | |||
def _lenlastline(s): | ||||
"""Get the length of the last line. More intelligent than | ||||
len(s.splitlines()[-1]). | ||||
""" | ||||
if not s or s.endswith(('\n', '\r')): | ||||
return 0 | ||||
return len(s.splitlines()[-1]) | ||||
|
r5495 | |||
|
r5724 | |||
class UserNSFormatter(Formatter): | ||||
"""A Formatter that falls back on a shell's user_ns and __builtins__ for name resolution""" | ||||
def __init__(self, shell): | ||||
self.shell = shell | ||||
def get_value(self, key, args, kwargs): | ||||
# try regular formatting first: | ||||
try: | ||||
return Formatter.get_value(self, key, args, kwargs) | ||||
except Exception: | ||||
pass | ||||
# next, look in user_ns and builtins: | ||||
for container in (self.shell.user_ns, __builtins__): | ||||
if key in container: | ||||
return container[key] | ||||
# nothing found, put error message in its place | ||||
return "<ERROR: '%s' not found>" % key | ||||
|
r5495 | class PromptManager(Configurable): | ||
|
r5496 | """This is the primary interface for producing IPython's prompts.""" | ||
|
r5495 | shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') | ||
color_scheme_table = Instance(coloransi.ColorSchemeTable) | ||||
|
r5496 | color_scheme = Unicode('Linux', config=True) | ||
|
r5495 | def _color_scheme_changed(self, name, new_value): | ||
self.color_scheme_table.set_active_scheme(new_value) | ||||
for pname in ['in', 'in2', 'out', 'rewrite']: | ||||
# We need to recalculate the number of invisible characters | ||||
self.update_prompt(pname) | ||||
|
r5496 | lazy_evaluate_fields = Dict(help=""" | ||
This maps field names used in the prompt templates to functions which | ||||
will be called when the prompt is rendered. This allows us to include | ||||
things like the current time in the prompts. Functions are only called | ||||
if they are used in the prompt. | ||||
""") | ||||
|
r5495 | def _lazy_evaluate_fields_default(self): return lazily_evaluate.copy() | ||
|
r5548 | in_template = Unicode('In [\\#]: ', config=True, | ||
help="Input prompt. '\\#' will be transformed to the prompt number") | ||||
in2_template = Unicode(' .\\D.: ', config=True, | ||||
help="Continuation prompt.") | ||||
out_template = Unicode('Out[\\#]: ', config=True, | ||||
help="Output prompt. '\\#' will be transformed to the prompt number") | ||||
|
r5495 | |||
|
r5496 | justify = Bool(True, config=True, help=""" | ||
If True (default), each prompt will be right-aligned with the | ||||
preceding one. | ||||
""") | ||||
|
r5495 | |||
# We actually store the expanded templates here: | ||||
templates = Dict() | ||||
# The number of characters in the last prompt rendered, not including | ||||
# colour characters. | ||||
width = Int() | ||||
|
r5552 | txtwidth = Int() # Not including right-justification | ||
|
r5495 | |||
# The number of characters in each prompt which don't contribute to width | ||||
invisible_chars = Dict() | ||||
def _invisible_chars_default(self): | ||||
|
r5555 | return {'in': 0, 'in2': 0, 'out': 0, 'rewrite':0} | ||
|
r5495 | |||
def __init__(self, shell, config=None): | ||||
super(PromptManager, self).__init__(shell=shell, config=config) | ||||
# Prepare colour scheme table | ||||
self.color_scheme_table = coloransi.ColorSchemeTable([PColNoColors, | ||||
PColLinux, PColLightBG], self.color_scheme) | ||||
|
r5724 | self._formatter = UserNSFormatter(shell) | ||
|
r5555 | # Prepare templates & numbers of invisible characters | ||
|
r5495 | self.update_prompt('in', self.in_template) | ||
self.update_prompt('in2', self.in2_template) | ||||
self.update_prompt('out', self.out_template) | ||||
|
r5555 | self.update_prompt('rewrite') | ||
|
r5495 | self.on_trait_change(self._update_prompt_trait, ['in_template', | ||
|
r5555 | 'in2_template', 'out_template']) | ||
|
r5495 | |||
def update_prompt(self, name, new_template=None): | ||||
|
r5496 | """This is called when a prompt template is updated. It processes | ||
abbreviations used in the prompt template (like \#) and calculates how | ||||
many invisible characters (ANSI colour escapes) the resulting prompt | ||||
contains. | ||||
It is also called for each prompt on changing the colour scheme. In both | ||||
cases, traitlets should take care of calling this automatically. | ||||
""" | ||||
|
r5495 | if new_template is not None: | ||
self.templates[name] = multiple_replace(prompt_abbreviations, new_template) | ||||
|
r5657 | # We count invisible characters (colour escapes) on the last line of the | ||
# prompt, to calculate the width for lining up subsequent prompts. | ||||
invis_chars = _lenlastline(self._render(name, color=True)) - \ | ||||
_lenlastline(self._render(name, color=False)) | ||||
|
r5495 | self.invisible_chars[name] = invis_chars | ||
def _update_prompt_trait(self, traitname, new_template): | ||||
name = traitname[:-9] # Cut off '_template' | ||||
self.update_prompt(name, new_template) | ||||
|
r5552 | def _render(self, name, color=True, **kwargs): | ||
"""Render but don't justify, or update the width or txtwidth attributes. | ||||
|
r5495 | """ | ||
|
r5555 | if name == 'rewrite': | ||
return self._render_rewrite(color=color) | ||||
|
r5495 | if color: | ||
scheme = self.color_scheme_table.active_colors | ||||
if name=='out': | ||||
colors = color_lists['normal'] | ||||
colors.number, colors.prompt, colors.normal = \ | ||||
scheme.out_number, scheme.out_prompt, scheme.normal | ||||
else: | ||||
colors = color_lists['inp'] | ||||
colors.number, colors.prompt, colors.normal = \ | ||||
scheme.in_number, scheme.in_prompt, scheme.in_normal | ||||
if name=='in2': | ||||
colors.prompt = scheme.in_prompt2 | ||||
|
r0 | else: | ||
|
r5495 | # No color | ||
colors = color_lists['nocolor'] | ||||
colors.number, colors.prompt, colors.normal = '', '', '' | ||||
count = self.shell.execution_count # Shorthand | ||||
# Build the dictionary to be passed to string formatting | ||||
fmtargs = dict(color=colors, count=count, | ||||
|
r5552 | dots="."*len(str(count)), | ||
width=self.width, txtwidth=self.txtwidth ) | ||||
|
r5495 | fmtargs.update(self.lazy_evaluate_fields) | ||
fmtargs.update(kwargs) | ||||
# Prepare the prompt | ||||
prompt = colors.prompt + self.templates[name] + colors.normal | ||||
# Fill in required fields | ||||
|
r5724 | return self._formatter.format(prompt, **fmtargs) | ||
|
r5552 | |||
|
r5555 | def _render_rewrite(self, color=True): | ||
"""Render the ---> rewrite prompt.""" | ||||
if color: | ||||
scheme = self.color_scheme_table.active_colors | ||||
# We need a non-input version of these escapes | ||||
color_prompt = scheme.in_prompt.replace("\001","").replace("\002","") | ||||
color_normal = scheme.normal | ||||
else: | ||||
color_prompt, color_normal = '', '' | ||||
return color_prompt + "-> ".rjust(self.txtwidth, "-") + color_normal | ||||
|
r5552 | def render(self, name, color=True, just=None, **kwargs): | ||
""" | ||||
Render the selected prompt. | ||||
Parameters | ||||
---------- | ||||
name : str | ||||
Which prompt to render. One of 'in', 'in2', 'out', 'rewrite' | ||||
color : bool | ||||
If True (default), include ANSI escape sequences for a coloured prompt. | ||||
just : bool | ||||
If True, justify the prompt to the width of the last prompt. The | ||||
default is stored in self.justify. | ||||
**kwargs : | ||||
Additional arguments will be passed to the string formatting operation, | ||||
so they can override the values that would otherwise fill in the | ||||
template. | ||||
Returns | ||||
------- | ||||
A string containing the rendered prompt. | ||||
""" | ||||
res = self._render(name, color=color, **kwargs) | ||||
|
r5495 | |||
# Handle justification of prompt | ||||
invis_chars = self.invisible_chars[name] if color else 0 | ||||
|
r5657 | self.txtwidth = _lenlastline(res) - invis_chars | ||
|
r5495 | just = self.justify if (just is None) else just | ||
|
r5657 | # If the prompt spans more than one line, don't try to justify it: | ||
if just and ('\n' not in res) and ('\r' not in res): | ||||
|
r5495 | res = res.rjust(self.width + invis_chars) | ||
|
r5657 | self.width = _lenlastline(res) - invis_chars | ||
|
r5495 | return res | ||