diff --git a/powerline/__init__.py b/powerline/__init__.py index 4e148f05..4df195dd 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -10,10 +10,43 @@ from powerline.lib.config import ConfigLoader from threading import Lock, Event +try: + from __builtin__ import unicode +except ImportError: + unicode = str # NOQA + DEFAULT_SYSTEM_CONFIG_DIR = None +def safe_unicode(s): + '''Return unicode instance without raising an exception. + ''' + try: + try: + return unicode(s) + except UnicodeDecodeError: + try: + return unicode(s, 'utf-8') + except TypeError: + return unicode(str(s), 'utf-8') + except Exception as e: + return safe_unicode(e) + + +class FailedUnicode(unicode): + '''Builtin ``unicode`` (``str`` in python 3) subclass indicating fatal + error. + + If your code for some reason wants to determine whether `.render()` method + failed it should check returned string for being a FailedUnicode instance. + Alternatively you could subclass Powerline and override `.render()` method + to do what you like in place of catching the exception and returning + FailedUnicode. + ''' + pass + + def find_config_file(search_paths, config_file): config_file += '.json' for path in search_paths: @@ -61,6 +94,29 @@ class PowerlineLogger(object): self._log('debug', msg, *args, **kwargs) +_fallback_logger = None + + +def _get_fallback_logger(): + global _fallback_logger + if _fallback_logger: + return _fallback_logger + + log_format = '%(asctime)s:%(levelname)s:%(message)s' + formatter = logging.Formatter(log_format) + + level = logging.WARNING + handler = logging.StreamHandler() + handler.setLevel(level) + handler.setFormatter(formatter) + + logger = logging.getLogger('powerline') + logger.setLevel(level) + logger.addHandler(handler) + _fallback_logger = PowerlineLogger(None, logger, '_fallback_') + return _fallback_logger + + class Powerline(object): '''Main powerline class, entrance point for all powerline uses. Sets powerline up and loads the configuration. @@ -233,7 +289,7 @@ class Powerline(object): try: Renderer = __import__(self.renderer_module, fromlist=['renderer']).renderer except Exception as e: - self.pl.exception('Failed to import renderer module: {0}', str(e)) + self.exception('Failed to import renderer module: {0}', str(e)) sys.exit(1) # Renderer updates configuration file via segments’ .startup thus it @@ -242,7 +298,7 @@ class Powerline(object): try: renderer = Renderer(**self.renderer_options) except Exception as e: - self.pl.exception('Failed to construct renderer object: {0}', str(e)) + self.exception('Failed to construct renderer object: {0}', str(e)) if not hasattr(self, 'renderer'): raise else: @@ -360,16 +416,34 @@ class Powerline(object): try: self.create_renderer(**create_renderer_kwargs) except Exception as e: - self.pl.exception('Failed to create renderer: {0}', str(e)) - finally: - self.create_renderer_kwargs.clear() + self.exception('Failed to create renderer: {0}', str(e)) + if hasattr(self, 'renderer'): + with self.cr_kwargs_lock: + self.create_renderer_kwargs.clear() + else: + raise + else: + with self.cr_kwargs_lock: + 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) + try: + self.update_renderer() + return self.renderer.render(*args, **kwargs) + except Exception as e: + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + # Updates e variable to new value, masking previous one. + # Normally it is the same exception (due to raise in case pl is + # unset), but it may also show error in logger. Note that latter + # is not logged by logger for obvious reasons, thus this also + # prevents us from seeing logger traceback. + pass + return FailedUnicode(safe_unicode(e)) def shutdown(self): '''Shut down all background threads. Must be run only prior to exiting @@ -410,3 +484,9 @@ class Powerline(object): def __exit__(self, *args): self.shutdown() + + def exception(self, msg, *args, **kwargs): + if 'prefix' not in kwargs: + kwargs['prefix'] = 'powerline' + pl = getattr(self, 'pl', None) or _get_fallback_logger() + return pl.exception(msg, *args, **kwargs) diff --git a/powerline/lib/config.py b/powerline/lib/config.py index e826e3bf..de2d9902 100644 --- a/powerline/lib/config.py +++ b/powerline/lib/config.py @@ -155,6 +155,10 @@ class ConfigLoader(MultiRunnedThread): try: self.loaded[path] = deepcopy(self._load(path)) except Exception as e: + try: + self.loaded.pop(path) + except KeyError: + pass self.exception('Error while loading {0}: {1}', path, str(e)) def run(self): diff --git a/powerline/segments/common.py b/powerline/segments/common.py index 41e75b77..b5cd1016 100644 --- a/powerline/segments/common.py +++ b/powerline/segments/common.py @@ -707,13 +707,13 @@ class NetworkLoadSegment(KwThreadedSegment): total = activity interface = name - if interface in self.interfaces: + try: idata = self.interfaces[interface] try: idata['prev'] = idata['last'] except KeyError: pass - else: + except KeyError: idata = {} if self.run_once: idata['prev'] = (monotonic(), _get_bytes(interface)) diff --git a/tests/test_config_reload.py b/tests/test_config_reload.py index f74aa411..224baf4d 100644 --- a/tests/test_config_reload.py +++ b/tests/test_config_reload.py @@ -141,7 +141,7 @@ class TestConfigReload(TestCase): self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') self.assertAccessEvents('config', 'themes/test/nonexistent') # It should normally handle file missing error - self.assertEqual(p.logger._pop_msgs(), ['exception:test:Failed to create renderer: themes/test/nonexistent']) + self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: themes/test/nonexistent']) config['config']['ext']['test']['theme'] = 'default' add_watcher_events(p, 'config') @@ -154,7 +154,7 @@ class TestConfigReload(TestCase): self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') self.assertAccessEvents('config', 'colorschemes/test/nonexistent') # It should normally handle file missing error - self.assertEqual(p.logger._pop_msgs(), ['exception:test:Failed to create renderer: colorschemes/test/nonexistent']) + self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: colorschemes/test/nonexistent']) config['config']['ext']['test']['colorscheme'] = '2' add_watcher_events(p, 'config') @@ -187,7 +187,7 @@ class TestConfigReload(TestCase): add_watcher_events(p, 'config') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') self.assertAccessEvents('config') - self.assertIn('exception:test:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs()) + self.assertIn('exception:test:powerline:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs()) config['colorschemes/test/nonexistentraise'] = { 'groups': {