Release 2.5

- Added IPython-5 support: in new major IPython version prompt code was largerly
  reworked, resulting in different set of hacks needed for powerline to work.
  IPython still does not have features needed to avoid using hacks.
- Added stash segment.
- Fixed trailing whitespace segment: it could incorrectly report trailing
  whitespace if line ended with `b` or `'` on Python-3.
- Fixed weather segment and altered geoip service used.
- Fixed escaping used for status-left option on tmux-2.1 and higher.
This commit is contained in:
Foo 2016-07-14 00:43:25 +03:00
commit 6e2e0b2f92
34 changed files with 654 additions and 200 deletions

View File

@ -12,7 +12,7 @@ Awesome, i3 and Qtile.**
* `Support forum`_ (powerline-support@googlegroups.com) * `Support forum`_ (powerline-support@googlegroups.com)
* `Development discussion`_ (powerline-dev@googlegroups.com) * `Development discussion`_ (powerline-dev@googlegroups.com)
.. image:: https://api.travis-ci.org/powerline/powerline.png?branch=develop .. image:: https://api.travis-ci.org/powerline/powerline.svg?branch=develop
:target: `travis-build-status`_ :target: `travis-build-status`_
:alt: Build status :alt: Build status
@ -52,7 +52,7 @@ hassle for me / what happened to the original vim-powerline project / …*
You should check out some of the Powerline derivatives. The most lightweight You should check out some of the Powerline derivatives. The most lightweight
and feature-rich alternative is currently Bailey Lings `vim-airline and feature-rich alternative is currently Bailey Lings `vim-airline
<https://github.com/bling/vim-airline>`_ project. <https://github.com/vim-airline/vim-airline>`_ project.
------ ------
@ -92,4 +92,4 @@ Vim statusline
The font in the screenshots is `Pragmata Pro`_ by Fabrizio Schiavi. The font in the screenshots is `Pragmata Pro`_ by Fabrizio Schiavi.
.. _`Pragmata Pro`: http://www.fsd.it/fonts/pragmatapro.htm .. _`Pragmata Pro`: http://www.fsd.it/shop/fonts/pragmatapro

View File

@ -2,11 +2,19 @@
from __future__ import (unicode_literals, division, absolute_import, print_function) from __future__ import (unicode_literals, division, absolute_import, print_function)
from weakref import ref from weakref import ref
from warnings import warn
from IPython.core.prompts import PromptManager try:
from IPython.core.prompts import PromptManager
has_prompt_manager = True
except ImportError:
has_prompt_manager = False
from IPython.core.magic import Magics, magics_class, line_magic 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
@magics_class @magics_class
@ -23,16 +31,24 @@ class PowerlineMagics(Magics):
raise ValueError('Expected `reload`, but got {0}'.format(line)) raise ValueError('Expected `reload`, but got {0}'.format(line))
class IPythonInfo(object): old_prompt_manager = None
def __init__(self, shell):
self._shell = shell
@property
def prompt_count(self):
return self._shell.execution_count
class PowerlinePromptManager(PromptManager): class ShutdownHook(object):
def __init__(self, ip):
self.powerline = lambda: None
ip.hooks.shutdown_hook.add(self)
def __call__(self):
from IPython.core.hooks import TryNext
powerline = self.powerline()
if powerline is not None:
powerline.shutdown()
raise TryNext()
if has_prompt_manager:
class PowerlinePromptManager(PromptManager):
def __init__(self, powerline, shell): def __init__(self, powerline, shell):
self.powerline = powerline self.powerline = powerline
self.powerline_segment_info = IPythonInfo(shell) self.powerline_segment_info = IPythonInfo(shell)
@ -55,52 +71,53 @@ class PowerlinePromptManager(PromptManager):
else: else:
return ret return ret
class ConfigurableIPythonPowerline(IPythonPowerline):
class ShutdownHook(object):
powerline = lambda: None
def __call__(self):
from IPython.core.hooks import TryNext
powerline = self.powerline()
if powerline is not None:
powerline.shutdown()
raise TryNext()
class ConfigurableIPythonPowerline(IPythonPowerline):
def init(self, ip): def init(self, ip):
config = ip.config.Powerline config = ip.config.Powerline
self.config_overrides = config.get('config_overrides') self.config_overrides = config.get('config_overrides')
self.theme_overrides = config.get('theme_overrides', {}) self.theme_overrides = config.get('theme_overrides', {})
self.config_paths = config.get('config_paths') 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): def do_setup(self, ip, shutdown_hook):
global old_prompt_manager
if old_prompt_manager is None:
old_prompt_manager = ip.prompt_manager
prompt_manager = PowerlinePromptManager( prompt_manager = PowerlinePromptManager(
powerline=self, powerline=self,
shell=ip.prompt_manager.shell, shell=ip.prompt_manager.shell,
) )
ip.prompt_manager = prompt_manager
magics = PowerlineMagics(ip, self) magics = PowerlineMagics(ip, self)
shutdown_hook.powerline = ref(self) shutdown_hook.powerline = ref(self)
ip.prompt_manager = prompt_manager
ip.register_magics(magics) ip.register_magics(magics)
old_prompt_manager = None
def load_ipython_extension(ip): def load_ipython_extension(ip):
global old_prompt_manager if has_prompt_manager:
old_prompt_manager = ip.prompt_manager shutdown_hook = ShutdownHook(ip)
powerline = ConfigurableIPythonPowerline(ip) powerline = ConfigurableIPythonPowerline(ip)
shutdown_hook = ShutdownHook()
powerline.setup(ip, shutdown_hook) powerline.setup(ip, shutdown_hook)
else:
ip.hooks.shutdown_hook.add(shutdown_hook) from powerline.bindings.ipython.since_5 import PowerlinePrompts
ip.prompts_class = PowerlinePrompts
ip.prompts = PowerlinePrompts(ip)
warn(DeprecationWarning(
'post_0_11 extension is deprecated since IPython 5, use\n'
' from powerline.bindings.ipython.since_5 import PowerlinePrompts\n'
' c.TerminalInteractiveShell.prompts_class = PowerlinePrompts\n'
))
def unload_ipython_extension(ip): def unload_ipython_extension(ip):
global old_prompt_manager
if old_prompt_manager is not None:
ip.prompt_manager = old_prompt_manager ip.prompt_manager = old_prompt_manager
old_prompt_manager = None

