From 4f9224774f7ec7c1c8ed4fedef2f2b62390064d2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 May 2009 01:04:35 -0600 Subject: Added Param 'include' and 'exclude' kwargs; added frontend.UsesParams base class with methods implementing the filtering to restrict params to only certain contexts --- ipalib/config.py | 4 +- ipalib/frontend.py | 107 ++++++++++++++++++++++++++++++++++- ipalib/parameters.py | 52 +++++++++++++++-- tests/test_ipalib/test_parameters.py | 39 ++++++++++++- 4 files changed, 194 insertions(+), 8 deletions(-) diff --git a/ipalib/config.py b/ipalib/config.py index f955ce08..37ab2748 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -198,9 +198,11 @@ class Env(object): __locked = False - def __init__(self): + def __init__(self, **initialize): object.__setattr__(self, '_Env__d', {}) object.__setattr__(self, '_Env__done', set()) + if initialize: + self._merge(**initialize) def __lock__(self): """ diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 27c85695..83cf2601 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -25,7 +25,7 @@ import re import inspect import plugable from plugable import lock, check_name -from parameters import create_param, Param, Str, Flag, Password +from parameters import create_param, parse_param_spec, Param, Str, Flag, Password from util import make_repr from errors import ZeroArgumentError, MaxArgumentError, OverlapError, RequiresRoot @@ -43,6 +43,111 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True +class UsesParams(plugable.Plugin): + """ + Base class for plugins that use param namespaces. + """ + + def _get_params_iterable(self, name): + """ + Return an iterable of params defined by the attribute named ``name``. + + A sequence of params can be defined one of three ways: as a ``tuple``; + as a callable that returns an iterable; or as a param spec (a `Param` or + ``str`` instance). This method returns a uniform iterable regardless of + how the param sequence was defined. + + For example, when defined with a tuple: + + >>> class ByTuple(UsesParams): + ... takes_args = (Param('foo'), Param('bar')) + ... + >>> by_tuple = ByTuple() + >>> list(by_tuple._get_params_iterable('takes_args')) + [Param('foo'), Param('bar')] + + Or you can define your param sequence with a callable when you need to + reference attributes on your plugin instance (for validation rules, + etc.). For example: + + >>> class ByCallable(UsesParams): + ... def takes_args(self): + ... yield Param('foo', self.validate_foo) + ... yield Param('bar', self.validate_bar) + ... + ... def validate_foo(self, _, value, **kw): + ... if value != 'Foo': + ... return _("must be 'Foo'") + ... + ... def validate_bar(self, _, value, **kw): + ... if value != 'Bar': + ... return _("must be 'Bar'") + ... + >>> by_callable = ByCallable() + >>> list(by_callable._get_params_iterable('takes_args')) + [Param('foo', validate_foo), Param('bar', validate_bar)] + + Lastly, as a convenience for when a param sequence contains a single + param, your defining attribute may a param spec (either a `Param` + or an ``str`` instance). For example: + + >>> class BySpec(UsesParams): + ... takes_args = Param('foo') + ... takes_options = 'bar?' + ... + >>> by_spec = BySpec() + >>> list(by_spec._get_params_iterable('takes_args')) + [Param('foo')] + >>> list(by_spec._get_params_iterable('takes_options')) + ['bar?'] + + For information on how an ``str`` param spec is interpreted, see the + `create_param()` and `parse_param_spec()` functions in the + `ipalib.parameters` module. + + Also see `UsesParams._filter_params_by_context()`. + """ + attr = getattr(self, name) + if isinstance(attr, (Param, str)): + return (attr,) + if callable(attr): + return attr() + return attr + + def _filter_params_by_context(self, name, env=None): + """ + Filter params on attribute named ``name`` by environment ``env``. + + For example: + + >>> from ipalib.config import Env + >>> class Example(UsesParams): + ... takes_args = ( + ... Str('foo_only', include=['foo']), + ... Str('not_bar', exclude=['bar']), + ... 'both', + ... ) + ... + >>> eg = Example() + >>> foo = Env(context='foo') + >>> bar = Env(context='bar') + >>> another = Env(context='another') + >>> (foo.context, bar.context, another.context) + ('foo', 'bar', 'another') + >>> list(eg._filter_params_by_context('takes_args', foo)) + [Str('foo_only', include=['foo']), Str('not_bar', exclude=['bar']), Str('both')] + >>> list(eg._filter_params_by_context('takes_args', bar)) + [Str('both')] + >>> list(eg._filter_params_by_context('takes_args', another)) + [Str('not_bar', exclude=['bar']), Str('both')] + """ + env = getattr(self, 'env', env) + for spec in self._get_params_iterable(name): + param = create_param(spec) + if env is None or param.use_in_context(env): + yield param + + class Command(plugable.Plugin): """ A public IPA atomic operation. diff --git a/ipalib/parameters.py b/ipalib/parameters.py index 13fd50b5..0c2748ee 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -232,7 +232,8 @@ class Param(ReadOnly): ('autofill', bool, False), ('query', bool, False), ('attribute', bool, False), - ('limit_to', frozenset, None), + ('include', frozenset, None), + ('exclude', frozenset, None), ('flags', frozenset, frozenset()), # The 'default' kwarg gets appended in Param.__init__(): @@ -328,6 +329,16 @@ class Param(ReadOnly): else: self._get_default = None + # Check that only 'include' or 'exclude' was provided: + if None not in (self.include, self.exclude): + raise ValueError( + '%s: cannot have both %s=%r and %s=%r' % ( + self.nice, + 'include', self.include, + 'exclude', self.exclude, + ) + ) + # Check that all the rules are callable self.class_rules = tuple(class_rules) self.rules = rules @@ -345,12 +356,18 @@ class Param(ReadOnly): """ Return an expresion that could construct this `Param` instance. """ - return make_repr( + return '%s(%s)' % ( self.__class__.__name__, - self.param_spec, - **self.__kw + ', '.join(self.__repr_iter()) ) + def __repr_iter(self): + yield repr(self.param_spec) + for rule in self.rules: + yield rule.__name__ + for key in sorted(self.__kw): + yield '%s=%r' % (key, self.__kw[key]) + def __call__(self, value, **kw): """ One stop shopping. @@ -362,6 +379,33 @@ class Param(ReadOnly): self.validate(value) return value + def use_in_context(self, env): + """ + Return ``True`` if this param should be used in ``env.context``. + + For example: + + >>> from ipalib.config import Env + >>> server = Env() + >>> server.context = 'server' + >>> client = Env() + >>> client.context = 'client' + >>> param = Param('my_param', include=['server', 'webui']) + >>> param.use_in_context(server) + True + >>> param.use_in_context(client) + False + + So that a subclass can add additional logic basic on other environment + variables, the `config.Env` instance is passed in rather than just the + value of ``env.context``. + """ + if self.include is not None: + return (env.context in self.include) + if self.exclude is not None: + return (env.context not in self.exclude) + return True + def safe_value(self, value): """ Return a value safe for logging. diff --git a/tests/test_ipalib/test_parameters.py b/tests/test_ipalib/test_parameters.py index ea098a95..db9c01ef 100644 --- a/tests/test_ipalib/test_parameters.py +++ b/tests/test_ipalib/test_parameters.py @@ -27,7 +27,7 @@ from inspect import isclass from tests.util import raises, ClassChecker, read_only from tests.util import dummy_ugettext, assert_equal from tests.data import binary_bytes, utf8_bytes, unicode_str -from ipalib import parameters, request, errors +from ipalib import parameters, request, errors, config from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR, NULLS @@ -168,7 +168,8 @@ class test_Param(ClassChecker): assert o.autofill is False assert o.query is False assert o.attribute is False - assert o.limit_to is None + assert o.include is None + assert o.exclude is None assert o.flags == frozenset() # Test that ValueError is raised when a kwarg from a subclass @@ -223,6 +224,18 @@ class test_Param(ClassChecker): "Param('my_param')", 'default_from', 'create_default', ) + # Test that ValueError is raised if you provide both include and + # exclude: + e = raises(ValueError, self.cls, 'my_param', + include=['server', 'foo'], + exclude=['client', 'bar'], + ) + assert str(e) == '%s: cannot have both %s=%r and %s=%r' % ( + "Param('my_param')", + 'include', frozenset(['server', 'foo']), + 'exclude', frozenset(['client', 'bar']), + ) + # Test that _get_default gets set: call1 = lambda first, last: first[0] + last call2 = lambda **kw: 'The Default' @@ -245,6 +258,28 @@ class test_Param(ClassChecker): o = self.cls('name', multivalue=True) assert repr(o) == "Param('name', multivalue=True)" + def test_use_in_context(self): + """ + Test the `ipalib.parameters.Param.use_in_context` method. + """ + set1 = ('one', 'two', 'three') + set2 = ('four', 'five', 'six') + param1 = self.cls('param1') + param2 = self.cls('param2', include=set1) + param3 = self.cls('param3', exclude=set2) + for context in set1: + env = config.Env() + env.context = context + assert param1.use_in_context(env) is True, context + assert param2.use_in_context(env) is True, context + assert param3.use_in_context(env) is True, context + for context in set2: + env = config.Env() + env.context = context + assert param1.use_in_context(env) is True, context + assert param2.use_in_context(env) is False, context + assert param3.use_in_context(env) is False, context + def test_safe_value(self): """ Test the `ipalib.parameters.Param.safe_value` method. -- cgit