summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Pokorný <jpokorny@redhat.com>2014-09-04 22:28:52 +0200
committerJan Pokorný <jpokorny@redhat.com>2014-09-04 22:28:52 +0200
commit749603e2faa6f44c26883cac83c41faad6b81fd4 (patch)
tree30ced06bd655ebf8fc09ac32a26bf1c4ca4aa1bb
parentc584b8dfa318fc409660b974671162a5af1f49ca (diff)
downloadclufter-749603e2faa6f44c26883cac83c41faad6b81fd4.tar.gz
clufter-749603e2faa6f44c26883cac83c41faad6b81fd4.tar.xz
clufter-749603e2faa6f44c26883cac83c41faad6b81fd4.zip
Solve a "read-only dict as API" question once forever
Signed-off-by: Jan Pokorný <jpokorny@redhat.com>
-rw-r--r--command_context.py117
-rw-r--r--command_manager.py14
-rw-r--r--filter_manager.py6
-rw-r--r--format.py4
-rw-r--r--format_manager.py5
-rw-r--r--main.py6
-rw-r--r--plugin_registry.py37
-rw-r--r--tests/command_context.py4
-rw-r--r--utils.py4
-rw-r--r--utils_prog.py111
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ý <jpokorny @at@ Red Hat .dot. com>"
-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 @@ class CommandContextBase(MutableMapping):
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 @@ class CommandContext(CommandContextBase):
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 @@ class CommandManager(PluginManager):
@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 @@ class CommandManager(PluginManager):
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 @@ class CommandManager(PluginManager):
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 @@ class FilterManager(PluginManager):
@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 @@ from .utils import arg2wrapped, args2tuple, args2unwrapped, \
immutable, \
popattr, \
tuplist
+from .utils_prog import ProtectedDict
from .utils_xml import rng_get_start, rng_pivot
log = getLogger(__name__)
@@ -177,7 +178,8 @@ class Format(object):
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 [<global option> ...] [<cmd> [<cmd option ...>]]",
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 imp
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 @@ class PluginRegistry(type):
# 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 @@ class PluginRegistry(type):
@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 @@ class PluginRegistry(type):
@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 @@ class PluginManager(object):
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ý <jpokorny @at@ Red Hat .dot. com>"
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 subprocess import Popen
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)
#