Merge remote-tracking branch 'zyx-i/more-threaded-globals' into develop

This commit is contained in:
Kim Silkebækken 2013-04-08 11:31:04 +02:00
commit c1ed109422
19 changed files with 982 additions and 375 deletions

View File

@ -1,2 +1,2 @@
setlocal noexpandtab setlocal noexpandtab
let g:syntastic_python_flake8_args = '--ignore=W191,E501,E121,E122,E123,E128' let g:syntastic_python_flake8_args = '--ignore=W191,E501,E121,E122,E123,E128,E225,W291'

View File

@ -166,6 +166,16 @@ Common configuration is a subdictionary that is a value of ``common`` key in
String, determines format of the log messages. Defaults to String, determines format of the log messages. Defaults to
``'%(asctime)s:%(level)s:%(message)s'``. ``'%(asctime)s:%(level)s:%(message)s'``.
``interval``
Number, determines time (in seconds) between checks for changed
configuration. Checks are done in a seprate thread. Use ``null`` to check
for configuration changes on ``.render()`` call in main thread.
Defaults to ``None``.
``reload_config``
Boolean, determines whether configuration should be reloaded at all.
Defaults to ``True``.
Extension-specific configuration Extension-specific configuration
-------------------------------- --------------------------------

View File

@ -100,8 +100,14 @@ absolute path to your Powerline installation directory:
If you're using Vundle or Pathogen and don't want Powerline functionality in If you're using Vundle or Pathogen and don't want Powerline functionality in
any other applications, simply add Powerline as a bundle and point the path any other applications, simply add Powerline as a bundle and point the path
above to the Powerline bundle directory, e.g. above to the Powerline bundle directory, e.g.
``~/.vim/bundle/powerline/powerline/bindings/vim``. ``~/.vim/bundle/powerline/powerline/bindings/vim``. For vim-addon-manager it is
even easier since you dont need to write this big path or install anything by
hand: ``powerline`` is installed and run just like any other plugin using
.. code-block:: vim
call vam#ActivateAddons(['powerline'])
Shell prompts Shell prompts
------------- -------------

View File

@ -1,64 +1,18 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import absolute_import from __future__ import absolute_import
import json
import os import os
import sys import sys
import logging import logging
from powerline.colorscheme import Colorscheme from powerline.colorscheme import Colorscheme
from powerline.lib.file_watcher import create_file_watcher from powerline.lib.config import ConfigLoader
from threading import Lock, Thread, Event from threading import Lock, Event
from collections import defaultdict
DEFAULT_SYSTEM_CONFIG_DIR = None DEFAULT_SYSTEM_CONFIG_DIR = None
watcher = None
class MultiClientWatcher(object):
subscribers = set()
received_events = {}
def __init__(self):
global watcher
self.subscribers.add(self)
if not watcher:
watcher = create_file_watcher()
def watch(self, file):
watcher.watch(file)
def __call__(self, file):
if self not in self.subscribers:
return False
if file in self.received_events and self not in self.received_events[file]:
self.received_events[file].add(self)
if self.received_events[file] >= self.subscribers:
self.received_events.pop(file)
return True
if watcher(file):
self.received_events[file] = set([self])
return True
return False
def unsubscribe(self):
try:
self.subscribers.remove(self)
except KeyError:
pass
__del__ = unsubscribe
def open_file(path):
return open(path, 'r')
def find_config_file(search_paths, config_file): def find_config_file(search_paths, config_file):
config_file += '.json' config_file += '.json'
@ -69,11 +23,6 @@ def find_config_file(search_paths, config_file):
raise IOError('Config file not found in search path: {0}'.format(config_file)) raise IOError('Config file not found in search path: {0}'.format(config_file))
def load_json_config(config_file_path, load=json.load, open_file=open_file):
with open_file(config_file_path) as config_file_fp:
return load(config_file_fp)
class PowerlineState(object): class PowerlineState(object):
def __init__(self, use_daemon_threads, logger, ext): def __init__(self, use_daemon_threads, logger, ext):
self.logger = logger self.logger = logger
@ -85,7 +34,9 @@ class PowerlineState(object):
def _log(self, attr, msg, *args, **kwargs): def _log(self, attr, msg, *args, **kwargs):
prefix = kwargs.get('prefix') or self.prefix prefix = kwargs.get('prefix') or self.prefix
prefix = self.ext + ((':' + prefix) if prefix else '') prefix = self.ext + ((':' + prefix) if prefix else '')
msg = prefix + ':' + msg.format(*args, **kwargs) if args or kwargs:
msg = msg.format(*args, **kwargs)
msg = prefix + ':' + msg
key = attr + ':' + prefix key = attr + ':' + prefix
if msg != self.last_msgs.get(key): if msg != self.last_msgs.get(key):
getattr(self.logger, attr)(msg) getattr(self.logger, attr)(msg)
@ -132,9 +83,12 @@ class Powerline(object):
during python session. during python session.
:param Logger logger: :param Logger logger:
If present, no new logger will be created and this logger will be used. If present, no new logger will be created and this logger will be used.
:param float interval: :param bool use_daemon_threads:
When reloading configuration wait for this amount of seconds. Set it to Use daemon threads for.
None if you dont want to reload configuration automatically. :param Event shutdown_event:
Use this Event as shutdown_event.
:param ConfigLoader config_loader:
Class that manages (re)loading of configuration.
''' '''
def __init__(self, def __init__(self,
@ -143,41 +97,39 @@ class Powerline(object):
run_once=False, run_once=False,
logger=None, logger=None,
use_daemon_threads=True, use_daemon_threads=True,
interval=10, shutdown_event=None,
watcher=None): config_loader=None):
self.ext = ext self.ext = ext
self.renderer_module = renderer_module or ext self.renderer_module = renderer_module or ext
self.run_once = run_once self.run_once = run_once
self.logger = logger self.logger = logger
self.use_daemon_threads = use_daemon_threads self.use_daemon_threads = use_daemon_threads
self.interval = interval
if '.' not in self.renderer_module: if '.' not in self.renderer_module:
self.renderer_module = 'powerline.renderers.' + self.renderer_module self.renderer_module = 'powerline.renderers.' + self.renderer_module
elif self.renderer_module[-1] == '.': elif self.renderer_module[-1] == '.':
self.renderer_module = self.renderer_module[:-1] self.renderer_module = self.renderer_module[:-1]
self.config_paths = self.get_config_paths() config_paths = self.get_config_paths()
self.find_config_file = lambda cfg_path: find_config_file(config_paths, cfg_path)
self.configs_lock = Lock()
self.cr_kwargs_lock = Lock() self.cr_kwargs_lock = Lock()
self.create_renderer_kwargs = {} self.create_renderer_kwargs = {
self.shutdown_event = Event() 'load_main': True,
self.configs = defaultdict(set) 'load_colors': True,
self.missing = defaultdict(set) 'load_colorscheme': True,
'load_theme': True,
self.thread = None }
self.shutdown_event = shutdown_event or Event()
self.config_loader = config_loader or ConfigLoader(shutdown_event=self.shutdown_event)
self.run_loader_update = False
self.renderer_options = {} self.renderer_options = {}
self.watcher = watcher or MultiClientWatcher()
self.prev_common_config = None self.prev_common_config = None
self.prev_ext_config = None self.prev_ext_config = None
self.pl = None self.pl = None
self.create_renderer(load_main=True, load_colors=True, load_colorscheme=True, load_theme=True)
def create_renderer(self, load_main=False, load_colors=False, load_colorscheme=False, load_theme=False): def create_renderer(self, load_main=False, load_colors=False, load_colorscheme=False, load_theme=False):
'''(Re)create renderer object. Can be used after Powerline object was '''(Re)create renderer object. Can be used after Powerline object was
successfully initialized. If any of the below parameters except successfully initialized. If any of the below parameters except
@ -224,6 +176,8 @@ class Powerline(object):
if not self.pl: if not self.pl:
self.pl = PowerlineState(self.use_daemon_threads, self.logger, self.ext) self.pl = PowerlineState(self.use_daemon_threads, self.logger, self.ext)
if not self.config_loader.pl:
self.config_loader.pl = self.pl
self.renderer_options.update( self.renderer_options.update(
pl=self.pl, pl=self.pl,
@ -235,9 +189,17 @@ class Powerline(object):
'ext': self.ext, 'ext': self.ext,
'common_config': self.common_config, 'common_config': self.common_config,
'run_once': self.run_once, 'run_once': self.run_once,
'shutdown_event': self.shutdown_event,
}, },
) )
if not self.run_once and self.common_config.get('reload_config', True):
interval = self.common_config.get('interval', None)
self.config_loader.set_interval(interval)
self.run_loader_update = (interval is None)
if interval is not None and not self.config_loader.is_alive():
self.config_loader.start()
self.ext_config = config['ext'][self.ext] self.ext_config = config['ext'][self.ext]
if self.ext_config != self.prev_ext_config: if self.ext_config != self.prev_ext_config:
ext_config_differs = True ext_config_differs = True
@ -286,9 +248,6 @@ class Powerline(object):
else: else:
self.renderer = renderer self.renderer = renderer
if not self.run_once and not self.is_alive() and self.interval is not None:
self.start()
def get_log_handler(self): def get_log_handler(self):
'''Get log handler. '''Get log handler.
@ -324,24 +283,20 @@ class Powerline(object):
return config_paths return config_paths
def _load_config(self, cfg_path, type): def _load_config(self, cfg_path, type):
'''Load configuration and setup watcher.''' '''Load configuration and setup watches.'''
function = getattr(self, 'on_' + type + '_change')
try: try:
path = find_config_file(self.config_paths, cfg_path) path = self.find_config_file(cfg_path)
except IOError: except IOError:
with self.configs_lock: self.config_loader.register_missing(self.find_config_file, function, cfg_path)
self.missing[type].add(cfg_path)
raise raise
with self.configs_lock: self.config_loader.register(function, path)
self.configs[type].add(path) return self.config_loader.load(path)
self.watcher.watch(path)
return load_json_config(path)
def _purge_configs(self, type): def _purge_configs(self, type):
try: function = getattr(self, 'on_' + type + '_change')
with self.configs_lock: self.config_loader.unregister_functions(set((function,)))
self.configs.pop(type) self.config_loader.unregister_missing(set(((self.find_config_file, function),)))
except KeyError:
pass
def load_theme_config(self, name): def load_theme_config(self, name):
'''Get theme configuration. '''Get theme configuration.
@ -393,63 +348,59 @@ class Powerline(object):
''' '''
return None return None
def render(self, *args, **kwargs): def update_renderer(self):
'''Lock renderer from modifications and pass all arguments further to '''Updates/creates a renderer if needed.'''
``self.renderer.render()``. if self.run_loader_update:
''' self.config_loader.update()
create_renderer_kwargs = None
with self.cr_kwargs_lock: with self.cr_kwargs_lock:
if self.create_renderer_kwargs: if self.create_renderer_kwargs:
try: create_renderer_kwargs = self.create_renderer_kwargs.copy()
self.create_renderer(**self.create_renderer_kwargs) if create_renderer_kwargs:
except Exception as e: try:
self.pl.exception('Failed to create renderer: {0}', str(e)) self.create_renderer(**create_renderer_kwargs)
finally: except Exception as e:
self.create_renderer_kwargs.clear() self.pl.exception('Failed to create renderer: {0}', str(e))
finally:
self.create_renderer_kwargs.clear()
def render(self, *args, **kwargs):
'''Update/create renderer if needed and pass all arguments further to
``self.renderer.render()``.
'''
self.update_renderer()
return self.renderer.render(*args, **kwargs) return self.renderer.render(*args, **kwargs)
def shutdown(self): def shutdown(self):
'''Lock renderer from modifications and run its ``.shutdown()`` method. '''Shut down all background threads. Must be run only prior to exiting
current application.
''' '''
self.shutdown_event.set() self.shutdown_event.set()
if self.use_daemon_threads and self.is_alive():
# Give the worker thread a chance to shutdown, but don't block for too long
self.thread.join(.01)
self.renderer.shutdown() self.renderer.shutdown()
self.watcher.unsubscribe() functions = (
self.on_main_change,
self.on_colors_change,
self.on_colorscheme_change,
self.on_theme_change,
)
self.config_loader.unregister_functions(set(functions))
self.config_loader.unregister_missing(set(((find_config_file, function) for function in functions)))
def is_alive(self): def on_main_change(self, path):
return self.thread and self.thread.is_alive() with self.cr_kwargs_lock:
self.create_renderer_kwargs['load_main'] = True
def start(self): def on_colors_change(self, path):
self.thread = Thread(target=self.run) with self.cr_kwargs_lock:
if self.use_daemon_threads: self.create_renderer_kwargs['load_colors'] = True
self.thread.daemon = True
self.thread.start()
def run(self): def on_colorscheme_change(self, path):
while not self.shutdown_event.is_set(): with self.cr_kwargs_lock:
kwargs = {} self.create_renderer_kwargs['load_colorscheme'] = True
removes = []
with self.configs_lock: def on_theme_change(self, path):
for type, paths in self.configs.items(): with self.cr_kwargs_lock:
for path in paths: self.create_renderer_kwargs['load_theme'] = True
if self.watcher(path):
kwargs['load_' + type] = True
for type, cfg_paths in self.missing.items():
for cfg_path in cfg_paths:
try:
find_config_file(self.config_paths, cfg_path)
except IOError:
pass
else:
kwargs['load_' + type] = True
removes.append((type, cfg_path))
for type, cfg_path in removes:
self.missing[type].remove(cfg_path)
if kwargs:
with self.cr_kwargs_lock:
self.create_renderer_kwargs.update(kwargs)
self.shutdown_event.wait(self.interval)
def __enter__(self): def __enter__(self):
return self return self