View File

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

View File

@ -0,0 +1,79 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
from weakref import ref
from IPython.terminal.prompts import Prompts
from pygments.token import Token # NOQA
from powerline.ipython import IPythonPowerline
from powerline.renderers.ipython.since_5 import PowerlinePromptStyle
from powerline.bindings.ipython.post_0_11 import PowerlineMagics, ShutdownHook
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(
renderer_module='.since_5')
def do_setup(self, ip, prompts, shutdown_hook):
prompts.powerline = self
saved_msfn = ip._make_style_from_name
if hasattr(saved_msfn, 'powerline_original'):
saved_msfn = saved_msfn.powerline_original
def _make_style_from_name(ip, name):
prev_style = saved_msfn(name)
new_style = PowerlinePromptStyle(lambda: prev_style)
return new_style
_make_style_from_name.powerline_original = saved_msfn
if not isinstance(ip._style, PowerlinePromptStyle):
prev_style = ip._style
ip._style = PowerlinePromptStyle(lambda: prev_style)
if not isinstance(saved_msfn, type(self.init)):
_saved_msfn = saved_msfn
saved_msfn = lambda: _saved_msfn(ip)
ip._make_style_from_name = _make_style_from_name
magics = PowerlineMagics(ip, self)
ip.register_magics(magics)
if shutdown_hook:
shutdown_hook.powerline = ref(self)
class PowerlinePrompts(Prompts):
'''Class that returns powerline prompts
'''
def __init__(self, shell):
shutdown_hook = ShutdownHook(shell)
powerline = ConfigurableIPythonPowerline(shell)
self.shell = shell
powerline.do_setup(shell, self, shutdown_hook)
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=self.shell,'
' ) + [(Token.Generic.Prompt, " ")]\n'
' return self.last_output["{0}"]'
).format(prompt, 'in2' if prompt == 'continuation' else prompt))

View File

@ -0,0 +1,2 @@
# Starting from tmux-2.1 escaping of dollar signs inside #() is harmful
set -qg status-left "#{?client_prefix,#[fg=$_POWERLINE_SESSION_PREFIX_FG]#[bg=$_POWERLINE_SESSION_PREFIX_BG]#[$_POWERLINE_SESSION_PREFIX_ATTR],#[fg=$_POWERLINE_SESSION_FG]#[bg=$_POWERLINE_SESSION_BG]#[$_POWERLINE_SESSION_ATTR]} #S #{?client_prefix,#[fg=$_POWERLINE_SESSION_PREFIX_BG],#[fg=$_POWERLINE_SESSION_BG]}#[bg=$_POWERLINE_BACKGROUND_BG]#[nobold]$_POWERLINE_LEFT_HARD_DIVIDER#(env $POWERLINE_COMMAND $POWERLINE_COMMAND_ARGS tmux left --width=`tmux display -p '#{client_width}'` -R width_adjust=`tmux show-options -g status-right-length | cut -d' ' -f2` -R pane_id=\"`tmux display -p '#D'`\")"

View File

@ -43,6 +43,8 @@
"branch_dirty": { "fg": "brightyellow", "bg": "gray2", "attrs": [] }, "branch_dirty": { "fg": "brightyellow", "bg": "gray2", "attrs": [] },
"branch_clean": { "fg": "gray9", "bg": "gray2", "attrs": [] }, "branch_clean": { "fg": "gray9", "bg": "gray2", "attrs": [] },
"branch:divider": { "fg": "gray7", "bg": "gray2", "attrs": [] }, "branch:divider": { "fg": "gray7", "bg": "gray2", "attrs": [] },
"stash": "branch_dirty",
"stash:divider": "branch:divider",
"cwd": "information:additional", "cwd": "information:additional",
"cwd:current_folder": "information:regular", "cwd:current_folder": "information:regular",
"cwd:divider": { "fg": "gray7", "bg": "gray4", "attrs": [] }, "cwd:divider": { "fg": "gray7", "bg": "gray4", "attrs": [] },

View File

@ -15,6 +15,7 @@
"branch": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, "branch": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] },
"branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": [] }, "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": [] },
"branch_clean": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, "branch_clean": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] },
"stash": "branch_dirty",
"email_alert_gradient": { "fg": "solarized:base3", "bg": "yellow_orange_red", "attrs": [] }, "email_alert_gradient": { "fg": "solarized:base3", "bg": "yellow_orange_red", "attrs": [] },
"email_alert": "warning:regular", "email_alert": "warning:regular",
"cwd": "information:additional", "cwd": "information:additional",

View File

@ -12,6 +12,7 @@
"readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base01", "attrs": [] }, "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base01", "attrs": [] },
"branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base01", "attrs": [] }, "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base01", "attrs": [] },
"branch:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attrs": [] }, "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attrs": [] },
"stash:divider": "branch:divider",
"file_name": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, "file_name": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] },
"window_title": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, "window_title": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] },
"file_name_no_file": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, "file_name_no_file": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] },

View File

@ -12,6 +12,8 @@
"readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] }, "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] },
"branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": [] }, "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": [] },
"branch:divider": { "fg": "solarized:base1", "bg": "solarized:base2", "attrs": [] }, "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base2", "attrs": [] },
"stash": "branch_dirty",
"stash:divider": "branch:divider",
"file_name": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] }, "file_name": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] },
"window_title": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, "window_title": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] },
"file_size": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, "file_size": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] },

View File

