summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/config.py4
-rw-r--r--ipalib/frontend.py107
-rw-r--r--ipalib/parameters.py52
-rw-r--r--tests/test_ipalib/test_parameters.py39
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.