View File

@ -14,14 +14,13 @@ _powerline_tmux_set_columns() {
} }
_powerline_install_precmd() { _powerline_install_precmd() {
emulate -L zsh emulate zsh
for f in "${precmd_functions[@]}"; do for f in "${precmd_functions[@]}"; do
if [[ "$f" = "_powerline_precmd" ]]; then if [[ "$f" = "_powerline_precmd" ]]; then
return return
fi fi
done done
chpwd_functions+=( _powerline_tmux_set_pwd ) chpwd_functions+=( _powerline_tmux_set_pwd )
setopt nolocaloptions
setopt promptpercent setopt promptpercent
setopt promptsubst setopt promptsubst
if zmodload zsh/zpython &>/dev/null ; then if zmodload zsh/zpython &>/dev/null ; then

156
powerline/lib/config.py Normal file
View File

@ -0,0 +1,156 @@
# vim:fileencoding=utf-8:noet
from powerline.lib.threaded import MultiRunnedThread
from powerline.lib.file_watcher import create_file_watcher
from threading import Event, Lock
from collections import defaultdict
import json
def open_file(path):
return open(path, 'r')
def load_json_config(config_file_path, load=json.load, open_file=open_file):
with open_file(config_file_path) as config_file_fp:
return load(config_file_fp)
class ConfigLoader(MultiRunnedThread):
def __init__(self, shutdown_event=None, watcher=None, load=load_json_config):
super(ConfigLoader, self).__init__()
self.shutdown_event = shutdown_event or Event()
self.watcher = watcher or create_file_watcher()
self._load = load
self.pl = None
self.interval = None
self.lock = Lock()
self.watched = defaultdict(set)
self.missing = defaultdict(set)
self.loaded = {}
def set_pl(self, pl):
self.pl = pl
def set_interval(self, interval):
self.interval = interval
def register(self, function, path):
'''Register function that will be run when file changes.
:param function function:
Function that will be called when file at the given path changes.
:param str path:
Path that will be watched for.
'''
with self.lock:
self.watched[path].add(function)
self.watcher.watch(path)
def register_missing(self, condition_function, function, key):
'''Register any function that will be called with given key each
interval seconds (interval is defined at __init__). Its result is then
passed to ``function``, but only if the result is true.
:param function condition_function:
Function which will be called each ``interval`` seconds. All
exceptions from it will be ignored.
:param function function:
Function which will be called if condition_function returns
something that is true. Accepts result of condition_function as an
argument.
:param str key:
Any value, it will be passed to condition_function on each call.
Note: registered functions will be automatically removed if
condition_function results in something true.
'''
with self.lock:
self.missing[key].add((condition_function, function))
def unregister_functions(self, removed_functions):
'''Unregister files handled by these functions.
:param set removed_functions:
Set of functions previously passed to ``.register()`` method.
'''
with self.lock:
for path, functions in list(self.watched.items()):
functions -= removed_functions
if not functions:
self.watched.pop(path)
self.loaded.pop(path, None)
def unregister_missing(self, removed_functions):
'''Unregister files handled by these functions.
:param set removed_functions:
Set of pairs (2-tuples) representing ``(condition_function,
function)`` function pairs previously passed as an arguments to
``.register_missing()`` method.
'''
with self.lock:
for key, functions in list(self.missing.items()):
functions -= removed_functions
if not functions:
self.missing.pop(key)
def load(self, path):
try:
# No locks: GIL does what we need
return self.loaded[path]
except KeyError:
r = self._load(path)
self.loaded[path] = r
return r
def update(self):
toload = []
with self.lock:
for path, functions in self.watched.items():
for function in functions:
try:
modified = self.watcher(path)
except OSError as e:
modified = True
self.exception('Error while running watcher for path {0}: {1}', path, str(e))
else:
if modified:
toload.append(path)
if modified:
function(path)
with self.lock:
for key, functions in list(self.missing.items()):
for condition_function, function in list(functions):
try:
path = condition_function(key)
except Exception as e:
self.exception('Error while running condition function for key {0}: {1}', key, str(e))
else:
if path:
toload.append(path)
function(path)
functions.remove((condition_function, function))
if not functions:
self.missing.pop(key)
for path in toload:
try:
self.loaded[path] = self._load(path)
except Exception as e:
self.exception('Error while loading {0}: {1}', path, str(e))
def run(self):
while self.interval is not None and not self.shutdown_event.is_set():
self.update()
self.shutdown_event.wait(self.interval)
def exception(self, msg, *args, **kwargs):
if self.pl:
self.pl.exception(msg, prefix='config_loader', *args, **kwargs)
else:
raise

View File

@ -1,4 +1,4 @@
# vim:fileencoding=UTF-8:noet # vim:fileencoding=utf-8:noet
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
@ -6,120 +6,23 @@ __docformat__ = 'restructuredtext en'
import os import os
import sys import sys
import errno
from time import sleep from time import sleep
from threading import RLock from threading import RLock
from powerline.lib.monotonic import monotonic from powerline.lib.monotonic import monotonic
from powerline.lib.inotify import INotify, INotifyError
class INotifyError(Exception):
pass
class INotifyWatch(object): class INotifyWatch(INotify):
is_stat_based = False is_stat_based = False
# See <sys/inotify.h> for the flags defined below def __init__(self, expire_time=10):
super(INotifyWatch, self).__init__()
# Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH.
ACCESS = 0x00000001 # File was accessed.
MODIFY = 0x00000002 # File was modified.
ATTRIB = 0x00000004 # Metadata changed.
CLOSE_WRITE = 0x00000008 # Writtable file was closed.
CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed.
OPEN = 0x00000020 # File was opened.
MOVED_FROM = 0x00000040 # File was moved from X.
MOVED_TO = 0x00000080 # File was moved to Y.
CREATE = 0x00000100 # Subfile was created.
DELETE = 0x00000200 # Subfile was deleted.
DELETE_SELF = 0x00000400 # Self was deleted.
MOVE_SELF = 0x00000800 # Self was moved.
# Events sent by the kernel.
UNMOUNT = 0x00002000 # Backing fs was unmounted.
Q_OVERFLOW = 0x00004000 # Event queued overflowed.
IGNORED = 0x00008000 # File was ignored.
# Helper events.
CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close.
MOVE = (MOVED_FROM | MOVED_TO) # Moves.
# Special flags.
ONLYDIR = 0x01000000 # Only watch the path if it is a directory.
DONT_FOLLOW = 0x02000000 # Do not follow a sym link.
EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects.
MASK_ADD = 0x20000000 # Add to the mask of an already existing watch.
ISDIR = 0x40000000 # Event occurred against dir.
ONESHOT = 0x80000000 # Only send event once.
# All events which a program can wait on.
ALL_EVENTS = (ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE |
OPEN | MOVED_FROM | MOVED_TO | CREATE | DELETE |
DELETE_SELF | MOVE_SELF)
# See <bits/inotify.h>
CLOEXEC = 0x80000
NONBLOCK = 0x800
def __init__(self, inotify_fd, add_watch, rm_watch, read, expire_time=10):
import ctypes
import struct
self._add_watch, self._rm_watch = add_watch, rm_watch
self._read = read
# We keep a reference to os to prevent it from being deleted
# during interpreter shutdown, which would lead to errors in the
# __del__ method
self.os = os
self.watches = {} self.watches = {}
self.modified = {} self.modified = {}
self.last_query = {} self.last_query = {}
self._buf = ctypes.create_string_buffer(5000)
self.fenc = sys.getfilesystemencoding() or 'utf-8'
self.hdr = struct.Struct(b'iIII')
if self.fenc == 'ascii':
self.fenc = 'utf-8'
self.lock = RLock() self.lock = RLock()
self.expire_time = expire_time * 60 self.expire_time = expire_time * 60
self._inotify_fd = inotify_fd
def handle_error(self):
import ctypes
eno = ctypes.get_errno()
raise OSError(eno, self.os.strerror(eno))
def __del__(self):
# This method can be called during interpreter shutdown, which means we
# must do the absolute minimum here. Note that there could be running
# daemon threads that are trying to call other methods on this object.
try:
self.os.close(self._inotify_fd)
except (AttributeError, TypeError):
pass
def read(self):
import ctypes
buf = []
while True:
num = self._read(self._inotify_fd, self._buf, len(self._buf))
if num == 0:
break
if num < 0:
en = ctypes.get_errno()
if en == errno.EAGAIN:
break # No more data
if en == errno.EINTR:
continue # Interrupted, try again
raise OSError(en, self.os.strerror(en))
buf.append(self._buf.raw[:num])
raw = b''.join(buf)
pos = 0
lraw = len(raw)
while lraw - pos >= self.hdr.size:
wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos)
# We dont care about names as we only watch files
pos += self.hdr.size + name_len
self.process_event(wd, mask, cookie)
def expire_watches(self): def expire_watches(self):
now = monotonic() now = monotonic()
@ -127,7 +30,19 @@ class INotifyWatch(object):
if last_query - now > self.expire_time: if last_query - now > self.expire_time:
self.unwatch(path) self.unwatch(path)
def process_event(self, wd, mask, cookie): def process_event(self, wd, mask, cookie, name):
if wd == -1 and (mask & self.Q_OVERFLOW):
# We missed some INOTIFY events, so we dont
# know the state of any tracked files.
for path in tuple(self.modified):
if os.path.exists(path):
self.modified[path] = True
else:
self.watches.pop(path, None)
self.modified.pop(path, None)
self.last_query.pop(path, None)
return
for path, num in tuple(self.watches.items()): for path, num in tuple(self.watches.items()):
if num == wd: if num == wd:
if mask & self.IGNORED: if mask & self.IGNORED:
@ -176,7 +91,7 @@ class INotifyWatch(object):
# exist/you dont have permission # exist/you dont have permission
self.watch(path) self.watch(path)
return True return True
self.read() self.read(get_name=False)
if path not in self.modified: if path not in self.modified:
# An ignored event was received which means the path has been # An ignored event was received which means the path has been
# automatically unwatched # automatically unwatched
@ -193,50 +108,7 @@ class INotifyWatch(object):
self.unwatch(path) self.unwatch(path)
except OSError: except OSError:
pass pass
if hasattr(self, '_inotify_fd'): super(INotifyWatch, self).close()
self.os.close(self._inotify_fd)
del self.os
del self._add_watch
del self._rm_watch
del self._inotify_fd
def get_inotify(expire_time=10):
''' Initialize the inotify based file watcher '''
import ctypes
if not hasattr(ctypes, 'c_ssize_t'):
raise INotifyError('You need python >= 2.7 to use inotify')
from ctypes.util import find_library
name = find_library('c')
if not name:
raise INotifyError('Cannot find C library')
libc = ctypes.CDLL(name, use_errno=True)
for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"):
if not hasattr(libc, function):
raise INotifyError('libc is too old')
# inotify_init1()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True)
init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),))
# inotify_add_watch()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True)
add_watch = prototype(('inotify_add_watch', libc), (
(1, "fd"), (1, "pathname"), (1, "mask")), use_errno=True)
# inotify_rm_watch()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True)
rm_watch = prototype(('inotify_rm_watch', libc), (
(1, "fd"), (1, "wd")), use_errno=True)
# read()
prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True)
read = prototype(('read', libc), (
(1, "fd"), (1, "buf"), (1, "count")), use_errno=True)
inotify_fd = init1(INotifyWatch.CLOEXEC | INotifyWatch.NONBLOCK)
if inotify_fd == -1:
raise INotifyError(os.strerror(ctypes.get_errno()))
return INotifyWatch(inotify_fd, add_watch, rm_watch, read, expire_time=expire_time)
class StatWatch(object): class StatWatch(object):
@ -289,7 +161,7 @@ def create_file_watcher(use_stat=False, expire_time=10):
if use_stat: if use_stat:
return StatWatch() return StatWatch()
try: try:
return get_inotify(expire_time=expire_time) return INotifyWatch(expire_time=expire_time)
except INotifyError: except INotifyError:
pass pass
return StatWatch() return StatWatch()

