mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Copied DefaultFrom into parameter.py; added docstring to new Param.normalize() method; more work and unit tests in new Param class
This commit is contained in:
parent
5c47b56d14
commit
64ae4bc986
@ -72,6 +72,110 @@ def parse_param_spec(spec):
|
|||||||
return (spec, dict(required=True, multivalue=False))
|
return (spec, dict(required=True, multivalue=False))
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultFrom(ReadOnly):
|
||||||
|
"""
|
||||||
|
Derive a default value from other supplied values.
|
||||||
|
|
||||||
|
For example, say you wanted to create a default for the user's login from
|
||||||
|
the user's first and last names. It could be implemented like this:
|
||||||
|
|
||||||
|
>>> login = DefaultFrom(lambda first, last: first[0] + last)
|
||||||
|
>>> login(first='John', last='Doe')
|
||||||
|
'JDoe'
|
||||||
|
|
||||||
|
If you do not explicitly provide keys when you create a DefaultFrom
|
||||||
|
instance, the keys are implicitly derived from your callback by
|
||||||
|
inspecting ``callback.func_code.co_varnames``. The keys are available
|
||||||
|
through the ``DefaultFrom.keys`` instance attribute, like this:
|
||||||
|
|
||||||
|
>>> login.keys
|
||||||
|
('first', 'last')
|
||||||
|
|
||||||
|
The callback is available through the ``DefaultFrom.callback`` instance
|
||||||
|
attribute, like this:
|
||||||
|
|
||||||
|
>>> login.callback # doctest:+ELLIPSIS
|
||||||
|
<function <lambda> at 0x...>
|
||||||
|
>>> login.callback.func_code.co_varnames # The keys
|
||||||
|
('first', 'last')
|
||||||
|
|
||||||
|
The keys can be explicitly provided as optional positional arguments after
|
||||||
|
the callback. For example, this is equivalent to the ``login`` instance
|
||||||
|
above:
|
||||||
|
|
||||||
|
>>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last')
|
||||||
|
>>> login2.keys
|
||||||
|
('first', 'last')
|
||||||
|
>>> login2.callback.func_code.co_varnames # Not the keys
|
||||||
|
('a', 'b')
|
||||||
|
>>> login2(first='John', last='Doe')
|
||||||
|
'JDoe'
|
||||||
|
|
||||||
|
If any keys are missing when calling your DefaultFrom instance, your
|
||||||
|
callback is not called and None is returned. For example:
|
||||||
|
|
||||||
|
>>> login(first='John', lastname='Doe') is None
|
||||||
|
True
|
||||||
|
>>> login() is None
|
||||||
|
True
|
||||||
|
|
||||||
|
Any additional keys are simply ignored, like this:
|
||||||
|
|
||||||
|
>>> login(last='Doe', first='John', middle='Whatever')
|
||||||
|
'JDoe'
|
||||||
|
|
||||||
|
As above, because `DefaultFrom.__call__` takes only pure keyword
|
||||||
|
arguments, they can be supplied in any order.
|
||||||
|
|
||||||
|
Of course, the callback need not be a lambda expression. This third
|
||||||
|
example is equivalent to both the ``login`` and ``login2`` instances
|
||||||
|
above:
|
||||||
|
|
||||||
|
>>> def get_login(first, last):
|
||||||
|
... return first[0] + last
|
||||||
|
...
|
||||||
|
>>> login3 = DefaultFrom(get_login)
|
||||||
|
>>> login3.keys
|
||||||
|
('first', 'last')
|
||||||
|
>>> login3.callback.func_code.co_varnames
|
||||||
|
('first', 'last')
|
||||||
|
>>> login3(first='John', last='Doe')
|
||||||
|
'JDoe'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, callback, *keys):
|
||||||
|
"""
|
||||||
|
:param callback: The callable to call when all keys are present.
|
||||||
|
:param keys: Optional keys used for source values.
|
||||||
|
"""
|
||||||
|
if not callable(callback):
|
||||||
|
raise TypeError('callback must be callable; got %r' % callback)
|
||||||
|
self.callback = callback
|
||||||
|
if len(keys) == 0:
|
||||||
|
fc = callback.func_code
|
||||||
|
self.keys = fc.co_varnames[:fc.co_argcount]
|
||||||
|
else:
|
||||||
|
self.keys = keys
|
||||||
|
for key in self.keys:
|
||||||
|
if type(key) is not str:
|
||||||
|
raise_TypeError(key, str, 'keys')
|
||||||
|
lock(self)
|
||||||
|
|
||||||
|
def __call__(self, **kw):
|
||||||
|
"""
|
||||||
|
If all keys are present, calls the callback; otherwise returns None.
|
||||||
|
|
||||||
|
:param kw: The keyword arguments.
|
||||||
|
"""
|
||||||
|
vals = tuple(kw.get(k, None) for k in self.keys)
|
||||||
|
if None in vals:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
return self.callback(*vals)
|
||||||
|
except StandardError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Param(ReadOnly):
|
class Param(ReadOnly):
|
||||||
"""
|
"""
|
||||||
Base class for all IPA types.
|
Base class for all IPA types.
|
||||||
@ -89,14 +193,23 @@ class Param(ReadOnly):
|
|||||||
flags=(frozenset, frozenset()),
|
flags=(frozenset, frozenset()),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, name, kwargs, **overrides):
|
def __init__(self, name, kwargs, **override):
|
||||||
self.param_spec = name
|
self.param_spec = name
|
||||||
|
self.__override = dict(override)
|
||||||
|
if not ('required' in override or 'multivalue' in override):
|
||||||
|
(name, kw_from_spec) = parse_param_spec(name)
|
||||||
|
override.update(kw_from_spec)
|
||||||
self.name = check_name(name)
|
self.name = check_name(name)
|
||||||
|
if 'cli_name' not in override:
|
||||||
|
override['cli_name'] = self.name
|
||||||
|
df = override.get('default_from', None)
|
||||||
|
if callable(df) and not isinstance(df, DefaultFrom):
|
||||||
|
override['default_from'] = DefaultFrom(df)
|
||||||
kwargs = dict(kwargs)
|
kwargs = dict(kwargs)
|
||||||
assert set(self.__kwargs).intersection(kwargs) == set()
|
assert set(self.__kwargs).intersection(kwargs) == set()
|
||||||
kwargs.update(self.__kwargs)
|
kwargs.update(self.__kwargs)
|
||||||
for (key, (kind, default)) in kwargs.iteritems():
|
for (key, (kind, default)) in kwargs.iteritems():
|
||||||
value = overrides.get(key, default)
|
value = override.get(key, default)
|
||||||
if value is None:
|
if value is None:
|
||||||
if kind is bool:
|
if kind is bool:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -104,7 +217,8 @@ class Param(ReadOnly):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
type(kind) is type and type(value) is not kind or
|
type(kind) is type and type(value) is not kind
|
||||||
|
or
|
||||||
type(kind) is tuple and not isinstance(value, kind)
|
type(kind) is tuple and not isinstance(value, kind)
|
||||||
):
|
):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -119,12 +233,35 @@ class Param(ReadOnly):
|
|||||||
key, self.__class__.__name__)
|
key, self.__class__.__name__)
|
||||||
)
|
)
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
check_name(self.cli_name)
|
||||||
lock(self)
|
lock(self)
|
||||||
|
|
||||||
def normalize(self, value):
|
def normalize(self, value):
|
||||||
"""
|
"""
|
||||||
|
Normalize ``value`` using normalizer callback.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> param = Str('telephone',
|
||||||
|
... normalizer=lambda value: value.replace('.', '-')
|
||||||
|
... )
|
||||||
|
>>> param.normalize(u'800.123.4567')
|
||||||
|
u'800-123-4567'
|
||||||
|
|
||||||
|
(Note that `Str` is a subclass of `Param`.)
|
||||||
|
|
||||||
|
If this `Param` instance was created with a normalizer callback and
|
||||||
|
``value`` is a unicode instance, the normalizer callback is called and
|
||||||
|
*its* return value is returned.
|
||||||
|
|
||||||
|
On the other hand, if this `Param` instance was *not* created with a
|
||||||
|
normalizer callback, if ``value`` is *not* a unicode instance, or if an
|
||||||
|
exception is caught when calling the normalizer callback, ``value`` is
|
||||||
|
returned unchanged.
|
||||||
|
|
||||||
|
:param value: A proposed value for this parameter.
|
||||||
"""
|
"""
|
||||||
if self.__normalize is None:
|
if self.normalizer is None:
|
||||||
return value
|
return value
|
||||||
if self.multivalue:
|
if self.multivalue:
|
||||||
if type(value) in (tuple, list):
|
if type(value) in (tuple, list):
|
||||||
@ -143,7 +280,7 @@ class Param(ReadOnly):
|
|||||||
if type(value) is not unicode:
|
if type(value) is not unicode:
|
||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
return self.__normalize(value)
|
return self.normalizer(value)
|
||||||
except StandardError:
|
except StandardError:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -22,12 +22,67 @@
|
|||||||
Test the `ipalib.parameter` module.
|
Test the `ipalib.parameter` module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from tests.util import raises, ClassChecker
|
from tests.util import raises, ClassChecker, read_only
|
||||||
from tests.data import binary_bytes, utf8_bytes, unicode_str
|
from tests.data import binary_bytes, utf8_bytes, unicode_str
|
||||||
from ipalib import parameter
|
from ipalib import parameter
|
||||||
from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR
|
from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
class test_DefaultFrom(ClassChecker):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.parameter.DefaultFrom` class.
|
||||||
|
"""
|
||||||
|
_cls = parameter.DefaultFrom
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.parameter.DefaultFrom.__init__` method.
|
||||||
|
"""
|
||||||
|
def callback(*args):
|
||||||
|
return args
|
||||||
|
keys = ('givenname', 'sn')
|
||||||
|
o = self.cls(callback, *keys)
|
||||||
|
assert read_only(o, 'callback') is callback
|
||||||
|
assert read_only(o, 'keys') == keys
|
||||||
|
lam = lambda first, last: first[0] + last
|
||||||
|
o = self.cls(lam)
|
||||||
|
assert read_only(o, 'keys') == ('first', 'last')
|
||||||
|
|
||||||
|
def test_call(self):
|
||||||
|
"""
|
||||||
|
Test the `ipalib.parameter.DefaultFrom.__call__` method.
|
||||||
|
"""
|
||||||
|
def callback(givenname, sn):
|
||||||
|
return givenname[0] + sn[0]
|
||||||
|
keys = ('givenname', 'sn')
|
||||||
|
o = self.cls(callback, *keys)
|
||||||
|
kw = dict(
|
||||||
|
givenname='John',
|
||||||
|
sn='Public',
|
||||||
|
hello='world',
|
||||||
|
)
|
||||||
|
assert o(**kw) == 'JP'
|
||||||
|
assert o() is None
|
||||||
|
for key in ('givenname', 'sn'):
|
||||||
|
kw_copy = dict(kw)
|
||||||
|
del kw_copy[key]
|
||||||
|
assert o(**kw_copy) is None
|
||||||
|
|
||||||
|
# Test using implied keys:
|
||||||
|
o = self.cls(lambda first, last: first[0] + last)
|
||||||
|
assert o(first='john', last='doe') == 'jdoe'
|
||||||
|
assert o(first='', last='doe') is None
|
||||||
|
assert o(one='john', two='doe') is None
|
||||||
|
|
||||||
|
# Test that co_varnames slice is used:
|
||||||
|
def callback2(first, last):
|
||||||
|
letter = first[0]
|
||||||
|
return letter + last
|
||||||
|
o = self.cls(callback2)
|
||||||
|
assert o.keys == ('first', 'last')
|
||||||
|
assert o(first='john', last='doe') == 'jdoe'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_param_spec():
|
def test_parse_param_spec():
|
||||||
"""
|
"""
|
||||||
Test the `ipalib.parameter.parse_param_spec` function.
|
Test the `ipalib.parameter.parse_param_spec` function.
|
||||||
@ -51,8 +106,11 @@ class test_Param(ClassChecker):
|
|||||||
"""
|
"""
|
||||||
name = 'my_param'
|
name = 'my_param'
|
||||||
o = self.cls(name, {})
|
o = self.cls(name, {})
|
||||||
|
assert o.__islocked__() is True
|
||||||
|
|
||||||
|
# Test default values:
|
||||||
assert o.name is name
|
assert o.name is name
|
||||||
# assert o.cli_name is name
|
assert o.cli_name is name
|
||||||
assert o.doc == ''
|
assert o.doc == ''
|
||||||
assert o.required is True
|
assert o.required is True
|
||||||
assert o.multivalue is False
|
assert o.multivalue is False
|
||||||
@ -61,7 +119,9 @@ class test_Param(ClassChecker):
|
|||||||
assert o.default is None
|
assert o.default is None
|
||||||
assert o.default_from is None
|
assert o.default_from is None
|
||||||
assert o.flags == frozenset()
|
assert o.flags == frozenset()
|
||||||
assert o.__islocked__() is True
|
|
||||||
|
# Test that ValueError is raised when a kwarg from a subclass
|
||||||
|
# conflicts with an attribute:
|
||||||
kwarg = dict(convert=(callable, None))
|
kwarg = dict(convert=(callable, None))
|
||||||
e = raises(ValueError, self.cls, name, kwarg)
|
e = raises(ValueError, self.cls, name, kwarg)
|
||||||
assert str(e) == "kwarg 'convert' conflicts with attribute on Param"
|
assert str(e) == "kwarg 'convert' conflicts with attribute on Param"
|
||||||
@ -69,13 +129,15 @@ class test_Param(ClassChecker):
|
|||||||
pass
|
pass
|
||||||
e = raises(ValueError, Subclass, name, kwarg)
|
e = raises(ValueError, Subclass, name, kwarg)
|
||||||
assert str(e) == "kwarg 'convert' conflicts with attribute on Subclass"
|
assert str(e) == "kwarg 'convert' conflicts with attribute on Subclass"
|
||||||
|
|
||||||
|
# Test type validation of keyword arguments:
|
||||||
kwargs = dict(
|
kwargs = dict(
|
||||||
extra1=(bool, True),
|
extra1=(bool, True),
|
||||||
extra2=(str, 'Hello'),
|
extra2=(str, 'Hello'),
|
||||||
extra3=((int, float), 42),
|
extra3=((int, float), 42),
|
||||||
extra4=(callable, lambda whatever: whatever + 7),
|
extra4=(callable, lambda whatever: whatever + 7),
|
||||||
)
|
)
|
||||||
# Check that we don't accept None if kind is bool:
|
# Note: we don't accept None if kind is bool:
|
||||||
e = raises(TypeError, self.cls, 'my_param', kwargs, extra1=None)
|
e = raises(TypeError, self.cls, 'my_param', kwargs, extra1=None)
|
||||||
assert str(e) == TYPE_ERROR % ('extra1', bool, None, type(None))
|
assert str(e) == TYPE_ERROR % ('extra1', bool, None, type(None))
|
||||||
for (key, (kind, default)) in kwargs.items():
|
for (key, (kind, default)) in kwargs.items():
|
||||||
@ -88,7 +150,7 @@ class test_Param(ClassChecker):
|
|||||||
assert str(e) == CALLABLE_ERROR % (key, value, type(value))
|
assert str(e) == CALLABLE_ERROR % (key, value, type(value))
|
||||||
else:
|
else:
|
||||||
assert str(e) == TYPE_ERROR % (key, kind, value, type(value))
|
assert str(e) == TYPE_ERROR % (key, kind, value, type(value))
|
||||||
if kind is bool:
|
if kind is bool: # See note above
|
||||||
continue
|
continue
|
||||||
# Test with None:
|
# Test with None:
|
||||||
overrides = {key: None}
|
overrides = {key: None}
|
||||||
|
Loading…
Reference in New Issue
Block a user