summaryrefslogtreecommitdiffstats
path: root/ipalib
diff options
context:
space:
mode:
authorJason Gerard DeRose <jderose@redhat.com>2009-05-20 15:19:09 -0600
committerRob Crittenden <rcritten@redhat.com>2009-05-21 14:32:45 -0400
commit7e58b29a92157fad40b50ef31f8c075b9dc363b7 (patch)
tree5f541cde18209bda0bd5df7c34365ce59d4c70b2 /ipalib
parent7b93f7bbd7d52132503a3c5691841c3e757616f9 (diff)
downloadfreeipa-7e58b29a92157fad40b50ef31f8c075b9dc363b7.tar.gz
freeipa-7e58b29a92157fad40b50ef31f8c075b9dc363b7.tar.xz
freeipa-7e58b29a92157fad40b50ef31f8c075b9dc363b7.zip
Completed Param.use_in_context() functionality, which is now used by Command and Object
Diffstat (limited to 'ipalib')
-rw-r--r--ipalib/__init__.py14
-rw-r--r--ipalib/frontend.py320
-rw-r--r--ipalib/parameters.py54
3 files changed, 298 insertions, 90 deletions
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 7108f65ec..4ad58d6e9 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -409,9 +409,9 @@ For example:
>>> class nudge(Command):
... """Takes one argument, one option"""
...
-... takes_args = ['programmer']
+... takes_args = ('programmer',)
...
-... takes_options = [Str('stuff', default=u'documentation')]
+... takes_options = (Str('stuff', default=u'documentation'))
...
... def execute(self, programmer, **kw):
... return '%s, go write more %s!' % (programmer, kw['stuff'])
@@ -462,7 +462,7 @@ here is a quick teaser:
>>> from ipalib import Int
>>> class create_player(Command):
-... takes_options = [
+... takes_options = (
... 'first',
... 'last',
... Str('nick',
@@ -470,7 +470,7 @@ here is a quick teaser:
... default_from=lambda first, last: first[0] + last,
... ),
... Int('points', default=0),
-... ]
+... )
...
>>> cp = create_player()
>>> cp.finalize()
@@ -573,9 +573,9 @@ For example, say we setup a command like this:
>>> class show_items(Command):
...
-... takes_args = ['key?']
+... takes_args = ('key?',)
...
-... takes_options = [Flag('reverse')]
+... takes_options = (Flag('reverse'),)
...
... def execute(self, key, **options):
... items = dict(
@@ -660,7 +660,7 @@ For example:
>>> class paint_house(Command):
...
-... takes_args = ['color']
+... takes_args = 'color'
...
... def execute(self, color):
... """Uses self.log.error()"""
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 83cf26015..54e962a76 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -23,8 +23,8 @@ Base classes for all front-end plugins.
import re
import inspect
-import plugable
-from plugable import lock, check_name
+from base import lock, check_name, NameSpace
+from plugable import Plugin
from parameters import create_param, parse_param_spec, Param, Str, Flag, Password
from util import make_repr
@@ -43,12 +43,162 @@ def is_rule(obj):
return callable(obj) and getattr(obj, RULE_FLAG, False) is True
-class UsesParams(plugable.Plugin):
+class HasParam(Plugin):
"""
- Base class for plugins that use param namespaces.
+ Base class for plugins that have `Param` `NameSpace` attributes.
+
+ Subclasses of `HasParam` will on one or more attributes store `NameSpace`
+ instances containing zero or more `Param` instances. These parameters might
+ describe, for example, the arguments and options a command takes, or the
+ attributes an LDAP entry can include, or whatever else the subclass sees
+ fit.
+
+ Although the interface a subclass must implement is very simple, it must
+ conform to a specific naming convention: if you want a namespace
+ ``SubClass.foo``, you must define a ``Subclass.takes_foo`` attribute and a
+ ``SubCLass.get_foo()`` method, and you may optionally define a
+ ``SubClass.check_foo()`` method.
+
+
+ A quick big-picture example
+ ===========================
+
+ Say you want the ``options`` instance attribute on your subclass to be a
+ `Param` `NameSpace`... then according to the enforced naming convention,
+ your subclass must define a ``takes_options`` attribute and a
+ ``get_options()`` method. For example:
+
+ >>> from ipalib import Str, Int
+ >>> class Example(HasParam):
+ ...
+ ... options = None # This will be replaced with your namespace
+ ...
+ ... takes_options = (Str('one'), Int('two'))
+ ...
+ ... def get_options(self):
+ ... return self._get_param_iterable('options')
+ ...
+ >>> eg = Example()
+
+ The ``Example.takes_options`` attribute is a ``tuple`` defining the
+ parameters you want your ``Example.options`` namespace to contain. Your
+ ``Example.takes_options`` attribute will be accessed via
+ `HasParam._get_param_iterable()`, which, among other things, enforces the
+ ``('takes_' + name)`` naming convention. For example:
+
+ >>> eg._get_param_iterable('options')
+ (Str('one'), Int('two'))
+
+ The ``Example.get_options()`` method simply returns
+ ``Example.takes_options`` by calling `HasParam._get_param_iterable()`. Your
+ ``Example.get_options()`` method will be called via
+ `HasParam._filter_param_by_context()`, which, among other things, enforces
+ the ``('get_' + name)`` naming convention. For example:
+
+ >>> list(eg._filter_param_by_context('options'))
+ [Str('one'), Int('two')]
+
+ At this point, the ``eg.options`` instance attribute is still ``None``:
+
+ >>> eg.options is None
+ True
+
+ `HasParam._create_param_namespace()` will create the ``eg.options``
+ namespace from the parameters yielded by
+ `HasParam._filter_param_by_context()`. For example:
+
+ >>> eg._create_param_namespace('options')
+ >>> eg.options
+ NameSpace(<2 members>, sort=False)
+ >>> list(eg.options) # Like dict.__iter__()
+ ['one', 'two']
+
+ Your subclass can optionally define a ``check_options()`` method to perform
+ sanity checks. If it exists, the ``check_options()`` method is called by
+ `HasParam._create_param_namespace()` with a single value, the `NameSpace`
+ instance it created. For example:
+
+ >>> class Example2(Example):
+ ...
+ ... def check_options(self, namespace):
+ ... for param in namespace(): # Like dict.itervalues()
+ ... if param.name == 'three':
+ ... raise ValueError("I dislike the param 'three'")
+ ... print ' ** Looks good! **' # Note output below
+ ...
+ >>> eg = Example2()
+ >>> eg._create_param_namespace('options')
+ ** Looks good! **
+ >>> eg.options
+ NameSpace(<2 members>, sort=False)
+
+ However, if we subclass again and add a `Param` named ``'three'``:
+
+ >>> class Example3(Example2):
+ ...
+ ... takes_options = (Str('one'), Int('two'), Str('three'))
+ ...
+ >>> eg = Example3()
+ >>> eg._create_param_namespace('options')
+ Traceback (most recent call last):
+ ...
+ ValueError: I dislike the param 'three'
+ >>> eg.options is None # eg.options was not set
+ True
+
+
+ The Devil and the details
+ =========================
+
+ In the above example, ``takes_options`` is a ``tuple``, but it can also be
+ a param spec (see `create_param()`), or a callable that returns an iterable
+ containing one or more param spec. Regardless of how ``takes_options`` is
+ defined, `HasParam._get_param_iterable()` will return a uniform iterable,
+ conveniently hiding the details.
+
+ The above example uses the simplest ``get_options()`` method possible, but
+ you could instead implement a ``get_options()`` method that would, for
+ example, produce (or withhold) certain parameters based on the whether
+ certain plugins are loaded.
+
+ Think of ``takes_options`` as declarative, a simple definition of *what*
+ parameters should be included in the namespace. You should only implement
+ a ``takes_options()`` method if a `Param` must reference attributes on your
+ plugin instance (for example, for validation rules); you should not use a
+ ``takes_options()`` method to filter the parameters or add any other
+ procedural behaviour.
+
+ On the other hand, think of the ``get_options()`` method as imperative, a
+ procedure for *how* the parameters should be created and filtered. In the
+ example above the *how* just returns the *what* unchanged, but arbitrary
+ logic can be implemented in the ``get_options()`` method. For example, you
+ might filter certain parameters from ``takes_options`` base on some
+ criteria, or you might insert additional parameters provided by other
+ plugins.
+
+ The typical use case for using ``get_options()`` this way is to procedurally
+ generate the arguments and options for all the CRUD commands operating on a
+ specific LDAP object: the `Object` plugin defines the possible LDAP entry
+ attributes (as `Param`), and then the CRUD commands intelligently build
+ their ``args`` and ``options`` namespaces based on which attribute is the
+ primary key. In this way new LDAP attributes (aka parameters) can be added
+ to the single point of definition (the `Object` plugin), and all the
+ corresponding CRUD commands pick up these new parameters without requiring
+ modification. For an example of how this is done, see the
+ `ipalib.crud.Create` base class.
+
+ However, there is one type of filtering you should not implement in your
+ ``get_options()`` method, because it's already provided at a higher level:
+ you should not filter parameters based on the value of ``api.env.context``
+ nor (preferably) on any values in ``api.env``.
+ `HasParam._filter_param_by_context()` already does this by calling
+ `Param.use_in_context()` for each parameter. Although the base
+ `Param.use_in_context()` implementation makes a decision solely on the value
+ of ``api.env.context``, subclasses can override this with implementations
+ that consider arbitrary ``api.env`` values.
"""
- def _get_params_iterable(self, name):
+ def _get_param_iterable(self, name):
"""
Return an iterable of params defined by the attribute named ``name``.
@@ -59,18 +209,18 @@ class UsesParams(plugable.Plugin):
For example, when defined with a tuple:
- >>> class ByTuple(UsesParams):
+ >>> class ByTuple(HasParam):
... takes_args = (Param('foo'), Param('bar'))
...
>>> by_tuple = ByTuple()
- >>> list(by_tuple._get_params_iterable('takes_args'))
+ >>> list(by_tuple._get_param_iterable('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):
+ >>> class ByCallable(HasParam):
... def takes_args(self):
... yield Param('foo', self.validate_foo)
... yield Param('bar', self.validate_bar)
@@ -84,71 +234,105 @@ class UsesParams(plugable.Plugin):
... return _("must be 'Bar'")
...
>>> by_callable = ByCallable()
- >>> list(by_callable._get_params_iterable('takes_args'))
+ >>> list(by_callable._get_param_iterable('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):
+ >>> class BySpec(HasParam):
... takes_args = Param('foo')
... takes_options = 'bar?'
...
>>> by_spec = BySpec()
- >>> list(by_spec._get_params_iterable('takes_args'))
+ >>> list(by_spec._get_param_iterable('args'))
[Param('foo')]
- >>> list(by_spec._get_params_iterable('takes_options'))
+ >>> list(by_spec._get_param_iterable('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()`.
+ Also see `HasParam._filter_param_by_context()`.
"""
- attr = getattr(self, name)
- if isinstance(attr, (Param, str)):
- return (attr,)
- if callable(attr):
- return attr()
- return attr
+ takes_name = 'takes_' + name
+ takes = getattr(self, takes_name, None)
+ if type(takes) is tuple:
+ return takes
+ if isinstance(takes, (Param, str)):
+ return (takes,)
+ if callable(takes):
+ return takes()
+ if takes is None:
+ return tuple()
+ raise TypeError(
+ '%s.%s must be a tuple, callable, or spec; got %r' % (
+ self.name, takes_name, takes
+ )
+ )
- def _filter_params_by_context(self, name, env=None):
+ def _filter_param_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):
+ >>> class Example(HasParam):
+ ...
... takes_args = (
... Str('foo_only', include=['foo']),
... Str('not_bar', exclude=['bar']),
... 'both',
... )
...
+ ... def get_args(self):
+ ... return self._get_param_iterable('args')
+ ...
+ ...
>>> 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))
+ >>> list(eg._filter_param_by_context('args', foo))
[Str('foo_only', include=['foo']), Str('not_bar', exclude=['bar']), Str('both')]
- >>> list(eg._filter_params_by_context('takes_args', bar))
+ >>> list(eg._filter_param_by_context('args', bar))
[Str('both')]
- >>> list(eg._filter_params_by_context('takes_args', another))
+ >>> list(eg._filter_param_by_context('args', another))
[Str('not_bar', exclude=['bar']), Str('both')]
"""
env = getattr(self, 'env', env)
- for spec in self._get_params_iterable(name):
+ get_name = 'get_' + name
+ if not hasattr(self, get_name):
+ raise NotImplementedError(
+ '%s.%s()' % (self.name, get_name)
+ )
+ get = getattr(self, get_name)
+ if not callable(get):
+ raise TypeError(
+ '%s.%s must be a callable; got %r' % (self.name, get_name, get)
+ )
+ for spec in get():
param = create_param(spec)
if env is None or param.use_in_context(env):
yield param
+ def _create_param_namespace(self, name, env=None):
+ namespace = NameSpace(
+ self._filter_param_by_context(name, env),
+ sort=False
+ )
+ check = getattr(self, 'check_' + name, None)
+ if callable(check):
+ check(namespace)
+ setattr(self, name, namespace)
-class Command(plugable.Plugin):
+
+class Command(HasParam):
"""
A public IPA atomic operation.
@@ -372,7 +556,7 @@ class Command(plugable.Plugin):
>>> from ipalib import Str
>>> class my_command(Command):
- ... takes_args = [Str('color', default=u'Red')]
+ ... takes_args = Str('color', default=u'Red')
...
>>> c = my_command()
>>> c.finalize()
@@ -453,65 +637,46 @@ class Command(plugable.Plugin):
loaded in self.api to determine what their custom `Command.get_args`
and `Command.get_options` methods should yield.
"""
- self.args = plugable.NameSpace(self.__create_args(), sort=False)
+ self._create_param_namespace('args')
if len(self.args) == 0 or not self.args[-1].multivalue:
self.max_args = len(self.args)
else:
self.max_args = None
- self.options = plugable.NameSpace(
- (create_param(spec) for spec in self.get_options()),
- sort=False
- )
+ self._create_param_namespace('options')
def get_key(p):
if p.required:
if p.default_from is None:
return 0
return 1
return 2
- self.params = plugable.NameSpace(
+ self.params = NameSpace(
sorted(tuple(self.args()) + tuple(self.options()), key=get_key),
sort=False
)
super(Command, self).finalize()
- def _get_takes(self, name):
- attr = getattr(self, name)
- if isinstance(attr, (Param, str)):
- return (attr,)
- if callable(attr):
- return attr()
- return attr
-
def get_args(self):
"""
Iterate through parameters for ``Command.args`` namespace.
- Subclasses can override this to customize how the arguments
- are determined. For an example of why this can be useful,
- see `ipalib.crud.Mod`.
+ This method gets called by `HasParam._create_param_namespace()`.
+
+ Subclasses can override this to customize how the arguments are
+ determined. For an example of why this can be useful, see the
+ `ipalib.crud.Create` subclass.
"""
- for arg in self._get_takes('takes_args'):
+ for arg in self._get_param_iterable('args'):
yield arg
- def get_options(self):
+ def check_args(self, args):
"""
- Iterate through parameters for ``Command.options`` namespace.
+ Sanity test for args namespace.
- Subclasses can override this to customize how the options
- are determined. For an example of why this can be useful,
- see `ipalib.crud.Mod`.
- """
- for option in self._get_takes('takes_options'):
- yield option
-
- def __create_args(self):
- """
- Generator used to create args namespace.
+ This method gets called by `HasParam._create_param_namespace()`.
"""
optional = False
multivalue = False
- for arg in self.get_args():
- arg = create_param(arg)
+ for arg in args():
if optional and arg.required:
raise ValueError(
'%s: required argument after optional' % arg.name
@@ -524,7 +689,19 @@ class Command(plugable.Plugin):
optional = True
if arg.multivalue:
multivalue = True
- yield arg
+
+ def get_options(self):
+ """
+ Iterate through parameters for ``Command.options`` namespace.
+
+ This method gets called by `HasParam._create_param_namespace()`.
+
+ Subclasses can override this to customize how the arguments are
+ determined. For an example of why this can be useful, see the
+ `ipalib.crud.Create` subclass.
+ """
+ for option in self._get_param_iterable('options'):
+ yield option
class LocalOrRemote(Command):
@@ -558,7 +735,7 @@ class LocalOrRemote(Command):
return self.execute(*args, **options)
-class Object(plugable.Plugin):
+class Object(HasParam):
__public__ = frozenset((
'backend',
'methods',
@@ -582,15 +759,13 @@ class Object(plugable.Plugin):
def set_api(self, api):
super(Object, self).set_api(api)
- self.methods = plugable.NameSpace(
+ self.methods = NameSpace(
self.__get_attrs('Method'), sort=False
)
- self.properties = plugable.NameSpace(
+ self.properties = NameSpace(
self.__get_attrs('Property'), sort=False
)
- self.params = plugable.NameSpace(
- self.__get_params(), sort=False
- )
+ self._create_param_namespace('params')
pkeys = filter(lambda p: p.primary_key, self.params())
if len(pkeys) > 1:
raise ValueError(
@@ -601,7 +776,7 @@ class Object(plugable.Plugin):
)
if len(pkeys) == 1:
self.primary_key = pkeys[0]
- self.params_minus_pk = plugable.NameSpace(
+ self.params_minus_pk = NameSpace(
filter(lambda p: not p.primary_key, self.params()), sort=False
)
@@ -630,14 +805,17 @@ class Object(plugable.Plugin):
if name not in self.api:
return
namespace = self.api[name]
- assert type(namespace) is plugable.NameSpace
+ assert type(namespace) is NameSpace
for proxy in namespace(): # Equivalent to dict.itervalues()
if proxy.obj_name == self.name:
yield proxy.__clone__('attr_name')
- def __get_params(self):
+ def get_params(self):
+ """
+ This method gets called by `HasParam._create_param_namespace()`.
+ """
props = self.properties.__todict__()
- for spec in self.takes_params:
+ for spec in self._get_param_iterable('params'):
if type(spec) is str:
key = spec.rstrip('?*+')
else:
@@ -657,7 +835,7 @@ class Object(plugable.Plugin):
yield prop.param
-class Attribute(plugable.Plugin):
+class Attribute(Plugin):
"""
Base class implementing the attribute-to-object association.
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 0c2748ee5..2ecc6178d 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -381,24 +381,54 @@ class Param(ReadOnly):
def use_in_context(self, env):
"""
- Return ``True`` if this param should be used in ``env.context``.
+ Return ``True`` if this parameter should be used in ``env.context``.
- For example:
+ If a parameter is created with niether the ``include`` nor the
+ ``exclude`` kwarg, this method will always return ``True``. 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)
+ >>> param = Param('my_param')
+ >>> param.use_in_context(Env(context='foo'))
+ True
+ >>> param.use_in_context(Env(context='bar'))
+ True
+
+ If a parameter is created with an ``include`` kwarg, this method will
+ only return ``True`` if ``env.context`` is in ``include``. For example:
+
+ >>> param = Param('my_param', include=['foo', 'whatever'])
+ >>> param.include
+ frozenset(['foo', 'whatever'])
+ >>> param.use_in_context(Env(context='foo'))
True
- >>> param.use_in_context(client)
+ >>> param.use_in_context(Env(context='bar'))
+ False
+
+ If a paremeter is created with an ``exclude`` kwarg, this method will
+ only return ``True`` if ``env.context`` is not in ``exclude``. For
+ example:
+
+ >>> param = Param('my_param', exclude=['foo', 'whatever'])
+ >>> param.exclude
+ frozenset(['foo', 'whatever'])
+ >>> param.use_in_context(Env(context='foo'))
False
+ >>> param.use_in_context(Env(context='bar'))
+ True
+
+ Note that the ``include`` and ``exclude`` kwargs are mutually exclusive
+ and that at most one can be suppelied to `Param.__init__()`. For
+ example:
+
+ >>> param = Param('nope', include=['foo'], exclude=['bar'])
+ Traceback (most recent call last):
+ ...
+ ValueError: Param('nope'): cannot have both include=frozenset(['foo']) and exclude=frozenset(['bar'])
- 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``.
+ So that subclasses can add additional logic based on other environment
+ variables, the entire `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)