Add support for IPython-5

Fixes #1617
This commit is contained in:
Foo 2016-07-08 20:24:38 +03:00
parent d816de054a
commit 2954c83330
8 changed files with 369 additions and 129 deletions

View File

@ -3,10 +3,20 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct
from weakref import ref
from IPython.core.prompts import PromptManager
try:
from IPython.core.prompts import PromptManager
has_prompt_manager = True
except ImportError:
from IPython.terminal.interactiveshell import TerminalInteractiveShell
has_prompt_manager = False
from IPython.core.magic import Magics, magics_class, line_magic
from powerline.ipython import IPythonPowerline, RewriteResult
from powerline.ipython import IPythonPowerline, IPythonInfo
if has_prompt_manager:
from powerline.ipython import RewriteResult
else:
from powerline.renderers.ipython.since_5 import PowerlinePromptStyle, PowerlinePrompts
@magics_class
@ -23,37 +33,29 @@ class PowerlineMagics(Magics):
raise ValueError('Expected `reload`, but got {0}'.format(line))
class IPythonInfo(object):
def __init__(self, shell):
self._shell = shell
if has_prompt_manager:
class PowerlinePromptManager(PromptManager):
def __init__(self, powerline, shell):
self.powerline = powerline
self.powerline_segment_info = IPythonInfo(shell)
self.shell = shell
@property
def prompt_count(self):
return self._shell.execution_count
class PowerlinePromptManager(PromptManager):
def __init__(self, powerline, shell):
self.powerline = powerline
self.powerline_segment_info = IPythonInfo(shell)
self.shell = shell
def render(self, name, color=True, *args, **kwargs):
res = self.powerline.render(
is_prompt=name.startswith('in'),
side='left',
output_width=True,
output_raw=not color,
matcher_info=name,
segment_info=self.powerline_segment_info,
)
self.txtwidth = res[-1]
self.width = res[-1]
ret = res[0] if color else res[1]
if name == 'rewrite':
return RewriteResult(ret)
else:
return ret
def render(self, name, color=True, *args, **kwargs):
res = self.powerline.render(
is_prompt=name.startswith('in'),
side='left',
output_width=True,
output_raw=not color,
matcher_info=name,
segment_info=self.powerline_segment_info,
)
self.txtwidth = res[-1]
self.width = res[-1]
ret = res[0] if color else res[1]
if name == 'rewrite':
return RewriteResult(ret)
else:
return ret
class ShutdownHook(object):
@ -67,33 +69,59 @@ class ShutdownHook(object):
raise TryNext()
old_prompt_manager = None
old_style = None
old_prompts = None
class ConfigurableIPythonPowerline(IPythonPowerline):
def init(self, ip):
config = ip.config.Powerline
self.config_overrides = config.get('config_overrides')
self.theme_overrides = config.get('theme_overrides', {})
self.config_paths = config.get('config_paths')
super(ConfigurableIPythonPowerline, self).init()
if has_prompt_manager:
renderer_module = '.pre_5'
else:
renderer_module = '.since_5'
super(ConfigurableIPythonPowerline, self).init(
renderer_module=renderer_module)
def do_setup(self, ip, shutdown_hook):
prompt_manager = PowerlinePromptManager(
powerline=self,
shell=ip.prompt_manager.shell,
)
global old_prompt_manager
global old_style
global old_prompts
if has_prompt_manager:
if old_prompt_manager is None:
old_prompt_manager = ip.prompt_manager
prompt_manager = PowerlinePromptManager(
powerline=self,
shell=ip.prompt_manager.shell,
)
ip.prompt_manager = prompt_manager
else:
if ip.pt_cli is not None:
if old_style is None:
old_style = ip.pt_cli.application.style
prev_style = ip.pt_cli.application.style
while isinstance(prev_style, PowerlinePromptStyle):
prev_style = prev_style.get_style()
new_style = PowerlinePromptStyle(lambda: prev_style)
ip.pt_cli.application.style = new_style
ip.pt_cli.renderer.style = new_style
if old_prompts is None:
old_prompts = ip.prompts
ip.prompts = PowerlinePrompts(ip.prompts, self)
magics = PowerlineMagics(ip, self)
shutdown_hook.powerline = ref(self)
ip.prompt_manager = prompt_manager
ip.register_magics(magics)
old_prompt_manager = None
def load_ipython_extension(ip):
global old_prompt_manager
old_prompt_manager = ip.prompt_manager
powerline = ConfigurableIPythonPowerline(ip)
shutdown_hook = ShutdownHook()
@ -103,4 +131,11 @@ def load_ipython_extension(ip):
def unload_ipython_extension(ip):
ip.prompt_manager = old_prompt_manager
if old_prompt_manager is not None:
ip.prompt_manager = old_prompt_manager
if old_style is not None:
ip.pt_cli.application.style = old_style
ip.pt_cli.renderer.style = old_style
ip.prompts = old_prompts
old_prompt_manager = None
old_style = None