@ -15,6 +15,9 @@
"branch": { "branch": {
"before": "BR " "before": "BR "
}, },
"stash": {
"before": "ST "
},
"cwd": { "cwd": {
"args": { "args": {
"ellipsis": "..." "ellipsis": "..."

View File

@ -14,6 +14,9 @@
"branch": { "branch": {
"before": " " "before": " "
}, },
"stash": {
"before": "⌆ "
},
"cwd": { "cwd": {
"args": { "args": {
"ellipsis": "⋯" "ellipsis": "⋯"

View File

@ -14,6 +14,9 @@
"branch": { "branch": {
"before": "🔀 " "before": "🔀 "
}, },
"stash": {
"before": "📝"
},
"cwd": { "cwd": {
"args": { "args": {
"ellipsis": "⋯" "ellipsis": "⋯"

View File

@ -30,6 +30,10 @@
"function": "powerline.segments.shell.last_pipe_status", "function": "powerline.segments.shell.last_pipe_status",
"priority": 10 "priority": 10
}, },
{
"function": "powerline.segments.common.vcs.stash",
"priority": 50
},
{ {
"function": "powerline.segments.common.vcs.branch", "function": "powerline.segments.common.vcs.branch",
"priority": 40 "priority": 40

View File

@ -14,6 +14,9 @@
"branch": { "branch": {
"before": "⎇ " "before": "⎇ "
}, },
"stash": {
"before": "⌆"
},
"cwd": { "cwd": {
"args": { "args": {
"ellipsis": "⋯" "ellipsis": "⋯"

View File

@ -14,6 +14,9 @@
"branch": { "branch": {
"before": "BR " "before": "BR "
}, },
"stash": {
"before": "ST "
},
"cwd": { "cwd": {
"args": { "args": {
"ellipsis": "…" "ellipsis": "…"

View File

@ -14,6 +14,9 @@
"branch": { "branch": {
"before": "B " "before": "B "
}, },
"stash": {
"before": "S "
},
"cwd": { "cwd": {
"args": { "args": {
"use_path_separator": true, "use_path_separator": true,

View File

@ -6,6 +6,15 @@ from powerline.lib.dict import mergedicts
from powerline.lib.unicode import string 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 # HACK: ipython tries to only leave us with plain ASCII
class RewriteResult(object): class RewriteResult(object):
def __init__(self, prompt): def __init__(self, prompt):

View File

@ -102,6 +102,13 @@ try:
def ignore_event(path, name): def ignore_event(path, name):
return False return False
def stash(self):
try:
stashref = git.Repository(git_directory(self.directory)).lookup_reference('refs/stash')
except KeyError:
return 0
return sum(1 for _ in stashref.log())
def do_status(self, directory, path): def do_status(self, directory, path):
if path: if path:
try: try:
@ -171,6 +178,9 @@ except ImportError:
def _gitcmd(self, directory, *args): def _gitcmd(self, directory, *args):
return readlines(('git',) + args, directory) return readlines(('git',) + args, directory)
def stash(self):
return sum(1 for _ in self._gitcmd(self.directory, 'stash', 'list'))
def do_status(self, directory, path): def do_status(self, directory, path):
if path: if path:
try: try:

View File

@ -4,6 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct
import sys import sys
import os import os
import re import re
import operator
from itertools import chain 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): 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 '''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 # No width specified, so we dont need to crop or pad anything
if output_width: if output_width:
current_width = self._render_length(theme, segments, self.compute_divider_widths(theme)) 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'] segment['_rendered_hl']
for segment in self._render_segments(theme, segments) for segment in self._render_segments(theme, segments)
]) + self.hlstyle(), segments, current_width, output_raw, output_width) ]) + self.hlstyle(), segments, current_width, output_raw, output_width)
@ -378,7 +392,10 @@ class Renderer(object):
elif output_width: elif output_width:
current_width = self._render_length(theme, segments, divider_widths) 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: if rendered_highlighted:
rendered_highlighted += self.hlstyle() rendered_highlighted += self.hlstyle()

View File

@ -1,12 +1,11 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function) 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.theme import Theme
from powerline.renderers.shell import PromptRenderer
class IPythonRenderer(ShellRenderer): class IPythonRenderer(PromptRenderer):
'''Powerline ipython segment renderer.''' '''Powerline ipython segment renderer.'''
def get_segment_info(self, segment_info, mode): def get_segment_info(self, segment_info, mode):
r = self.segment_info.copy() r = self.segment_info.copy()
@ -33,50 +32,3 @@ class IPythonRenderer(ShellRenderer):
for match in self.local_themes.values(): for match in self.local_themes.values():
if 'theme' in match: if 'theme' in match:
match['theme'].shutdown() 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,130 @@
# 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 powerline.renderers.ipython import IPythonRenderer
from powerline.ipython import IPythonInfo
from powerline.colorscheme import ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE
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(PowerlineStyleDict, self).__init__()
self.missing_func = missing_func
def __missing__(self, key):
return self.missing_func(key)
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 = []
def get_segment_info(self, segment_info, mode):
return super(IPythonPygmentsRenderer, self).get_segment_info(
IPythonInfo(segment_info), mode)
@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 return r, g, b
class ShellRenderer(Renderer): class PromptRenderer(Renderer):
'''Powerline shell segment renderer.''' '''Powerline generic prompt 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 __init__(self, old_widths=None, **kwargs): 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 {} self.old_widths = old_widths if old_widths is not None else {}
def render(self, segment_info, **kwargs): def get_client_id(self, segment_info):
local_theme = segment_info.get('local_theme') '''Get client ID given segment info
return super(ShellRenderer, self).render(
matcher_info=local_theme, This is used by daemon to correctly cache widths for different clients
segment_info=segment_info, using a single renderer instance.
**kwargs
) :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): def do_render(self, output_width, segment_info, side, theme, width=None, **kwargs):
if self.term_escape_style == 'auto': client_id = self.get_client_id(segment_info)
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
if client_id is not None: if client_id is not None:
local_key = (client_id, side, None if theme is self.theme else id(theme)) local_key = (client_id, side, None if theme is self.theme else id(theme))
key = (client_id, side, None) key = (client_id, side, None)
@ -70,7 +59,7 @@ class ShellRenderer(Renderer):
width -= self.old_widths[(client_id, 'left', local_key[-1])] width -= self.old_widths[(client_id, 'left', local_key[-1])]
except KeyError: except KeyError:
pass pass
res = super(ShellRenderer, self).do_render( res = super(PromptRenderer, self).do_render(
output_width=True, output_width=True,
width=width, width=width,
theme=theme, theme=theme,
@ -86,6 +75,36 @@ class ShellRenderer(Renderer):
else: else:
return ret 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): def hlstyle(self, fg=None, bg=None, attrs=None):
'''Highlight a segment. '''Highlight a segment.

View File

@ -56,3 +56,34 @@ branch = with_docstring(BranchSegment(),
Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``.
''') ''')
@requires_filesystem_watcher
@requires_segment_info
class StashSegment(Segment):
divider_highlight_group = None
@staticmethod
def get_directory(segment_info):
return segment_info['getcwd']()
def __call__(self, pl, segment_info, create_watcher):
name = self.get_directory(segment_info)
if name:
repo = guess(path=name, create_watcher=create_watcher)
if repo is not None:
stash = getattr(repo, 'stash', None)
if stash:
stashes = stash()
if stashes:
return [{
'contents': str(stashes),
'highlight_groups': ['stash'],
'divider_highlight_group': self.divider_highlight_group
}]
stash = with_docstring(StashSegment(),
'''Return the number of current VCS stash entries, if any.
Highlight groups used: ``stash``.
''')

View File

@ -115,19 +115,20 @@ class WeatherSegment(KwThreadedSegment):
return self.location_urls[location_query] return self.location_urls[location_query]
except KeyError: except KeyError:
if location_query is None: if location_query is None:
location_data = json.loads(urllib_read('http://freegeoip.net/json/')) location_data = json.loads(urllib_read('http://geoip.nekudo.com/api/'))
location = ','.join(( location = ','.join((
location_data['city'], location_data['city'],
location_data['region_name'], location_data['country']['name'],
location_data['country_code'] location_data['country']['code']
)) ))
self.info('Location returned by freegeoip is {0}', location) self.info('Location returned by nekudo is {0}', location)
else: else:
location = location_query location = location_query
query_data = { query_data = {
'q': 'q':
'use "https://raw.githubusercontent.com/yql/yql-tables/master/weather/weather.bylocation.xml" as we;' 'use "https://raw.githubusercontent.com/yql/yql-tables/master/weather/weather.bylocation.xml" as we;'
'select * from we where location="{0}" and unit="c"'.format(location).encode('utf-8'), 'select * from weather.forecast where woeid in'
' (select woeid from geo.places(1) where text="{0}") and u="c"'.format(location).encode('utf-8'),
'format': 'json', 'format': 'json',
} }
self.location_urls[location_query] = url = ( self.location_urls[location_query] = url = (
@ -143,7 +144,7 @@ class WeatherSegment(KwThreadedSegment):
response = json.loads(raw_response) response = json.loads(raw_response)
try: try:
condition = response['query']['results']['weather']['rss']['channel']['item']['condition'] condition = response['query']['results']['channel']['item']['condition']
condition_code = int(condition['code']) condition_code = int(condition['code'])
temp = float(condition['temp']) temp = float(condition['temp'])
except (KeyError, ValueError): except (KeyError, ValueError):
@ -203,7 +204,7 @@ class WeatherSegment(KwThreadedSegment):
weather = with_docstring(WeatherSegment(), weather = with_docstring(WeatherSegment(),
'''Return weather from Yahoo! Weather. '''Return weather from Yahoo! Weather.
Uses GeoIP lookup from http://freegeoip.net/ to automatically determine Uses GeoIP lookup from http://geoip.nekudo.com to automatically determine
your current location. This should be changed if youre in a VPN or if your your current location. This should be changed if youre in a VPN or if your
IP address is registered at another location. IP address is registered at another location.

View File

@ -22,7 +22,7 @@ from powerline.lib import add_divider_highlight_group
from powerline.lib.vcs import guess from powerline.lib.vcs import guess
from powerline.lib.humanize_bytes import humanize_bytes from powerline.lib.humanize_bytes import humanize_bytes
from powerline.lib import wraps_saveargs as wraps from powerline.lib import wraps_saveargs as wraps
from powerline.segments.common.vcs import BranchSegment from powerline.segments.common.vcs import BranchSegment, StashSegment
from powerline.segments import with_docstring from powerline.segments import with_docstring
from powerline.lib.unicode import string, unicode from powerline.lib.unicode import string, unicode
@ -510,6 +510,25 @@ Divider highlight group used: ``branch:divider``.
''') ''')
@requires_filesystem_watcher
@requires_segment_info
class VimStashSegment(StashSegment):
divider_highlight_group = 'stash:divider'
@staticmethod
def get_directory(segment_info):
if vim_getbufoption(segment_info, 'buftype'):
return None
return buffer_name(segment_info)
stash = with_docstring(VimStashSegment(),
'''Return the number of stashes in the current working branch.
Highlight groups used: ``stash``.
''')
@requires_filesystem_watcher @requires_filesystem_watcher
@requires_segment_info @requires_segment_info
def file_vcs_status(pl, segment_info, create_watcher): def file_vcs_status(pl, segment_info, create_watcher):
@ -559,7 +578,7 @@ def trailing_whitespace(pl, segment_info):
else: else:
buf = segment_info['buffer'] buf = segment_info['buffer']
bws = b' \t' bws = b' \t'
sws = str(bws) sws = str(' \t') # Ignore unicode_literals and use native str.
for i in range(len(buf)): for i in range(len(buf)):
try: try:
line = buf[i] line = buf[i]

View File

@ -231,7 +231,7 @@ def do_one(sock, read_sockets, write_sockets, result_map):
def main_loop(sock): def main_loop(sock):
sock.listen(1) sock.listen(128)
sock.setblocking(0) sock.setblocking(0)
read_sockets, write_sockets = set(), set() read_sockets, write_sockets = set(), set()

View File

@ -59,7 +59,7 @@ else:
def get_version(): def get_version():
base_version = '2.4' base_version = '2.5'
base_version += '.dev9999' base_version += '.dev9999'
try: try:
return base_version + '+git.' + str(subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()) return base_version + '+git.' + str(subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip())
@ -70,7 +70,7 @@ def get_version():
setup( setup(
name='powerline-status', name='powerline-status',
version='2.4', version='2.5',
description='The ultimate statusline/prompt utility.', description='The ultimate statusline/prompt utility.',
long_description=README, long_description=README,
classifiers=[ classifiers=[

View File

@ -24,12 +24,19 @@ exit_suite() {
} }
fail() { fail() {
local allow_failure=
if test "x$1" = "x--allow-failure" ; then
shift
allow_failure=A
fi
local test_name="$1" local test_name="$1"
local fail_char="$2" local fail_char="$allow_failure$2"
local message="$3" local message="$3"
local full_msg="$fail_char $POWERLINE_CURRENT_SUITE|$test_name :: $message" local full_msg="$fail_char $POWERLINE_CURRENT_SUITE|$test_name :: $message"
FAIL_SUMMARY="${FAIL_SUMMARY}${NL}${full_msg}" FAIL_SUMMARY="${FAIL_SUMMARY}${NL}${full_msg}"
echo "Failed: $full_msg" echo "Failed: $full_msg"
echo "$full_msg" >> tests/failures echo "$full_msg" >> tests/failures
if test "x$allow_failure" = "x" ; then
FAILED=1 FAILED=1
fi
} }

View File

@ -44,13 +44,13 @@ def urllib_read(query_url):
return '127.0.0.1' return '127.0.0.1'
elif query_url.startswith('http://ipv4.icanhazip.com'): elif query_url.startswith('http://ipv4.icanhazip.com'):
return '2001:4801:7818:6:abc5:ba2c:ff10:275f' return '2001:4801:7818:6:abc5:ba2c:ff10:275f'
elif query_url.startswith('http://freegeoip.net/json/'): elif query_url.startswith('http://geoip.nekudo.com/api/'):
return '{"city": "Meppen", "region_code": "06", "region_name": "Niedersachsen", "areacode": "", "ip": "82.145.55.16", "zipcode": "49716", "longitude": 7.3167, "country_name": "Germany", "country_code": "DE", "metrocode": "", "latitude": 52.6833}' return '{"city":"Meppen","country":{"name":"Germany", "code":"DE"},"location":{"accuracy_radius":100,"latitude":52.6833,"longitude":7.3167,"time_zone":"Europe\/Berlin"},"ip":"82.145.55.16"}'
elif query_url.startswith('http://query.yahooapis.com/v1/public/'): elif query_url.startswith('http://query.yahooapis.com/v1/public/'):
if 'Meppen' in query_url: if 'Meppen' in query_url:
return r'{"query":{"count":1,"created":"2013-03-02T13:20:22Z","lang":"en-US","results":{"weather":{"rss":{"version":"2.0","geo":"http://www.w3.org/2003/01/geo/wgs84_pos#","yweather":"http://xml.weather.yahoo.com/ns/rss/1.0","channel":{"title":"Yahoo! Weather - Russia, RU","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","description":"Yahoo! Weather for Russia, RU","language":"en-us","lastBuildDate":"Sat, 02 Mar 2013 4:58 pm MSK","ttl":"60","location":{"city":"Russia","country":"Russia","region":""},"units":{"distance":"km","pressure":"mb","speed":"km/h","temperature":"C"},"wind":{"chill":"-9","direction":"0","speed":""},"atmosphere":{"humidity":"94","pressure":"1006.1","rising":"0","visibility":""},"astronomy":{"sunrise":"10:04 am","sunset":"7:57 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Russia, RU at 4:58 pm MSK","lat":"59.45","long":"108.83","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","pubDate":"Sat, 02 Mar 2013 4:58 pm MSK","condition":{"code":"30","date":"Sat, 02 Mar 2013 4:58 pm MSK","temp":"-9","text":"Partly Cloudy"},"description":"<img src=\"http://l.yimg.com/a/i/us/we/52/30.gif\"/><br />\n<b>Current Conditions:</b><br />\nPartly Cloudy, -9 C<BR />\n<BR /><b>Forecast:</b><BR />\nSat - Partly Cloudy. High: -9 Low: -19<br />\nSun - Partly Cloudy. High: -12 Low: -18<br />\n<br />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html\">Full Forecast at Yahoo! Weather</a><BR/><BR/>\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel</a>)<br/>","forecast":[{"code":"29","date":"2 Mar 2013","day":"Sat","high":"-9","low":"-19","text":"Partly Cloudy"},{"code":"30","date":"3 Mar 2013","day":"Sun","high":"-12","low":"-18","text":"Partly Cloudy"}],"guid":{"isPermaLink":"false","content":"RSXX1511_2013_03_03_7_00_MSK"}}}}}}}}' return r'{"query":{"count":1,"created":"2016-05-13T19:43:18Z","lang":"en-US","results":{"channel":{"units":{"distance":"mi","pressure":"in","speed":"mph","temperature":"C"},"title":"Yahoo! Weather - Meppen, NI, DE","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","description":"Yahoo! Weather for Meppen, NI, DE","language":"en-us","lastBuildDate":"Fri, 13 May 2016 09:43 PM CEST","ttl":"60","location":{"city":"Meppen","country":"Germany","region":" NI"},"wind":{"chill":"55","direction":"350","speed":"25"},"atmosphere":{"humidity":"57","pressure":"1004.0","rising":"0","visibility":"16.1"},"astronomy":{"sunrise":"5:35 am","sunset":"9:21 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Meppen, NI, DE at 08:00 PM CEST","lat":"52.68993","long":"7.29115","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","pubDate":"Fri, 13 May 2016 08:00 PM CEST","condition":{"code":"23","date":"Fri, 13 May 2016 08:00 PM CEST","temp":"14","text":"Breezy"},"forecast":[{"code":"30","date":"13 May 2016","day":"Fri","high":"71","low":"48","text":"Partly Cloudy"},{"code":"28","date":"14 May 2016","day":"Sat","high":"54","low":"44","text":"Mostly Cloudy"},{"code":"11","date":"15 May 2016","day":"Sun","high":"55","low":"43","text":"Showers"},{"code":"28","date":"16 May 2016","day":"Mon","high":"54","low":"42","text":"Mostly Cloudy"},{"code":"28","date":"17 May 2016","day":"Tue","high":"57","low":"43","text":"Mostly Cloudy"},{"code":"12","date":"18 May 2016","day":"Wed","high":"62","low":"45","text":"Rain"},{"code":"28","date":"19 May 2016","day":"Thu","high":"63","low":"48","text":"Mostly Cloudy"},{"code":"28","date":"20 May 2016","day":"Fri","high":"67","low":"50","text":"Mostly Cloudy"},{"code":"30","date":"21 May 2016","day":"Sat","high":"71","low":"50","text":"Partly Cloudy"},{"code":"30","date":"22 May 2016","day":"Sun","high":"74","low":"54","text":"Partly Cloudy"}],"description":"<![CDATA[<img src=\"http://l.yimg.com/a/i/us/we/52/23.gif\"/>\n<BR />\n<b>Current Conditions:</b>\n<BR />Breezy\n<BR />\n<BR />\n<b>Forecast:</b>\n<BR /> Fri - Partly Cloudy. High: 71Low: 48\n<BR /> Sat - Mostly Cloudy. High: 54Low: 44\n<BR /> Sun - Showers. High: 55Low: 43\n<BR /> Mon - Mostly Cloudy. High: 54Low: 42\n<BR /> Tue - Mostly Cloudy. High: 57Low: 43\n<BR />\n<BR />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/\">Full Forecast at Yahoo! Weather</a>\n<BR />\n<BR />\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel</a>)\n<BR />\n]]>","guid":{"isPermaLink":"false"}}}}}}'
elif 'Moscow' in query_url: elif 'Moscow' in query_url:
return r'{"query":{"count":1,"created":"2013-03-02T13:20:22Z","lang":"en-US","results":{"weather":{"rss":{"version":"2.0","geo":"http://www.w3.org/2003/01/geo/wgs84_pos#","yweather":"http://xml.weather.yahoo.com/ns/rss/1.0","channel":{"title":"Yahoo! Weather - Russia, RU","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","description":"Yahoo! Weather for Russia, RU","language":"en-us","lastBuildDate":"Sat, 02 Mar 2013 4:58 pm MSK","ttl":"60","location":{"city":"Russia","country":"Russia","region":""},"units":{"distance":"km","pressure":"mb","speed":"km/h","temperature":"C"},"wind":{"chill":"-9","direction":"0","speed":""},"atmosphere":{"humidity":"94","pressure":"1006.1","rising":"0","visibility":""},"astronomy":{"sunrise":"10:04 am","sunset":"7:57 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Russia, RU at 4:58 pm MSK","lat":"59.45","long":"108.83","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","pubDate":"Sat, 02 Mar 2013 4:58 pm MSK","condition":{"code":"30","date":"Sat, 02 Mar 2013 4:58 pm MSK","temp":"19","text":"Partly Cloudy"},"description":"<img src=\"http://l.yimg.com/a/i/us/we/52/30.gif\"/><br />\n<b>Current Conditions:</b><br />\nPartly Cloudy, -9 C<BR />\n<BR /><b>Forecast:</b><BR />\nSat - Partly Cloudy. High: -9 Low: -19<br />\nSun - Partly Cloudy. High: -12 Low: -18<br />\n<br />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html\">Full Forecast at Yahoo! Weather</a><BR/><BR/>\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel</a>)<br/>","forecast":[{"code":"29","date":"2 Mar 2013","day":"Sat","high":"-9","low":"-19","text":"Partly Cloudy"},{"code":"30","date":"3 Mar 2013","day":"Sun","high":"-12","low":"-18","text":"Partly Cloudy"}],"guid":{"isPermaLink":"false","content":"RSXX1511_2013_03_03_7_00_MSK"}}}}}}}}' return r'{"query":{"count":1,"created":"2016-05-13T19:47:01Z","lang":"en-US","results":{"channel":{"units":{"distance":"mi","pressure":"in","speed":"mph","temperature":"C"},"title":"Yahoo! Weather - Moscow, Moscow Federal City, RU","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-2122265/","description":"Yahoo! Weather for Moscow, Moscow Federal City, RU","language":"en-us","lastBuildDate":"Fri, 13 May 2016 10:47 PM MSK","ttl":"60","location":{"city":"Moscow","country":"Russia","region":" Moscow Federal City"},"wind":{"chill":"45","direction":"80","speed":"11"},"atmosphere":{"humidity":"52","pressure":"993.0","rising":"0","visibility":"16.1"},"astronomy":{"sunrise":"4:19 am","sunset":"8:34 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Moscow, Moscow Federal City, RU at 09:00 PM MSK","lat":"55.741638","long":"37.605061","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-2122265/","pubDate":"Fri, 13 May 2016 09:00 PM MSK","condition":{"code":"33","date":"Fri, 13 May 2016 09:00 PM MSK","temp":"9","text":"Mostly Clear"},"forecast":[{"code":"30","date":"13 May 2016","day":"Fri","high":"62","low":"41","text":"Partly Cloudy"},{"code":"30","date":"14 May 2016","day":"Sat","high":"64","low":"43","text":"Partly Cloudy"},{"code":"30","date":"15 May 2016","day":"Sun","high":"63","low":"44","text":"Partly Cloudy"},{"code":"12","date":"16 May 2016","day":"Mon","high":"60","low":"47","text":"Rain"},{"code":"12","date":"17 May 2016","day":"Tue","high":"64","low":"48","text":"Rain"},{"code":"28","date":"18 May 2016","day":"Wed","high":"67","low":"48","text":"Mostly Cloudy"},{"code":"12","date":"19 May 2016","day":"Thu","high":"68","low":"49","text":"Rain"},{"code":"39","date":"20 May 2016","day":"Fri","high":"66","low":"50","text":"Scattered Showers"},{"code":"39","date":"21 May 2016","day":"Sat","high":"69","low":"49","text":"Scattered Showers"},{"code":"30","date":"22 May 2016","day":"Sun","high":"73","low":"50","text":"Partly Cloudy"}],"description":"<![CDATA[<img src=\"http://l.yimg.com/a/i/us/we/52/33.gif\"/>\n<BR />\n<b>Current Conditions:</b>\n<BR />Mostly Clear\n<BR />\n<BR />\n<b>Forecast:</b>\n<BR /> Fri - Partly Cloudy. High: 62Low: 41\n<BR /> Sat - Partly Cloudy. High: 64Low: 43\n<BR /> Sun - Partly Cloudy. High: 63Low: 44\n<BR /> Mon - Rain. High: 60Low: 47\n<BR /> Tue - Rain. High: 64Low: 48\n<BR />\n<BR />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-2122265/\">Full Forecast at Yahoo! Weather</a>\n<BR />\n<BR />\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel</a>)\n<BR />\n]]>","guid":{"isPermaLink":"false"}}}}}}'
else: else:
raise NotImplementedError raise NotImplementedError

View File

@ -566,6 +566,32 @@ class TestVCS(TestCase):
self.do_branch_rename_test(repo, lambda b: re.match(r'^[a-f0-9]+$', b)) self.do_branch_rename_test(repo, lambda b: re.match(r'^[a-f0-9]+$', b))
finally: finally:
call(['git', 'checkout', '-q', 'master'], cwd=GIT_REPO) call(['git', 'checkout', '-q', 'master'], cwd=GIT_REPO)
# Test stashing
self.assertEqual(repo.stash(), 0)
def stash_save():
with open(os.path.join(GIT_REPO, 'file'), 'w') as f:
f.write('abc')
return call(['git', 'stash', '-u'], cwd=GIT_REPO, stdout=PIPE)
def stash_drop():
return call(['git', 'stash', 'drop'], cwd=GIT_REPO, stdout=PIPE)
def stash_list():
return call(['git', 'stash', 'list'], cwd=GIT_REPO, stdout=PIPE)
try:
stash_save()
self.assertEqual(repo.stash(), 1)
stash_save()
self.assertEqual(repo.stash(), 2)
stash_drop()
self.assertEqual(repo.stash(), 1)
stash_drop()
self.assertEqual(repo.stash(), 0)
finally:
while stash_list():
stash_drop()
def test_git_sym(self): def test_git_sym(self):
create_watcher = get_fallback_create_watcher() create_watcher = get_fallback_create_watcher()

View File

@ -154,11 +154,11 @@ class TestConfig(TestCase):
segment_info = Args(prompt_count=1) segment_info = Args(prompt_count=1)
with IpyPowerline(logger=get_logger()) as powerline: with IpyPowerline(logger=get_logger(), renderer_module='.pre_5') as powerline:
for prompt_type in ['in', 'in2']: for prompt_type in ['in', 'in2']:
powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info) powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info)
powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info) powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info)
with IpyPowerline(logger=get_logger()) as powerline: with IpyPowerline(logger=get_logger(), renderer_module='.pre_5') as powerline:
for prompt_type in ['out', 'rewrite']: for prompt_type in ['out', 'rewrite']:
powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info) powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info)
powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info) powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info)

View File

@ -738,6 +738,29 @@ class TestVcs(TestCommon):
'divider_highlight_group': None 'divider_highlight_group': None
}]) }])
def test_stash(self):
pl = Pl()
create_watcher = get_fallback_create_watcher()
stash = partial(self.module.stash, pl=pl, create_watcher=create_watcher, segment_info={'getcwd': os.getcwd})
def forge_stash(n):
return replace_attr(self.module, 'guess', get_dummy_guess(stash=lambda: n, directory='/tmp/tests'))
with forge_stash(0):
self.assertEqual(stash(), None)
with forge_stash(1):
self.assertEqual(stash(), [{
'highlight_groups': ['stash'],
'contents': '1',
'divider_highlight_group': None
}])
with forge_stash(2):
self.assertEqual(stash(), [{
'highlight_groups': ['stash'],
'contents': '2',
'divider_highlight_group': None
}])
class TestTime(TestCommon): class TestTime(TestCommon):
module_name = 'time' module_name = 'time'
@ -844,46 +867,46 @@ class TestWthr(TestCommon):
pl = Pl() pl = Pl()
with replace_attr(self.module, 'urllib_read', urllib_read): with replace_attr(self.module, 'urllib_read', urllib_read):
self.assertEqual(self.module.weather(pl=pl), [ self.assertEqual(self.module.weather(pl=pl), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, temp_coldest=0, temp_hottest=100), [ self.assertEqual(self.module.weather(pl=pl, temp_coldest=0, temp_hottest=100), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 14.0}
]) ])
self.assertEqual(self.module.weather(pl=pl, temp_coldest=-100, temp_hottest=-50), [ self.assertEqual(self.module.weather(pl=pl, temp_coldest=-100, temp_hottest=-50), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 100} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 100}
]) ])
self.assertEqual(self.module.weather(pl=pl, icons={'cloudy': 'o'}), [ self.assertEqual(self.module.weather(pl=pl, icons={'blustery': 'o'}), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'o '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'o '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, icons={'partly_cloudy_day': 'x'}), [ self.assertEqual(self.module.weather(pl=pl, icons={'windy': 'x'}), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'x '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'x '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, unit='F'), [ self.assertEqual(self.module.weather(pl=pl, unit='F'), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '16°F', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '57°F', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, unit='K'), [ self.assertEqual(self.module.weather(pl=pl, unit='K'), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '264K', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '287K', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, temp_format='{temp:.1e}C'), [ self.assertEqual(self.module.weather(pl=pl, temp_format='{temp:.1e}C'), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9.0e+00C', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '1.4e+01C', 'gradient_level': 62.857142857142854}
]) ])
with replace_attr(self.module, 'urllib_read', urllib_read): with replace_attr(self.module, 'urllib_read', urllib_read):
self.module.weather.startup(pl=pl, location_query='Meppen,06,DE') self.module.weather.startup(pl=pl, location_query='Meppen,06,DE')
self.assertEqual(self.module.weather(pl=pl), [ self.assertEqual(self.module.weather(pl=pl), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'WINDY '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 62.857142857142854}
]) ])
self.assertEqual(self.module.weather(pl=pl, location_query='Moscow,RU'), [ self.assertEqual(self.module.weather(pl=pl, location_query='Moscow,RU'), [
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_fair_night', 'weather_condition_night', 'weather_conditions', 'weather'], 'contents': 'NIGHT '},
{'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '19°C', 'gradient_level': 70.0} {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '9°C', 'gradient_level': 55.714285714285715}
]) ])
self.module.weather.shutdown() self.module.weather.shutdown()
@ -1390,6 +1413,30 @@ class TestVim(TestCase):
{'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_clean', 'branch'], 'contents': 'foo'} {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_clean', 'branch'], 'contents': 'foo'}
]) ])
def test_stash(self):
pl = Pl()
create_watcher = get_fallback_create_watcher()
with vim_module._with('buffer', '/foo') as segment_info:
stash = partial(self.vim.stash, pl=pl, create_watcher=create_watcher, segment_info=segment_info)
def forge_stash(n):
return replace_attr(self.vcs, 'guess', get_dummy_guess(stash=lambda: n))
with forge_stash(0):
self.assertEqual(stash(), None)
with forge_stash(1):
self.assertEqual(stash(), [{
'divider_highlight_group': 'stash:divider',
'highlight_groups': ['stash'],
'contents': '1'
}])
with forge_stash(2):
self.assertEqual(stash(), [{
'divider_highlight_group': 'stash:divider',
'highlight_groups': ['stash'],
'contents': '2'
}])
def test_file_vcs_status(self): def test_file_vcs_status(self):
pl = Pl() pl = Pl()
create_watcher = get_fallback_create_watcher() create_watcher = get_fallback_create_watcher()

View File

@ -88,6 +88,10 @@ do_run_test() {
|| test "$PYTHON_IMPLEMENTATION" = "PyPy" \ || test "$PYTHON_IMPLEMENTATION" = "PyPy" \
) \ ) \
) \ ) \
|| ( \
test "x${SH}" = "xipython" \
&& test "$("${PYTHON}" -mIPython --version | head -n1 | cut -d. -f1)" -ge 5 \
) \
) ; then ) ; then
wait_for_echo_arg="--wait-for-echo" wait_for_echo_arg="--wait-for-echo"
fi fi
@ -363,13 +367,6 @@ if test -z "${ONLY_SHELL}" || test "x${ONLY_SHELL%sh}" != "x${ONLY_SHELL}" || te
fi fi
fi fi
SH="${TEST_COMMAND%% *}" SH="${TEST_COMMAND%% *}"
# dash tests are not stable, see #931
if test x$FAST$SH = x1dash ; then
continue
fi
if test x$FAST$SH = x1fish ; then
continue
fi
if test "x$ONLY_SHELL" != "x" && test "x$ONLY_SHELL" != "x$SH" ; then if test "x$ONLY_SHELL" != "x" && test "x$ONLY_SHELL" != "x$SH" ; then
continue continue
fi fi
@ -378,7 +375,13 @@ if test -z "${ONLY_SHELL}" || test "x${ONLY_SHELL%sh}" != "x${ONLY_SHELL}" || te
fi fi
echo ">>> $(readlink "tests/shell/path/$SH")" echo ">>> $(readlink "tests/shell/path/$SH")"
if ! run_test $TEST_TYPE $TEST_CLIENT $TEST_COMMAND ; then if ! run_test $TEST_TYPE $TEST_CLIENT $TEST_COMMAND ; then
fail "$SH-$TEST_TYPE-$TEST_CLIENT:test" F "Failed checking $TEST_COMMAND" ALLOW_FAILURE_ARG=
# dash tests are not stable, see #931
# also do not allow fish tests to spoil the build
if test x$FAST$SH = x1dash || test x$FAST$SH = x1fish ; then
ALLOW_FAILURE_ARG="--allow-failure"
fi
fail $ALLOW_FAILURE_ARG "$SH-$TEST_TYPE-$TEST_CLIENT:test" F "Failed checking $TEST_COMMAND"
fi fi
done done
done done
@ -448,7 +451,8 @@ if test "x${ONLY_SHELL}" = "x" || test "x${ONLY_SHELL}" = "xipython" ; then
export POWERLINE_THEME_OVERRIDES='in.segments.left=[]' export POWERLINE_THEME_OVERRIDES='in.segments.left=[]'
echo "> ipython" echo "> ipython"
if ! run_test ipython ipython ${IPYTHON_PYTHON} -mIPython ; then if ! run_test ipython ipython ${IPYTHON_PYTHON} -mIPython ; then
fail "ipython:test" F "Failed checking ${IPYTHON_PYTHON} -mIPython" # Do not allow ipython tests to spoil the build
fail --allow-failure "ipython:test" F "Failed checking ${IPYTHON_PYTHON} -mIPython"
fi fi
unset POWERLINE_THEME_OVERRIDES unset POWERLINE_THEME_OVERRIDES
unset POWERLINE_CONFIG_OVERRIDES unset POWERLINE_CONFIG_OVERRIDES