178
powerline/lib/inotify.py Normal file
View File

@ -0,0 +1,178 @@
# vim:fileencoding=utf-8:noet
from __future__ import unicode_literals, absolute_import
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys
import os
import errno
class INotifyError(Exception):
pass
_inotify = None
def load_inotify():
''' Initialize the inotify library '''
global _inotify
if _inotify is None:
if hasattr(sys, 'getwindowsversion'):
# On windows abort before loading the C library. Windows has
# multiple, incompatible C runtimes, and we have no way of knowing
# if the one chosen by ctypes is compatible with the currently
# loaded one.
raise INotifyError('INotify not available on windows')
import ctypes
if not hasattr(ctypes, 'c_ssize_t'):
raise INotifyError('You need python >= 2.7 to use inotify')
from ctypes.util import find_library
name = find_library('c')
if not name:
raise INotifyError('Cannot find C library')
libc = ctypes.CDLL(name, use_errno=True)
for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"):
if not hasattr(libc, function):
raise INotifyError('libc is too old')
# inotify_init1()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True)
init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),))
# inotify_add_watch()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True)
add_watch = prototype(('inotify_add_watch', libc), (
(1, "fd"), (1, "pathname"), (1, "mask")), use_errno=True)
# inotify_rm_watch()
prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True)
rm_watch = prototype(('inotify_rm_watch', libc), (
(1, "fd"), (1, "wd")), use_errno=True)
# read()
prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True)
read = prototype(('read', libc), (
(1, "fd"), (1, "buf"), (1, "count")), use_errno=True)
_inotify = (init1, add_watch, rm_watch, read)
return _inotify
class INotify(object):
# See <sys/inotify.h> for the flags defined below
# Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH.
ACCESS = 0x00000001 # File was accessed.
MODIFY = 0x00000002 # File was modified.
ATTRIB = 0x00000004 # Metadata changed.
CLOSE_WRITE = 0x00000008 # Writtable file was closed.
CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed.
OPEN = 0x00000020 # File was opened.
MOVED_FROM = 0x00000040 # File was moved from X.
MOVED_TO = 0x00000080 # File was moved to Y.
CREATE = 0x00000100 # Subfile was created.
DELETE = 0x00000200 # Subfile was deleted.
DELETE_SELF = 0x00000400 # Self was deleted.
MOVE_SELF = 0x00000800 # Self was moved.
# Events sent by the kernel.
UNMOUNT = 0x00002000 # Backing fs was unmounted.
Q_OVERFLOW = 0x00004000 # Event queued overflowed.
IGNORED = 0x00008000 # File was ignored.
# Helper events.
CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close.
MOVE = (MOVED_FROM | MOVED_TO) # Moves.
# Special flags.
ONLYDIR = 0x01000000 # Only watch the path if it is a directory.
DONT_FOLLOW = 0x02000000 # Do not follow a sym link.
EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects.
MASK_ADD = 0x20000000 # Add to the mask of an already existing watch.
ISDIR = 0x40000000 # Event occurred against dir.
ONESHOT = 0x80000000 # Only send event once.
# All events which a program can wait on.
ALL_EVENTS = (ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE |
OPEN | MOVED_FROM | MOVED_TO | CREATE | DELETE |
DELETE_SELF | MOVE_SELF)
# See <bits/inotify.h>
CLOEXEC = 0x80000
NONBLOCK = 0x800
def __init__(self, cloexec=True, nonblock=True):
import ctypes
import struct
self._init1, self._add_watch, self._rm_watch, self._read = load_inotify()
flags = 0
if cloexec:
flags |= self.CLOEXEC
if nonblock:
flags |= self.NONBLOCK
self._inotify_fd = self._init1(flags)
if self._inotify_fd == -1:
raise INotifyError(os.strerror(ctypes.get_errno()))
self._buf = ctypes.create_string_buffer(5000)
self.fenc = sys.getfilesystemencoding() or 'utf-8'
self.hdr = struct.Struct(b'iIII')
if self.fenc == 'ascii':
self.fenc = 'utf-8'
# We keep a reference to os to prevent it from being deleted
# during interpreter shutdown, which would lead to errors in the
# __del__ method
self.os = os
def handle_error(self):
import ctypes
eno = ctypes.get_errno()
raise OSError(eno, self.os.strerror(eno))
def __del__(self):
# This method can be called during interpreter shutdown, which means we
# must do the absolute minimum here. Note that there could be running
# daemon threads that are trying to call other methods on this object.
try:
self.os.close(self._inotify_fd)
except (AttributeError, TypeError):
pass
def close(self):
if hasattr(self, '_inotify_fd'):
self.os.close(self._inotify_fd)
del self.os
del self._add_watch
del self._rm_watch
del self._inotify_fd
def read(self, get_name=True):
import ctypes
buf = []
while True:
num = self._read(self._inotify_fd, self._buf, len(self._buf))
if num == 0:
break
if num < 0:
en = ctypes.get_errno()
if en == errno.EAGAIN:
break # No more data
if en == errno.EINTR:
continue # Interrupted, try again
raise OSError(en, self.os.strerror(en))
buf.append(self._buf.raw[:num])
raw = b''.join(buf)
pos = 0
lraw = len(raw)
while lraw - pos >= self.hdr.size:
wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos)
pos += self.hdr.size
name = None
if get_name:
name = raw[pos:pos + name_len].rstrip(b'\0').decode(self.fenc)
pos += name_len
self.process_event(wd, mask, cookie, name)
def process_event(self, *args):
raise NotImplementedError()