View File

@ -99,7 +99,7 @@ class ConfigurableIPythonPowerline(IPythonPowerline):
self.config_overrides = config_overrides
self.theme_overrides = theme_overrides
self.config_paths = config_paths
super(ConfigurableIPythonPowerline, self).init()
super(ConfigurableIPythonPowerline, self).init(renderer_module='.pre_5')
def ipython_magic(self, ip, parameter_s=''):
if parameter_s == 'reload':

View File

@ -6,6 +6,15 @@ from powerline.lib.dict import mergedicts
from powerline.lib.unicode import string
class IPythonInfo(object):
def __init__(self, shell):
self._shell = shell
@property
def prompt_count(self):
return self._shell.execution_count
# HACK: ipython tries to only leave us with plain ASCII
class RewriteResult(object):
def __init__(self, prompt):

View File

@ -4,6 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct
import sys
import os
import re
import operator
from itertools import chain
@ -310,6 +311,19 @@ class Renderer(object):
},
}
hl_join = staticmethod(''.join)
'''Join a list of rendered segments into a resulting string
This method exists to deal with non-string render outputs, so `segments`
may actually be not an iterable with strings.
:param list segments:
Iterable containing rendered segments. By rendered segments
:py:meth:`Renderer.hl` output is meant.
:return: Results of joining these segments.
'''
def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme):
'''Like Renderer.render(), but accept theme in place of matcher_info
'''
@ -323,7 +337,7 @@ class Renderer(object):
# No width specified, so we dont need to crop or pad anything
if output_width:
current_width = self._render_length(theme, segments, self.compute_divider_widths(theme))
return construct_returned_value(''.join([
return construct_returned_value(self.hl_join([
segment['_rendered_hl']
for segment in self._render_segments(theme, segments)
]) + self.hlstyle(), segments, current_width, output_raw, output_width)
@ -378,7 +392,10 @@ class Renderer(object):
elif output_width:
current_width = self._render_length(theme, segments, divider_widths)
rendered_highlighted = ''.join([segment['_rendered_hl'] for segment in self._render_segments(theme, segments)])
rendered_highlighted = self.hl_join([
segment['_rendered_hl']
for segment in self._render_segments(theme, segments)
])
if rendered_highlighted:
rendered_highlighted += self.hlstyle()

View File

@ -1,12 +1,11 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
from powerline.renderers.shell import ShellRenderer
from powerline.renderers.shell.readline import ReadlineRenderer
from powerline.theme import Theme
from powerline.renderers.shell import PromptRenderer
class IPythonRenderer(ShellRenderer):
class IPythonRenderer(PromptRenderer):
'''Powerline ipython segment renderer.'''
def get_segment_info(self, segment_info, mode):
r = self.segment_info.copy()
@ -33,50 +32,3 @@ class IPythonRenderer(ShellRenderer):
for match in self.local_themes.values():
if 'theme' in match:
match['theme'].shutdown()
def render(self, **kwargs):
# XXX super(ShellRenderer), *not* super(IPythonRenderer)
return super(ShellRenderer, self).render(**kwargs)
def do_render(self, segment_info, **kwargs):
segment_info.update(client_id='ipython')
return super(IPythonRenderer, self).do_render(
segment_info=segment_info,
**kwargs
)
class IPythonPromptRenderer(IPythonRenderer, ReadlineRenderer):
'''Powerline ipython prompt (in and in2) renderer'''
pass
class IPythonNonPromptRenderer(IPythonRenderer):
'''Powerline ipython non-prompt (out and rewrite) renderer'''
pass
class RendererProxy(object):
'''Powerline IPython renderer proxy which chooses appropriate renderer
Instantiates two renderer objects: one will be used for prompts and the
other for non-prompts.
'''
def __init__(self, **kwargs):
old_widths = {}
self.non_prompt_renderer = IPythonNonPromptRenderer(old_widths=old_widths, **kwargs)
self.prompt_renderer = IPythonPromptRenderer(old_widths=old_widths, **kwargs)
def render_above_lines(self, *args, **kwargs):
return self.non_prompt_renderer.render_above_lines(*args, **kwargs)
def render(self, is_prompt, *args, **kwargs):
return (self.prompt_renderer if is_prompt else self.non_prompt_renderer).render(
*args, **kwargs)
def shutdown(self, *args, **kwargs):
self.prompt_renderer.shutdown(*args, **kwargs)
self.non_prompt_renderer.shutdown(*args, **kwargs)
renderer = RendererProxy

View File

@ -0,0 +1,56 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
from powerline.renderers.shell import ShellRenderer
from powerline.renderers.shell.readline import ReadlineRenderer
from powerline.renderers.ipython import IPythonRenderer
class IPythonPre50Renderer(IPythonRenderer, ShellRenderer):
'''Powerline ipython segment renderer for pre-5.0 IPython versions.'''
def render(self, **kwargs):
# XXX super(ShellRenderer), *not* super(IPythonPre50Renderer)
return super(ShellRenderer, self).render(**kwargs)
def do_render(self, segment_info, **kwargs):
segment_info.update(client_id='ipython')
return super(IPythonPre50Renderer, self).do_render(
segment_info=segment_info,
**kwargs
)
class IPythonPromptRenderer(IPythonPre50Renderer, ReadlineRenderer):
'''Powerline ipython prompt (in and in2) renderer'''
pass
class IPythonNonPromptRenderer(IPythonPre50Renderer):
'''Powerline ipython non-prompt (out and rewrite) renderer'''
pass
class RendererProxy(object):
'''Powerline IPython renderer proxy which chooses appropriate renderer
Instantiates two renderer objects: one will be used for prompts and the
other for non-prompts.
'''
def __init__(self, **kwargs):
old_widths = {}
self.non_prompt_renderer = IPythonNonPromptRenderer(old_widths=old_widths, **kwargs)
self.prompt_renderer = IPythonPromptRenderer(old_widths=old_widths, **kwargs)
def render_above_lines(self, *args, **kwargs):
return self.non_prompt_renderer.render_above_lines(*args, **kwargs)
def render(self, is_prompt, *args, **kwargs):
return (self.prompt_renderer if is_prompt else self.non_prompt_renderer).render(
*args, **kwargs)
def shutdown(self, *args, **kwargs):
self.prompt_renderer.shutdown(*args, **kwargs)
self.non_prompt_renderer.shutdown(*args, **kwargs)
renderer = RendererProxy

View File

@ -0,0 +1,152 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import operator
from collections import defaultdict
try:
from __builtin__ import reduce
except ImportError:
from functools import reduce
from pygments.token import Token
from prompt_toolkit.styles import DynamicStyle, Attrs
from IPython.terminal.prompts import Prompts
from powerline.renderers.ipython import IPythonRenderer
from powerline.ipython import IPythonInfo
PowerlinePromptToken = Token.Generic.Prompt.Powerline
# Note: since 2.7 there is dict.__missing__ with same purpose. But in 2.6 one
# must use defaultdict to get __missing__ working.
class PowerlineStyleDict(defaultdict):
'''Dictionary used for getting pygments style for Powerline groups
'''
def __new__(cls, missing_func):
return defaultdict.__new__(cls)
def __init__(self, missing_func):
super(IPythonPygmentsStyle, self).__init__()
self.missing_func = missing_func
def __missing__(self, key):
return self.missing_func(key)
class PowerlinePrompts(Prompts):
'''Class that returns powerline prompts
'''
def __init__(self, old_prompts, powerline):
self.old_prompts = old_prompts
self.shell = old_prompts.shell
self.powerline = powerline
self.last_output_count = None
self.last_output = {}
for prompt in ('in', 'continuation', 'rewrite', 'out'):
exec((
'def {0}_prompt_tokens(self, *args, **kwargs):\n'
' if self.last_output_count != self.shell.execution_count:\n'
' self.last_output.clear()\n'
' self.last_output_count = self.shell.execution_count\n'
' if "{0}" not in self.last_output:\n'
' self.last_output["{0}"] = self.powerline.render('
' side="left",'
' matcher_info="{1}",'
' segment_info=IPythonInfo(self.shell),'
' ) + [(Token.Generic.Prompt, " ")]\n'
' return self.last_output["{0}"]'
).format(prompt, 'in2' if prompt == 'continuation' else prompt))
class PowerlinePromptStyle(DynamicStyle):
def get_attrs_for_token(self, token):
if (
token not in PowerlinePromptToken
or len(token) != len(PowerlinePromptToken) + 1
or not token[-1].startswith('Pl')
or token[-1] == 'Pl'
):
return super(PowerlinePromptStyle, self).get_attrs_for_token(token)
ret = {
'color': None,
'bgcolor': None,
'bold': None,
'underline': None,
'italic': None,
'reverse': False,
'blink': False,
}
for prop in token[-1][3:].split('_'):
if prop[0] == 'a':
ret[prop[1:]] = True
elif prop[0] == 'f':
ret['color'] = prop[1:]
elif prop[0] == 'b':
ret['bgcolor'] = prop[1:]
return Attrs(**ret)
def get_token_to_attributes_dict(self):
dct = super(PowerlinePromptStyle, self).get_token_to_attributes_dict()
def fallback(key):
try:
return dct[key]
except KeyError:
return self.get_attrs_for_token(key)
return PowerlineStyleDict(fallback)
def invalidation_hash(self):
return super(PowerlinePromptStyle, self).invalidation_hash() + 1
class IPythonPygmentsRenderer(IPythonRenderer):
reduce_initial = []
@staticmethod
def hl_join(segments):
return reduce(operator.iadd, segments, [])
def hl(self, contents, fg=None, bg=None, attrs=None):
'''Output highlighted chunk.
This implementation outputs a list containing a single pair
(:py:class:`pygments.token.Token`,
:py:class:`powerline.lib.unicode.unicode`).
'''
guifg = None
guibg = None
attrs = []
if fg is not None and fg is not False:
guifg = fg[1]
if bg is not None and bg is not False:
guibg = bg[1]
if attrs:
attrs = []
if attrs & ATTR_BOLD:
attrs.append('bold')
if attrs & ATTR_ITALIC:
attrs.append('italic')
if attrs & ATTR_UNDERLINE:
attrs.append('underline')
name = (
'Pl'
+ ''.join(('_a' + attr for attr in attrs))
+ (('_f%6x' % guifg) if guifg is not None else '')
+ (('_b%6x' % guibg) if guibg is not None else '')
)
return [(getattr(Token.Generic.Prompt.Powerline, name), contents)]
def hlstyle(self, **kwargs):
return []
def get_client_id(self, segment_info):
return id(self)
renderer = IPythonPygmentsRenderer

View File

@ -13,41 +13,30 @@ def int_to_rgb(num):
return r, g, b
class ShellRenderer(Renderer):
'''Powerline shell segment renderer.'''
escape_hl_start = ''
escape_hl_end = ''
term_truecolor = False
term_escape_style = 'auto'
tmux_escape = False
screen_escape = False
character_translations = Renderer.character_translations.copy()
class PromptRenderer(Renderer):
'''Powerline generic prompt segment renderer'''
def __init__(self, old_widths=None, **kwargs):
super(ShellRenderer, self).__init__(**kwargs)
super(PromptRenderer, self).__init__(**kwargs)
self.old_widths = old_widths if old_widths is not None else {}
def render(self, segment_info, **kwargs):
local_theme = segment_info.get('local_theme')
return super(ShellRenderer, self).render(
matcher_info=local_theme,
segment_info=segment_info,
**kwargs
)
def get_client_id(self, segment_info):
'''Get client ID given segment info
This is used by daemon to correctly cache widths for different clients
using a single renderer instance.
:param dict segment_info:
:ref:`Segment info dictionary <dev-segments-info>`. Out of it only
``client_id`` key is used. It is OK for this dictionary to not
contain this key.
:return: Any hashable value or ``None``.
'''
return segment_info.get('client_id') if isinstance(segment_info, dict) else None
def do_render(self, output_width, segment_info, side, theme, width=None, **kwargs):
if self.term_escape_style == 'auto':
if segment_info['environ'].get('TERM') == 'fbterm':
self.used_term_escape_style = 'fbterm'
else:
self.used_term_escape_style = 'xterm'
else:
self.used_term_escape_style = self.term_escape_style
if isinstance(segment_info, dict):
client_id = segment_info.get('client_id')
else:
client_id = None
client_id = self.get_client_id(segment_info)
if client_id is not None:
local_key = (client_id, side, None if theme is self.theme else id(theme))
key = (client_id, side, None)
@ -70,7 +59,7 @@ class ShellRenderer(Renderer):
width -= self.old_widths[(client_id, 'left', local_key[-1])]
except KeyError:
pass
res = super(ShellRenderer, self).do_render(
res = super(PromptRenderer, self).do_render(
output_width=True,
width=width,
theme=theme,
@ -86,6 +75,36 @@ class ShellRenderer(Renderer):
else:
return ret
class ShellRenderer(PromptRenderer):
'''Powerline shell segment renderer.'''
escape_hl_start = ''
escape_hl_end = ''
term_truecolor = False
term_escape_style = 'auto'
tmux_escape = False
screen_escape = False
character_translations = Renderer.character_translations.copy()
def render(self, segment_info, **kwargs):
local_theme = segment_info.get('local_theme')
return super(ShellRenderer, self).render(
matcher_info=local_theme,
segment_info=segment_info,
**kwargs
)
def do_render(self, segment_info, **kwargs):
if self.term_escape_style == 'auto':
if segment_info['environ'].get('TERM') == 'fbterm':
self.used_term_escape_style = 'fbterm'
else:
self.used_term_escape_style = 'xterm'
else:
self.used_term_escape_style = self.term_escape_style
return super(ShellRenderer, self).do_render(segment_info=segment_info, **kwargs)
def hlstyle(self, fg=None, bg=None, attrs=None):
'''Highlight a segment.