Skip to content

Commit

Permalink
Solve a "read-only dict as API" question once forever
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Pokorný <[email protected]>
  • Loading branch information
jnpkrn committed Sep 4, 2014
1 parent c584b8d commit 749603e
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 135 deletions.
117 changes: 25 additions & 92 deletions command_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
14 changes: 4 additions & 10 deletions command_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"""
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions filter_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
4 changes: 3 additions & 1 deletion format.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
immutable, \
popattr, \
tuplist
from .utils_prog import ProtectedDict
from .utils_xml import rng_get_start, rng_pivot

log = getLogger(__name__)
Expand Down Expand Up @@ -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 = {}
Expand Down
5 changes: 1 addition & 4 deletions format_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 2 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...>]]",
Expand Down
37 changes: 19 additions & 18 deletions plugin_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
4 changes: 2 additions & 2 deletions tests/command_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
4 changes: 4 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 749603e

Please sign in to comment.