View File

@ -7,7 +7,28 @@ from powerline.lib.monotonic import monotonic
from threading import Thread, Lock, Event from threading import Thread, Lock, Event
class ThreadedSegment(object): class MultiRunnedThread(object):
daemon = True
def __init__(self):
self.thread = None
def is_alive(self):
return self.thread and self.thread.is_alive()
def start(self):
self.shutdown_event.clear()
self.thread = Thread(target=self.run)
self.thread.daemon = self.daemon
self.thread.start()
def join(self, *args, **kwargs):
if self.thread:
return self.thread.join(*args, **kwargs)
return None
class ThreadedSegment(MultiRunnedThread):
min_sleep_time = 0.1 min_sleep_time = 0.1
update_first = True update_first = True
interval = 1 interval = 1
@ -15,12 +36,11 @@ class ThreadedSegment(object):
def __init__(self): def __init__(self):
super(ThreadedSegment, self).__init__() super(ThreadedSegment, self).__init__()
self.shutdown_event = Event()
self.run_once = True self.run_once = True
self.thread = None
self.skip = False self.skip = False
self.crashed_value = None self.crashed_value = None
self.update_value = None self.update_value = None
self.updated = False
def __call__(self, pl, update_first=True, **kwargs): def __call__(self, pl, update_first=True, **kwargs):
if self.run_once: if self.run_once:
@ -37,6 +57,7 @@ class ThreadedSegment(object):
self.start() self.start()
elif not self.updated: elif not self.updated:
update_value = self.get_update_value(True) update_value = self.get_update_value(True)
self.updated = True
else: else:
update_value = self.update_value update_value = self.update_value
@ -50,15 +71,6 @@ class ThreadedSegment(object):
self.update_value = self.update(self.update_value) self.update_value = self.update(self.update_value)
return self.update_value return self.update_value
def is_alive(self):
return self.thread and self.thread.is_alive()
def start(self):
self.shutdown_event.clear()
self.thread = Thread(target=self.run)
self.thread.daemon = self.daemon
self.thread.start()
def run(self): def run(self):
while not self.shutdown_event.is_set(): while not self.shutdown_event.is_set():
start_time = monotonic() start_time = monotonic()
@ -77,8 +89,9 @@ class ThreadedSegment(object):
def shutdown(self): def shutdown(self):
self.shutdown_event.set() self.shutdown_event.set()
if self.daemon and self.is_alive(): if self.daemon and self.is_alive():
# Give the worker thread a chance to shutdown, but don't block for too long # Give the worker thread a chance to shutdown, but don't block for
self.thread.join(.01) # too long
self.join(0.01)
def set_interval(self, interval=None): def set_interval(self, interval=None):
# Allowing “interval” keyword in configuration. # Allowing “interval” keyword in configuration.
@ -88,9 +101,10 @@ class ThreadedSegment(object):
interval = interval or getattr(self, 'interval') interval = interval or getattr(self, 'interval')
self.interval = interval self.interval = interval
def set_state(self, interval=None, update_first=True, **kwargs): def set_state(self, interval=None, update_first=True, shutdown_event=None, **kwargs):
self.set_interval(interval) self.set_interval(interval)
self.updated = not (update_first and self.update_first) self.shutdown_event = shutdown_event or Event()
self.updated = self.updated or (not (update_first and self.update_first))
def startup(self, pl, **kwargs): def startup(self, pl, **kwargs):
self.run_once = False self.run_once = False
@ -102,6 +116,15 @@ class ThreadedSegment(object):
if not self.is_alive(): if not self.is_alive():
self.start() self.start()
def critical(self, *args, **kwargs):
self.pl.critical(prefix=self.__class__.__name__, *args, **kwargs)
def exception(self, *args, **kwargs):
self.pl.exception(prefix=self.__class__.__name__, *args, **kwargs)
def info(self, *args, **kwargs):
self.pl.info(prefix=self.__class__.__name__, *args, **kwargs)
def error(self, *args, **kwargs): def error(self, *args, **kwargs):
self.pl.error(prefix=self.__class__.__name__, *args, **kwargs) self.pl.error(prefix=self.__class__.__name__, *args, **kwargs)
@ -112,13 +135,6 @@ class ThreadedSegment(object):
self.pl.debug(prefix=self.__class__.__name__, *args, **kwargs) self.pl.debug(prefix=self.__class__.__name__, *args, **kwargs)
def printed(func):
def f(*args, **kwargs):
print(func.__name__)
return func(*args, **kwargs)
return f
class KwThreadedSegment(ThreadedSegment): class KwThreadedSegment(ThreadedSegment):
drop_interval = 10 * 60 drop_interval = 10 * 60
update_first = True update_first = True
@ -174,8 +190,9 @@ class KwThreadedSegment(ThreadedSegment):
return update_value return update_value
def set_state(self, interval=None, **kwargs): def set_state(self, interval=None, shutdown_event=None, **kwargs):
self.set_interval(interval) self.set_interval(interval)
self.shutdown_event = shutdown_event or Event()
@staticmethod @staticmethod
def render_one(update_state, **kwargs): def render_one(update_state, **kwargs):

