From 749603e2faa6f44c26883cac83c41faad6b81fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pokorn=C3=BD?= Date: Thu, 4 Sep 2014 22:28:52 +0200 Subject: [PATCH] Solve a "read-only dict as API" question once forever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- command_context.py | 117 +++++++++------------------------------ command_manager.py | 14 ++--- filter_manager.py | 6 +- format.py | 4 +- format_manager.py | 5 +- main.py | 6 +- plugin_registry.py | 37 +++++++------ tests/command_context.py | 4 +- utils.py | 4 ++ utils_prog.py | 111 ++++++++++++++++++++++++++++++++++++- 10 files changed, 173 insertions(+), 135 deletions(-) diff --git a/command_context.py b/command_context.py index f1458a4..c0dad2a 100644 --- a/command_context.py +++ b/command_context.py @@ -5,64 +5,28 @@ """Command context, i.e., state distributed along filters chain""" __author__ = "Jan Pokorný " -from collections import MutableMapping, MutableSequence, MutableSet import logging +from collections import MutableMapping from .error import ClufterError from .utils import isinstanceexcept +from .utils_prog import TweakedDict log = logging.getLogger(__name__) -mutables = (MutableMapping, MutableSequence, MutableSet) class CommandContextError(ClufterError): pass -class notaint_context(object): - def __init__(self, self_outer, exit_off): - self._exit_off = exit_off - self._self_outer = self_outer - def __enter__(self): - self._exit_off |= not self._self_outer._notaint - self._self_outer._notaint = True - def __exit__(self, *exc): - self._self_outer._notaint = not self._exit_off - - -class CommandContextBase(MutableMapping): +class CommandContextBase(TweakedDict): """Object representing command context""" - def __init__(self, initial=None, parent=None, bypass=False, notaint=False): - self._parent = parent if parent is not None else self - self._notaint = notaint - if isinstance(initial, CommandContextBase): - assert initial._parent is None - self._dict = initial._dict # trust dict to have expected props - self._notaint = initial._notaint - else: - self._dict = {} - if initial is not None: - if not isinstance(initial, MutableMapping): - initial = dict(initial) - map(lambda (k, v): self.setdefault(k, v, bypass=bypass), - initial.iteritems()) - - def __delitem__(self, key): - del self._dict[key] - - def __getitem__(self, key): - # any notainting parent incl. self is an authority for us - try: - ret = self._dict[key] - except KeyError: - if self._parent is self: - raise - ret = self._parent[key] - if (isinstanceexcept(ret, mutables, CommandContextBase) - and any(getattr(p, '_notaint', False) for p in self.anabasis())): - ret = ret.copy() - return ret + def __init__(self, initial=None, parent=None, **kwargs): + super(CommandContextBase, self).__init__(initial=initial, **kwargs) + if parent is not None: + self._parent = parent + @property def anabasis(self): """Traverse nested contexts hierarchy upwards""" cur = self @@ -72,48 +36,33 @@ def anabasis(self): break cur = cur._parent - def setdefault(self, key, *args, **kwargs): - """Allows implicit arrangements to be bypassed via `bypass` flag""" - assert len(args) < 2 - bypass = kwargs.get('bypass', False) - if bypass: - return self._dict.setdefault(key, *args) - try: - return self.__getitem__(key) - except KeyError: - if not args: - raise - self.__setitem__(key, *args) - return args[0] - - def __iter__(self): - return iter(self._dict) - - def __len__(self): - return len(self._dict) - - def __repr__(self): - return "<{0}: {1}>".format(repr(self.__class__), repr(self._dict)) + @property + def parent(self): + return self._parent def __setitem__(self, key, value): # XXX value could be also any valid dict constructor argument - if any(getattr(p, '_notaint', False) for p in self.anabasis()): - raise RuntimeError("Cannot set item to notaint context") + if any(getattr(p, '_notaint', False) for p in self.anabasis): + raise RuntimeError("Cannot set item in notaint context") self._dict[key] = CommandContextBase(initial=value, parent=self) \ if isinstanceexcept(value, MutableMapping, CommandContextBase) \ else value - @property - def parent(self): - return self._parent - - def prevented_taint(self, exit_off=False): - """Context manager to safely yield underlying dicts while applied""" - return notaint_context(self, exit_off) - class CommandContext(CommandContextBase): + class notaint_context(CommandContextBase.notaint_context): + def __init__(self, self_outer, exit_off): + super(self.__class__, self).__init__(self_outer, exit_off) + self._fc = self_outer['__filter_context__'] \ + .prevented_taint(exit_off) + def __enter__(self): + super(self.__class__, self).__enter__() + self._fc.__enter__() + def __exit__(self, *exc): + self._fc.__exit__() + super(self.__class__, self).__exit__() + def __init__(self, *args, **kwargs): # filter_context ... where global arguments for filters to be stored # filters ... where filter instance + arguments hybrid is stored @@ -168,19 +117,3 @@ def filter(self, which=None): else: ret = self['__filter_context__'] return ret - - def prevented_taint(self, exit_off=False): - """Context manager to safely yield underlying dicts while applied""" - class notaint_command_context(notaint_context): - def __init__(self, self_outer, exit_off): - super(notaint_command_context, self).__init__(self_outer, - exit_off) - self._fc = self_outer['__filter_context__'] \ - .prevented_taint(exit_off) - def __enter__(self): - super(notaint_command_context, self).__enter__() - self._fc.__enter__() - def __exit__(self, *exc): - self._fc.__exit__() - super(notaint_command_context, self).__exit__() - return notaint_command_context(self, exit_off) diff --git a/command_manager.py b/command_manager.py index 2de9f1a..0de06a2 100644 --- a/command_manager.py +++ b/command_manager.py @@ -35,10 +35,7 @@ class CommandManager(PluginManager): def _init_handle_plugins(self, commands, flt_mgr, *args): log.debug("Commands before resolving: {0}".format(commands)) - self._commands = self._resolve(flt_mgr.filters, commands, *args) - - def __iter__(self): - return self._commands.itervalues() + return self._resolve(flt_mgr.filters, commands, *args) @classmethod def implicit(cls, *args): @@ -92,10 +89,7 @@ def _resolve(filters, commands, system='', system_extra=''): @property def commands(self): - return self._commands.copy() - - def completion(self, completion): - return completion(self._commands.iteritems()) + return self._plugins def __call__(self, parser, args=None): """Follow up of the entry point, facade to particular commands""" @@ -106,7 +100,7 @@ def __call__(self, parser, args=None): or args[0] while isinstance(command, basestring): canonical_cmd = command - command = self._commands.get(command, None) + command = self._plugins.get(command, None) if not command: raise CommandNotFoundError(cmd) @@ -152,7 +146,7 @@ def pretty_cmds(self, text_width=76, linesep_width=1, max(tuple(len(name) for name, _ in cat)) if cat else 0) for i, cat in enumerate( bifilter(lambda (name, obj): not isinstance(obj, basestring), - self._commands.iteritems()) + self._plugins.iteritems()) ) ] width = max(i[1] for i in cmds_aliases) + linesep_width diff --git a/filter_manager.py b/filter_manager.py index b8f9cf1..7d96113 100644 --- a/filter_manager.py +++ b/filter_manager.py @@ -25,7 +25,7 @@ class FilterManager(PluginManager): def _init_handle_plugins(self, filters, fmt_mgr): log.debug("Filters before resolving: {0}".format(filters)) - self._filters = self._resolve(fmt_mgr.formats, filters) + return self._resolve(fmt_mgr.formats, filters) @staticmethod def _resolve(formats, filters): @@ -49,9 +49,9 @@ def get_composite_onthefly(formats): @property def filters(self): - return self._filters.copy() + return self._plugins def __call__(self, which, in_decl, **kwargs): - flt = self._filters[which] + flt = self._plugins[which] in_obj = flt.in_format.as_instance(*in_decl) return flt(in_obj, **kwargs) diff --git a/format.py b/format.py index 3f8487d..11a62be 100644 --- a/format.py +++ b/format.py @@ -28,6 +28,7 @@ immutable, \ popattr, \ tuplist +from .utils_prog import ProtectedDict from .utils_xml import rng_get_start, rng_pivot log = getLogger(__name__) @@ -177,7 +178,8 @@ def produce(self, protocol, *args, **kwargs): def __init__(self, protocol, *args, **kwargs): """Format constructor, i.e., object = concrete internal data""" - self._representations = {} + rs = {} + self._representations, self._representations_ro = rs, ProtectedDict(rs) validator_specs = kwargs.pop('validator_specs', {}) default = validator_specs.setdefault('', None) # None ~ don't track validators = {} diff --git a/format_manager.py b/format_manager.py index 0498931..4e0f3fe 100644 --- a/format_manager.py +++ b/format_manager.py @@ -13,9 +13,6 @@ class FormatManager(PluginManager): """Class responsible for available formats of data to be converted""" _default_registry = formats - def _init_handle_plugins(self, formats): - self._formats = formats - @property def formats(self): - return self._formats.copy() + return self._plugins diff --git a/main.py b/main.py index 3cdf9ac..5cc08b8 100644 --- a/main.py +++ b/main.py @@ -194,11 +194,9 @@ def run(argv=None, *args): if opts.list: print cmds elif opts.completion: - print cm.completion( - Completion.get_completion(opts.completion, - prog, + c = Completion.get_completion(opts.completion, prog, opts_common, opts_main, opts_nonmain) - ) + print c(cm.plugins.iteritems()) else: print parser.format_customized_help( usage="%prog [ ...] [ []]", diff --git a/plugin_registry.py b/plugin_registry.py index 2862b36..40a1403 100644 --- a/plugin_registry.py +++ b/plugin_registry.py @@ -9,12 +9,11 @@ import logging from os import extsep, walk from os.path import abspath, dirname, join, splitext -from collections import Mapping from contextlib import contextmanager from sys import modules from .utils import classproperty, hybridproperty, tuplist -from .utils_prog import cli_decor, cli_undecor +from .utils_prog import ProtectedDict, cli_decor log = logging.getLogger(__name__) @@ -76,16 +75,6 @@ def __new__(registry, name, bases, attrs): # these are relevant for both (1) + (2) # - class ProxyPlugins(Mapping): - def __init__(self, d): - self._d = d - def __getitem__(self, name): - return self._d[name] - def __iter__(self): - return iter(self._d) - def __len__(self): - return len(self._d) - @classmethod def probe(registry, name, bases, attrs=None): """Meta-magic to register plugin""" @@ -133,9 +122,7 @@ def namespace(registry): @classproperty def plugins(registry): - if registry._proxy_plugins is None: - registry._proxy_plugins = registry.ProxyPlugins(registry._plugins) - return registry._proxy_plugins + return registry._plugins_ro # # these are relevant for use case (2) @@ -177,7 +164,9 @@ def _context(registry, paths): @classmethod def setup(registry, reset=False): """Implicit setup upon first registry involvement or external reset""" - attrs = ('_path_context', None), ('_path_mapping', {}), ('_plugins', {}) + ps = {} + attrs = (('_path_context', None), ('_path_mapping', {}), + ('_plugins', ps), ('_plugins_ro', ProtectedDict(ps))) if reset: map(lambda (a, d): setattr(registry, a, d), attrs) else: @@ -230,11 +219,23 @@ def __init__(self, *args, **kwargs): paths = kwargs.pop('paths', ()) plugins = registry.discover(paths) plugins.update(kwargs.pop(registry.name if registry else '', {})) - self._init_handle_plugins(plugins, *args, **kwargs) + self._plugins = ProtectedDict( + self._init_handle_plugins(plugins, *args, **kwargs), + ) def _init_handle_plugins(self, plugins, *args, **kwargs): - raise NotImplementedError('subclasses should implement') + log.info("Plugins under `{0}' manager left intact".format(self + ._registry + .name)) + return plugins @property def registry(self): return self._registry + + @property + def plugins(self): + return self._plugins + + #def __iter__(self): + # return self._plugins.itervalues() diff --git a/tests/command_context.py b/tests/command_context.py index 1076bdf..dcc9088 100644 --- a/tests/command_context.py +++ b/tests/command_context.py @@ -16,13 +16,13 @@ class TestCommandContextBase(unittest.TestCase): def testAnabasisConstructor(self): ccb = CommandContextBase({'a': {'b': {'c': {'d': {'e': 42}}}}}) e = ccb['a']['b']['c']['d'] - self.assertTrue(len(tuple(e.anabasis())) == 5) + self.assertTrue(len(tuple(e.anabasis)) == 5) def testAnabasisBuilt(self): ccb = CommandContextBase() ccb['a'] = {'b': {'c': {'d': {'e': 42}}}} e = ccb['a']['b']['c']['d'] - self.assertTrue(len(tuple(e.anabasis())) == 5) + self.assertTrue(len(tuple(e.anabasis)) == 5) def testPreventedTaint(self): ccb = CommandContextBase({'a': 42}) diff --git a/utils.py b/utils.py index 529d269..d43fdc3 100644 --- a/utils.py +++ b/utils.py @@ -53,6 +53,10 @@ def isinstanceexcept(subj, obj, exc=()): return isinstance(subj, obj) and not isinstance(subj, exc) +def areinstances(obj1, obj2): + isinstance(obj1, obj2.__class__) or isinstance(obj2, obj1.__class__) + + def popattr(obj, what, *args): assert len(args) < 2 ret = getattr(obj, what, *args) diff --git a/utils_prog.py b/utils_prog.py index 267c7b2..1f01fd0 100644 --- a/utils_prog.py +++ b/utils_prog.py @@ -6,6 +6,7 @@ __author__ = "Jan Pokorný " import logging +from collections import Mapping, MutableMapping, MutableSequence, MutableSet from optparse import make_option from os import environ, pathsep from os.path import abspath, dirname, samefile, \ @@ -16,7 +17,115 @@ from sys import stderr, stdin from .error import ClufterError -from .utils import filterdict_pop, func_defaults_varnames, selfaware, tuplist +from .utils import areinstances, \ + filterdict_pop, \ + func_defaults_varnames, \ + isinstanceexcept, \ + selfaware, \ + tuplist + + +# +# generics +# + +mutables = (MutableMapping, MutableSequence, MutableSet) + +class TweakedDict(MutableMapping): + """Object representing command context""" + + class notaint_context(object): + def __init__(self, self_outer, exit_off): + self._exit_off = exit_off + self._self_outer = self_outer + def __enter__(self): + self._exit_off |= not self._self_outer._notaint + self._self_outer._notaint = True + def __exit__(self, *exc): + self._self_outer._notaint = not self._exit_off + + def __init__(self, initial=None, bypass=False, notaint=False): + self._parent = self + self._notaint = True + if areinstances(initial, self): + assert initial._parent is initial + self._dict = initial._dict # trust dict to have expected props + notaint = initial._notaint + else: + self._dict = {} + if initial is not None: + if not isinstance(initial, Mapping): + initial = dict(initial) + elif not isinstance(initial, MutableMapping): + # silently? follow the immutability + notaint = True + bypass = True + if bypass or notaint: + self._dict = initial + if not bypass: + # full examination + self._notaint = False # temporarily need to to allow + map(lambda (k, v): self.__setitem__(k, v), + initial.iteritems()) + self._notaint = notaint + + def __delitem__(self, key): + if any(getattr(p, '_notaint', False) for p in self.anabasis): + raise RuntimeError("Cannot del item in notaint context") + del self._dict[key] + + def __getitem__(self, key): + # any notainting parent incl. self is an authority for us + try: + ret = self._dict[key] + except KeyError: + if self._parent is self: + raise + ret = self._parent[key] + if (isinstanceexcept(ret, mutables, TweakedDict) + and any(getattr(p, '_notaint', False) for p in self.anabasis)): + ret = ret.copy() + return ret + + @property + def anabasis(self): + """Traverse nested contexts hierarchy upwards""" + return (self, ) + + def setdefault(self, key, *args, **kwargs): + """Allows implicit arrangements to be bypassed via `bypass` flag""" + assert len(args) < 2 + bypass = kwargs.get('bypass', False) + if bypass: # for when adding MutableMapping that should be untouched + return self._dict.setdefault(key, *args) + try: + return self.__getitem__(key) + except KeyError: + if not args: + raise + self.__setitem__(key, *args) + return args[0] + + def __iter__(self): + return iter(self._dict) + + def __len__(self): + return len(self._dict) + + def __repr__(self): + return "<{0}: {1}>".format(repr(self.__class__), repr(self._dict)) + + def __setitem__(self, key, value): + # XXX value could be also any valid dict constructor argument + if any(getattr(p, '_notaint', False) for p in self.anabasis): + raise RuntimeError("Cannot set item in notaint context") + self._dict[key] = value + + def prevented_taint(self, exit_off=False): + """Context manager to safely yield underlying dicts while applied""" + return self.notaint_context(self, exit_off) + +ProtectedDict = lambda track: TweakedDict(track, notaint=True, bypass=True) #