View File

@ -0,0 +1,199 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, absolute_import, print_function)
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys
import os
import errno
from time import sleep
from powerline.lib.monotonic import monotonic
from powerline.lib.inotify import INotify, INotifyError
class NoSuchDir(ValueError):
pass
class DirTooLarge(ValueError):
def __init__(self, bdir):
ValueError.__init__(self, 'The directory {0} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir))
class INotifyTreeWatcher(INotify):
is_dummy = False
def __init__(self, basedir):
super(INotifyTreeWatcher, self).__init__()
self.basedir = os.path.abspath(basedir)
self.watch_tree()
self.modified = True
def watch_tree(self):
self.watched_dirs = {}
self.watched_rmap = {}
try:
self.add_watches(self.basedir)
except OSError as e:
if e.errno == errno.ENOSPC:
raise DirTooLarge(self.basedir)
def add_watches(self, base, top_level=True):
''' Add watches for this directory and all its descendant directories,
recursively. '''
base = os.path.abspath(base)
try:
is_dir = self.add_watch(base)
except OSError as e:
if e.errno == errno.ENOENT:
# The entry could have been deleted between listdir() and
# add_watch().
if top_level:
raise NoSuchDir('The dir {0} does not exist'.format(base))
return
if e.errno == errno.EACCES:
# We silently ignore entries for which we dont have permission,
# unless they are the top level dir
if top_level:
raise NoSuchDir('You do not have permission to monitor {0}'.format(base))
return
raise
else:
if is_dir:
try:
files = os.listdir(base)
except OSError as e:
if e.errno in (errno.ENOTDIR, errno.ENOENT):
# The dir was deleted/replaced between the add_watch()
# and listdir()
if top_level:
raise NoSuchDir('The dir {0} does not exist'.format(base))
return
raise
for x in files:
self.add_watches(os.path.join(base, x), top_level=False)
elif top_level:
# The top level dir is a file, not good.
raise NoSuchDir('The dir {0} does not exist'.format(base))
def add_watch(self, path):
import ctypes
bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
wd = self._add_watch(self._inotify_fd, ctypes.c_char_p(bpath),
# Ignore symlinks and watch only directories
self.DONT_FOLLOW | self.ONLYDIR |
self.MODIFY | self.CREATE | self.DELETE |
self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO |
self.ATTRIB | self.MOVE_SELF | self.DELETE_SELF)
if wd == -1:
eno = ctypes.get_errno()
if eno == errno.ENOTDIR:
return False
raise OSError(eno, 'Failed to add watch for: {0}: {1}'.format(path, self.os.strerror(eno)))
self.watched_dirs[path] = wd
self.watched_rmap[wd] = path
return True
def process_event(self, wd, mask, cookie, name):
if wd == -1 and (mask & self.Q_OVERFLOW):
# We missed some INOTIFY events, so we dont
# know the state of any tracked dirs.
self.watch_tree()
self.modified = True
return
path = self.watched_rmap.get(wd, None)
if path is not None:
self.modified = True
if mask & self.CREATE:
# A new sub-directory might have been created, monitor it.
try:
self.add_watch(os.path.join(path, name))
except OSError as e:
if e.errno == errno.ENOENT:
# Deleted before add_watch()
pass
elif e.errno == errno.ENOSPC:
raise DirTooLarge(self.basedir)
else:
raise
def __call__(self):
self.read()
ret = self.modified
self.modified = False
return ret
class DummyTreeWatcher(object):
is_dummy = True
def __init__(self, basedir):
self.basedir = os.path.abspath(basedir)
def __call__(self):
return False
class TreeWatcher(object):
def __init__(self, expire_time=10):
self.watches = {}
self.last_query_times = {}
self.expire_time = expire_time * 60
def watch(self, path, logger=None):
path = os.path.abspath(path)
try:
w = INotifyTreeWatcher(path)
except (INotifyError, DirTooLarge) as e:
if logger is not None:
logger.warn('Failed to watch path: {0} with error: {1}'.format(path, e))
w = DummyTreeWatcher(path)
self.watches[path] = w
return w
def is_actually_watched(self, path):
w = self.watches.get(path, None)
return not getattr(w, 'is_dummy', True)
def expire_old_queries(self):
pop = []
now = monotonic()
for path, lt in self.last_query_times.items():
if now - lt > self.expire_time:
pop.append(path)
for path in pop:
del self.last_query_times[path]
def __call__(self, path, logger=None):
path = os.path.abspath(path)
self.expire_old_queries()
self.last_query_times[path] = monotonic()
w = self.watches.get(path, None)
if w is None:
try:
self.watch(path)
except NoSuchDir:
pass
return True
try:
return w()
except DirTooLarge as e:
if logger is not None:
logger.warn(str(e))
self.watches[path] = DummyTreeWatcher(path)
return False
if __name__ == '__main__':
w = INotifyTreeWatcher(sys.argv[-1])
w()
print ('Monitoring', sys.argv[-1], 'press Ctrl-C to stop')
try:
while True:
if w():
print (sys.argv[-1], 'changed')
sleep(1)
except KeyboardInterrupt:
raise SystemExit(0)

View File

@ -1,5 +1,6 @@
from powerline.lint.markedjson import load from powerline.lint.markedjson import load
from powerline import load_json_config, find_config_file, Powerline from powerline import find_config_file, Powerline
from powerline.lib.config import load_json_config
from powerline.lint.markedjson.error import echoerr, MarkedError from powerline.lint.markedjson.error import echoerr, MarkedError
from powerline.segments.vim import vim_modes from powerline.segments.vim import vim_modes
import itertools import itertools
@ -73,11 +74,15 @@ class Spec(object):
spec.context_message(msg) spec.context_message(msg)
return self return self
def check_type(self, value, context_mark, data, context, echoerr, t): def check_type(self, value, context_mark, data, context, echoerr, types):
if type(value.value) is not t: if type(value.value) not in types:
echoerr(context=self.cmsg.format(key=context_key(context)), echoerr(context=self.cmsg.format(key=context_key(context)),
context_mark=context_mark, context_mark=context_mark,
problem='{0!r} must be a {1} instance, not {2}'.format(value, t.__name__, type(value.value).__name__), problem='{0!r} must be a {1} instance, not {2}'.format(
value,
', '.join((t.__name__ for t in types)),
type(value.value).__name__
),
problem_mark=value.mark) problem_mark=value.mark)
return False, True return False, True
return True, False return True, False
@ -141,8 +146,8 @@ class Spec(object):
return False, hadproblem return False, hadproblem
return True, hadproblem return True, hadproblem
def type(self, t): def type(self, *args):
self.checks.append(('check_type', t)) self.checks.append(('check_type', args))
return self return self
cmp_funcs = { cmp_funcs = {
@ -172,12 +177,14 @@ class Spec(object):
def cmp(self, comparison, cint, msg_func=None): def cmp(self, comparison, cint, msg_func=None):
if type(cint) is str: if type(cint) is str:
self.type(unicode) self.type(unicode)
elif type(cint) is float:
self.type(int, float)
else: else:
self.type(type(cint)) self.type(type(cint))
cmp_func = self.cmp_funcs[comparison] cmp_func = self.cmp_funcs[comparison]
msg_func = msg_func or (lambda value: '{0} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint)) msg_func = msg_func or (lambda value: '{0} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint))
self.checks.append(('check_func', self.checks.append(('check_func',
(lambda value, *args: (True, True, not cmp_func(value, cint))), (lambda value, *args: (True, True, not cmp_func(value.value, cint))),
msg_func)) msg_func))
return self return self
@ -242,6 +249,11 @@ class Spec(object):
msg_func)) msg_func))
return self return self
def error(self, msg):
self.checks.append(('check_func', lambda *args: (True, True, True),
lambda value: msg.format(value)))
return self
def either(self, *specs): def either(self, *specs):
start = len(self.specs) start = len(self.specs)
self.specs.extend(specs) self.specs.extend(specs)
@ -406,6 +418,8 @@ main_spec = (Spec(
log_level=Spec().re('^[A-Z]+$').func(lambda value, *args: (True, True, not hasattr(logging, value)), log_level=Spec().re('^[A-Z]+$').func(lambda value, *args: (True, True, not hasattr(logging, value)),
lambda value: 'unknown debugging level {0}'.format(value)).optional(), lambda value: 'unknown debugging level {0}'.format(value)).optional(),
log_format=Spec().type(str).optional(), log_format=Spec().type(str).optional(),
interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(),
reload_config=Spec().type(bool).optional(),
).context_message('Error while loading common configuration (key {key})'), ).context_message('Error while loading common configuration (key {key})'),
ext=Spec( ext=Spec(
vim=Spec( vim=Spec(
@ -786,8 +800,11 @@ def check_segment_data_key(key, data, context, echoerr):
# FIXME More checks, limit existing to ThreadedSegment instances only # FIXME More checks, limit existing to ThreadedSegment instances only
args_spec = Spec( args_spec = Spec(
interval=Spec().either(Spec().type(float), Spec().type(int)).optional(), interval=Spec().cmp('gt', 0.0).optional(),
update_first=Spec().type(bool).optional(), update_first=Spec().type(bool).optional(),
shutdown_event=Spec().error('Shutdown event must be set by powerline').optional(),
pl=Spec().error('pl object must be set by powerline').optional(),
segment_info=Spec().error('Segment info dictionary must be set by powerline').optional(),
).unknown_spec(Spec(), Spec()).optional().copy ).unknown_spec(Spec(), Spec()).optional().copy
highlight_group_spec = Spec().type(unicode).copy highlight_group_spec = Spec().type(unicode).copy
segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy
@ -801,7 +818,7 @@ segments_spec = Spec().optional().list(
draw_soft_divider=Spec().type(bool).optional(), draw_soft_divider=Spec().type(bool).optional(),
draw_inner_divider=Spec().type(bool).optional(), draw_inner_divider=Spec().type(bool).optional(),
module=segment_module_spec(), module=segment_module_spec(),
priority=Spec().cmp('ge', -1).optional(), priority=Spec().either(Spec().cmp('eq', -1), Spec().cmp('ge', 0.0)).optional(),
after=Spec().type(unicode).optional(), after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(), before=Spec().type(unicode).optional(),
width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(), width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(),

View File

@ -18,8 +18,10 @@ from powerline.lib.humanize_bytes import humanize_bytes
from powerline.theme import requires_segment_info from powerline.theme import requires_segment_info
from collections import namedtuple from collections import namedtuple
cpu_count = None cpu_count = None
@requires_segment_info @requires_segment_info
def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False): def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False):
'''Return the current hostname. '''Return the current hostname.
@ -840,7 +842,7 @@ class EmailIMAPSegment(KwThreadedSegment):
return [{ return [{
'contents': str(unread_count), 'contents': str(unread_count),
'highlight_group': ['email_alert_gradient', 'email_alert'], 'highlight_group': ['email_alert_gradient', 'email_alert'],
'gradient_level': unread_count * 100.0 / max_msgs, 'gradient_level': min(unread_count * 100.0 / max_msgs, 100),
}] }]

View File

@ -22,7 +22,14 @@ def requires_segment_info(func):
class Theme(object): class Theme(object):
def __init__(self, ext, theme_config, common_config, pl, top_theme_config=None, run_once=False): def __init__(self,
ext,
theme_config,
common_config,
pl,
top_theme_config=None,
run_once=False,
shutdown_event=None):
self.dividers = theme_config.get('dividers', common_config['dividers']) self.dividers = theme_config.get('dividers', common_config['dividers'])
self.spaces = theme_config.get('spaces', common_config['spaces']) self.spaces = theme_config.get('spaces', common_config['spaces'])
self.segments = { self.segments = {
@ -44,7 +51,7 @@ class Theme(object):
if not run_once: if not run_once:
if segment['startup']: if segment['startup']:
try: try:
segment['startup'](pl=pl, **segment['args']) segment['startup'](pl=pl, shutdown_event=shutdown_event, **segment['args'])
except Exception as e: except Exception as e:
pl.error('Exception during {0} startup: {1}', segment['name'], str(e)) pl.error('Exception during {0} startup: {1}', segment['name'], str(e))
continue continue

View File

@ -46,6 +46,7 @@ class VimPowerline(Powerline):
``True`` if theme was added successfully and ``False`` if theme with ``True`` if theme was added successfully and ``False`` if theme with
the same matcher already exists. the same matcher already exists.
''' '''
self.update_renderer()
key = self.get_matcher(key) key = self.get_matcher(key)
try: try:
self.renderer.add_local_theme(key, {'config': config}) self.renderer.add_local_theme(key, {'config': config})

View File

@ -2,5 +2,7 @@
import sys import sys
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
from unittest2 import TestCase, main # NOQA from unittest2 import TestCase, main # NOQA
from unittest2.case import SkipTest # NOQA
else: else:
from unittest import TestCase, main # NOQA from unittest import TestCase, main # NOQA
from unittest.case import SkipTest # NOQA

View File

@ -1,6 +1,7 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from threading import Lock from threading import Lock
from powerline.renderer import Renderer from powerline.renderer import Renderer
from powerline.lib.config import ConfigLoader
from powerline import Powerline from powerline import Powerline
from copy import deepcopy from copy import deepcopy
@ -9,12 +10,12 @@ access_log = []
access_lock = Lock() access_lock = Lock()
def load_json_config(config, config_file_path, *args, **kwargs): def load_json_config(config_file_path, *args, **kwargs):
global access_log global access_log
with access_lock: with access_lock:
access_log.append(config_file_path) access_log.append(config_file_path)
try: try:
return deepcopy(config[config_file_path]) return deepcopy(config_container['config'][config_file_path])
except KeyError: except KeyError:
raise IOError(config_file_path) raise IOError(config_file_path)
@ -41,10 +42,10 @@ class Watcher(object):
pass pass
def __call__(self, file): def __call__(self, file):
if file in self.events: with self.lock:
with self.lock: if file in self.events:
self.events.remove(file) self.events.remove(file)
return True return True
return False return False
def _reset(self, files): def _reset(self, files):
@ -98,18 +99,20 @@ def get_powerline(**kwargs):
return TestPowerline( return TestPowerline(
ext='test', ext='test',
renderer_module='tests.lib.config_mock', renderer_module='tests.lib.config_mock',
interval=0,
logger=Logger(), logger=Logger(),
watcher=Watcher(), config_loader=ConfigLoader(load=load_json_config, watcher=Watcher()),
**kwargs **kwargs
) )
def swap_attributes(config_container, powerline_module, replaces): config_container = None
def swap_attributes(cfg_container, powerline_module, replaces):
global config_container
config_container = cfg_container
if not replaces: if not replaces:
replaces = { replaces = {
'watcher': Watcher(),
'load_json_config': lambda *args: load_json_config(config_container['config'], *args),
'find_config_file': lambda *args: find_config_file(config_container['config'], *args), 'find_config_file': lambda *args: find_config_file(config_container['config'], *args),
} }
for attr, val in replaces.items(): for attr, val in replaces.items():

View File

@ -6,7 +6,6 @@ from tests import TestCase
from tests.lib import replace_item from tests.lib import replace_item
from tests.lib.config_mock import swap_attributes, get_powerline, pop_events from tests.lib.config_mock import swap_attributes, get_powerline, pop_events
from copy import deepcopy from copy import deepcopy
from threading import Lock
config = { config = {
@ -23,6 +22,7 @@ config = {
}, },
}, },
'spaces': 0, 'spaces': 0,
'interval': 0,
}, },
'ext': { 'ext': {
'test': { 'test': {
@ -97,7 +97,7 @@ def sleep(interval):
def add_watcher_events(p, *args, **kwargs): def add_watcher_events(p, *args, **kwargs):
p.watcher._reset(args) p.config_loader.watcher._reset(args)
while not p._will_create_renderer(): while not p._will_create_renderer():
sleep(kwargs.get('interval', 0.000001)) sleep(kwargs.get('interval', 0.000001))
if not kwargs.get('wait', True): if not kwargs.get('wait', True):
@ -127,8 +127,8 @@ class TestConfigReload(TestCase):
def test_reload_main(self): def test_reload_main(self):
with get_powerline(run_once=False) as p: with get_powerline(run_once=False) as p:
with replace_item(globals(), 'config', deepcopy(config)): with replace_item(globals(), 'config', deepcopy(config)):
self.assertAccessEvents('config', 'colors', 'colorschemes/test/default', 'themes/test/default')
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents('config', 'colors', 'colorschemes/test/default', 'themes/test/default')
config['config']['common']['spaces'] = 1 config['config']['common']['spaces'] = 1
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
@ -187,7 +187,7 @@ class TestConfigReload(TestCase):
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents('config') self.assertAccessEvents('config')
self.assertEqual(p.logger._pop_msgs(), ['exception:test:Failed to create renderer: fcf:colorschemes/test/nonexistentraise']) self.assertIn('exception:test:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs())
config['colorschemes/test/nonexistentraise'] = { config['colorschemes/test/nonexistentraise'] = {
'groups': { 'groups': {
@ -241,6 +241,20 @@ class TestConfigReload(TestCase):
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
pop_events() pop_events()
def test_reload_theme_main(self):
with replace_item(globals(), 'config', deepcopy(config)):
config['config']['common']['interval'] = None
with get_powerline(run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents('config', 'colors', 'colorschemes/test/default', 'themes/test/default')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default', wait=False)
self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents('themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [])
pop_events()
replaces = {} replaces = {}

View File

@ -5,7 +5,8 @@ from powerline.lib.vcs import guess
from subprocess import call, PIPE from subprocess import call, PIPE
import os import os
import sys import sys
from tests import TestCase from functools import partial
from tests import TestCase, SkipTest
class TestLib(TestCase): class TestLib(TestCase):
@ -37,6 +38,88 @@ class TestLib(TestCase):
self.assertEqual(humanize_bytes(1000000000, si_prefix=False), '953.7 MiB') self.assertEqual(humanize_bytes(1000000000, si_prefix=False), '953.7 MiB')
class TestFilesystemWatchers(TestCase):
def do_test_for_change(self, watcher, path):
import time
st = time.time()
while time.time() - st < 1:
if watcher(path):
return
time.sleep(0.1)
self.fail('The change to {0} was not detected'.format(path))
def test_file_watcher(self):
from powerline.lib.file_watcher import create_file_watcher
w = create_file_watcher(use_stat=False)
if w.is_stat_based:
raise SkipTest('This test is not suitable for a stat based file watcher')
f1, f2 = os.path.join(INOTIFY_DIR, 'file1'), os.path.join(INOTIFY_DIR, 'file2')
with open(f1, 'wb'):
with open(f2, 'wb'):
pass
ne = os.path.join(INOTIFY_DIR, 'notexists')
self.assertRaises(OSError, w, ne)
self.assertTrue(w(f1))
self.assertTrue(w(f2))
os.utime(f1, None), os.utime(f2, None)
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2)
# Repeat once
os.utime(f1, None), os.utime(f2, None)
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2)
# Check that no false changes are reported
self.assertFalse(w(f1), 'Spurious change detected')
self.assertFalse(w(f2), 'Spurious change detected')
# Check that open the file with 'w' triggers a change
with open(f1, 'wb'):
with open(f2, 'wb'):
pass
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2)
# Check that writing to a file with 'a' triggers a change
with open(f1, 'ab') as f:
f.write(b'1')
self.do_test_for_change(w, f1)
# Check that deleting a file registers as a change
os.unlink(f1)
self.do_test_for_change(w, f1)
def test_tree_watcher(self):
from powerline.lib.tree_watcher import TreeWatcher
tw = TreeWatcher()
subdir = os.path.join(INOTIFY_DIR, 'subdir')
os.mkdir(subdir)
if tw.watch(INOTIFY_DIR).is_dummy:
raise SkipTest('No tree watcher available')
import shutil
self.assertTrue(tw(INOTIFY_DIR))
self.assertFalse(tw(INOTIFY_DIR))
changed = partial(self.do_test_for_change, tw, INOTIFY_DIR)
open(os.path.join(INOTIFY_DIR, 'tree1'), 'w').close()
changed()
open(os.path.join(subdir, 'tree1'), 'w').close()
changed()
os.unlink(os.path.join(subdir, 'tree1'))
changed()
os.rmdir(subdir)
changed()
os.mkdir(subdir)
changed()
os.rename(subdir, subdir + '1')
changed()
shutil.rmtree(subdir + '1')
changed()
os.mkdir(subdir)
f = os.path.join(subdir, 'f')
open(f, 'w').close()
changed()
with open(f, 'a') as s:
s.write(' ')
changed()
os.rename(f, f + '1')
changed()
use_mercurial = use_bzr = sys.version_info < (3, 0) use_mercurial = use_bzr = sys.version_info < (3, 0)
@ -106,6 +189,7 @@ old_cwd = None
GIT_REPO = 'git_repo' + os.environ.get('PYTHON', '') GIT_REPO = 'git_repo' + os.environ.get('PYTHON', '')
HG_REPO = 'hg_repo' + os.environ.get('PYTHON', '') HG_REPO = 'hg_repo' + os.environ.get('PYTHON', '')
BZR_REPO = 'bzr_repo' + os.environ.get('PYTHON', '') BZR_REPO = 'bzr_repo' + os.environ.get('PYTHON', '')
INOTIFY_DIR = 'inotify' + os.environ.get('PYTHON', '')
def setUpModule(): def setUpModule():
@ -130,12 +214,13 @@ def setUpModule():
call(['bzr', 'config', 'email=Foo <bar@example.org>'], cwd=BZR_REPO) call(['bzr', 'config', 'email=Foo <bar@example.org>'], cwd=BZR_REPO)
call(['bzr', 'config', 'nickname=test_powerline'], cwd=BZR_REPO) call(['bzr', 'config', 'nickname=test_powerline'], cwd=BZR_REPO)
call(['bzr', 'config', 'create_signatures=0'], cwd=BZR_REPO) call(['bzr', 'config', 'create_signatures=0'], cwd=BZR_REPO)
os.mkdir(INOTIFY_DIR)
def tearDownModule(): def tearDownModule():
global old_cwd global old_cwd
global old_HGRCPATH global old_HGRCPATH
for repo_dir in [GIT_REPO] + ([HG_REPO] if use_mercurial else []) + ([BZR_REPO] if use_bzr else []): for repo_dir in [INOTIFY_DIR, GIT_REPO] + ([HG_REPO] if use_mercurial else []) + ([BZR_REPO] if use_bzr else []):
for root, dirs, files in list(os.walk(repo_dir, topdown=False)): for root, dirs, files in list(os.walk(repo_dir, topdown=False)):
for file in files: for file in files:
os.remove(os.path.join(root, file)) os.remove(os.path.join(root, file))

View File

@ -11,26 +11,114 @@ _options = {
_last_bufnr = 0 _last_bufnr = 0
_highlights = {} _highlights = {}
buffers = {}
windows = [] _thread_id = None
def _buffer(): def _set_thread_id():
return windows[_window - 1].buffer.number global _thread_id
from threading import current_thread
_thread_id = current_thread().ident
def _logged(func): # Assuming import is done from the main thread
_set_thread_id()
def _vim(func):
from functools import wraps from functools import wraps
from threading import current_thread
@wraps(func) @wraps(func)
def f(*args, **kwargs): def f(*args, **kwargs):
global _thread_id
if _thread_id != current_thread().ident:
raise RuntimeError('Accessing vim from separate threads is not allowed')
_log.append((func.__name__, args)) _log.append((func.__name__, args))
return func(*args, **kwargs) return func(*args, **kwargs)
return f return f
class _Buffers(object):
@_vim
def __init__(self):
self.d = {}
@_vim
def __getitem__(self, item):
return self.d[item]
@_vim
def __setitem__(self, item, value):
self.d[item] = value
@_vim
def __contains__(self, item):
return item in self.d
@_vim
def __nonzero__(self):
return not not self.d
@_vim
def keys(self):
return self.d.keys()
@_vim
def pop(self, *args, **kwargs):
return self.d.pop(*args, **kwargs)
buffers = _Buffers()
class _Windows(object):
@_vim
def __init__(self):
self.l = []
@_vim
def __getitem__(self, item):
return self.l[item]
@_vim
def __setitem__(self, item, value):
self.l[item] = value
@_vim
def __len__(self):
return len(self.l)
@_vim
def __iter__(self):
return iter(self.l)
@_vim
def __nonzero__(self):
return not not self.l
@_vim
def pop(self, *args, **kwargs):
return self.l.pop(*args, **kwargs)
@_vim
def append(self, *args, **kwargs):
return self.l.append(*args, **kwargs)
@_vim
def index(self, *args, **kwargs):
return self.l.index(*args, **kwargs)
windows = _Windows()
@_vim
def _buffer():
return windows[_window - 1].buffer.number
def _construct_result(r): def _construct_result(r):
import sys import sys
if sys.version_info < (3,): if sys.version_info < (3,):
@ -58,7 +146,7 @@ def _log_print():
sys.stdout.write(repr(entry) + '\n') sys.stdout.write(repr(entry) + '\n')
@_logged @_vim
def command(cmd): def command(cmd):
if cmd.startswith('let g:'): if cmd.startswith('let g:'):
import re import re
@ -71,7 +159,7 @@ def command(cmd):
raise NotImplementedError raise NotImplementedError
@_logged @_vim
def eval(expr): def eval(expr):
if expr.startswith('g:'): if expr.startswith('g:'):
return _g[expr[2:]] return _g[expr[2:]]
@ -83,7 +171,7 @@ def eval(expr):
raise NotImplementedError raise NotImplementedError
@_logged @_vim
def bindeval(expr): def bindeval(expr):
if expr == 'g:': if expr == 'g:':
return _g return _g
@ -95,7 +183,7 @@ def bindeval(expr):
raise NotImplementedError raise NotImplementedError
@_logged @_vim
@_str_func @_str_func
def _emul_mode(*args): def _emul_mode(*args):
if args and args[0]: if args and args[0]:
@ -104,11 +192,11 @@ def _emul_mode(*args):
return _mode[0] return _mode[0]
@_logged @_vim
@_str_func @_str_func
def _emul_getbufvar(bufnr, varname): def _emul_getbufvar(bufnr, varname):
if varname[0] == '&': if varname[0] == '&':
if bufnr not in _buf_options: if bufnr not in buffers:
return '' return ''
try: try:
return _buf_options[bufnr][varname[1:]] return _buf_options[bufnr][varname[1:]]
@ -120,25 +208,25 @@ def _emul_getbufvar(bufnr, varname):
raise NotImplementedError raise NotImplementedError
@_logged @_vim
@_str_func @_str_func
def _emul_getwinvar(winnr, varname): def _emul_getwinvar(winnr, varname):
return _win_scopes[winnr][varname] return _win_scopes[winnr][varname]
@_logged @_vim
def _emul_setwinvar(winnr, varname, value): def _emul_setwinvar(winnr, varname, value):
_win_scopes[winnr][varname] = value _win_scopes[winnr][varname] = value
@_logged @_vim
def _emul_virtcol(expr): def _emul_virtcol(expr):
if expr == '.': if expr == '.':
return windows[_window - 1].cursor[1] + 1 return windows[_window - 1].cursor[1] + 1
raise NotImplementedError raise NotImplementedError
@_logged @_vim
@_str_func @_str_func
def _emul_fnamemodify(path, modstring): def _emul_fnamemodify(path, modstring):
import os import os
@ -154,7 +242,7 @@ def _emul_fnamemodify(path, modstring):
return path return path
@_logged @_vim
@_str_func @_str_func
def _emul_expand(expr): def _emul_expand(expr):
if expr == '<abuf>': if expr == '<abuf>':
@ -162,21 +250,21 @@ def _emul_expand(expr):
raise NotImplementedError raise NotImplementedError
@_logged @_vim
def _emul_bufnr(expr): def _emul_bufnr(expr):
if expr == '$': if expr == '$':
return _last_bufnr return _last_bufnr
raise NotImplementedError raise NotImplementedError
@_logged @_vim
def _emul_exists(varname): def _emul_exists(varname):
if varname.startswith('g:'): if varname.startswith('g:'):
return varname[2:] in _g return varname[2:] in _g
raise NotImplementedError raise NotImplementedError
@_logged @_vim
def _emul_line2byte(line): def _emul_line2byte(line):
buflines = _buf_lines[_buffer()] buflines = _buf_lines[_buffer()]
if line == len(buflines) + 1: if line == len(buflines) + 1:
@ -287,7 +375,7 @@ current = _Current()
_dict = None _dict = None
@_logged @_vim
def _init(): def _init():
global _dict global _dict
@ -302,7 +390,7 @@ def _init():
return _dict return _dict
@_logged @_vim
def _get_segment_info(): def _get_segment_info():
mode_translations = { mode_translations = {
chr(ord('V') - 0x40): '^V', chr(ord('V') - 0x40): '^V',
@ -319,12 +407,12 @@ def _get_segment_info():
} }
@_logged @_vim
def _launch_event(event): def _launch_event(event):
pass pass
@_logged @_vim
def _start_mode(mode): def _start_mode(mode):
global _mode global _mode
if mode == 'i': if mode == 'i':
@ -334,7 +422,7 @@ def _start_mode(mode):
_mode = mode _mode = mode
@_logged @_vim
def _undo(): def _undo():
if len(_undostate[_buffer()]) == 1: if len(_undostate[_buffer()]) == 1:
return return
@ -344,7 +432,7 @@ def _undo():
_buf_options[_buffer()]['modified'] = 0 _buf_options[_buffer()]['modified'] = 0
@_logged @_vim
def _edit(name=None): def _edit(name=None):
global _last_bufnr global _last_bufnr
if _buffer() and buffers[_buffer()].name is None: if _buffer() and buffers[_buffer()].name is None:
@ -355,14 +443,14 @@ def _edit(name=None):
windows[_window - 1].buffer = buf windows[_window - 1].buffer = buf
@_logged @_vim
def _new(name=None): def _new(name=None):
global _window global _window
_Window(buffer={'name': name}) _Window(buffer={'name': name})
_window = len(windows) _window = len(windows)
@_logged @_vim
def _del_window(winnr): def _del_window(winnr):
win = windows.pop(winnr - 1) win = windows.pop(winnr - 1)
_win_scopes.pop(winnr) _win_scopes.pop(winnr)
@ -371,7 +459,7 @@ def _del_window(winnr):
return win return win
@_logged @_vim
def _close(winnr, wipe=True): def _close(winnr, wipe=True):
global _window global _window
win = _del_window(winnr) win = _del_window(winnr)
@ -387,7 +475,7 @@ def _close(winnr, wipe=True):
_Window() _Window()
@_logged @_vim
def _bw(bufnr=None): def _bw(bufnr=None):
bufnr = bufnr or _buffer() bufnr = bufnr or _buffer()
winnr = 1 winnr = 1
@ -401,12 +489,12 @@ def _bw(bufnr=None):
_b(max(buffers.keys())) _b(max(buffers.keys()))
@_logged @_vim
def _b(bufnr): def _b(bufnr):
windows[_window - 1].buffer = buffers[bufnr] windows[_window - 1].buffer = buffers[bufnr]
@_logged @_vim
def _set_cursor(line, col): def _set_cursor(line, col):
windows[_window - 1].cursor = (line, col) windows[_window - 1].cursor = (line, col)
if _mode == 'n': if _mode == 'n':
@ -415,12 +503,12 @@ def _set_cursor(line, col):
_launch_event('CursorMovedI') _launch_event('CursorMovedI')
@_logged @_vim
def _get_buffer(): def _get_buffer():
return buffers[_buffer()] return buffers[_buffer()]
@_logged @_vim
def _set_bufoption(option, value, bufnr=None): def _set_bufoption(option, value, bufnr=None):
_buf_options[bufnr or _buffer()][option] = value _buf_options[bufnr or _buffer()][option] = value
if option == 'filetype': if option == 'filetype':
@ -440,7 +528,7 @@ class _WithNewBuffer(object):
_bw(self.bufnr) _bw(self.bufnr)
@_logged @_vim
def _set_dict(d, new, setfunc=None): def _set_dict(d, new, setfunc=None):
if not setfunc: if not setfunc:
def setfunc(k, v): def setfunc(k, v):
@ -496,7 +584,7 @@ class _WithDict(object):
self.d.pop(k) self.d.pop(k)
@_logged @_vim
def _with(key, *args, **kwargs): def _with(key, *args, **kwargs):
if key == 'buffer': if key == 'buffer':
return _WithNewBuffer(_edit, *args, **kwargs) return _WithNewBuffer(_edit, *args, **kwargs)