From 2842e85d88f4c6cedfdf41d3cce2ee7449369871 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Sep 2008 23:51:03 +0000 Subject: 317: Renamed public.py to frontend.py; renamed test_public.py to test_frontend.py --- ipalib/frontend.py | 571 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 ipalib/frontend.py (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py new file mode 100644 index 00000000..678bd2de --- /dev/null +++ b/ipalib/frontend.py @@ -0,0 +1,571 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Base classes for the public plugable.API instance, which the XML-RPC, CLI, +and UI all use. +""" + +import re +import inspect +import plugable +from plugable import lock, check_name +import errors +from errors import check_type, check_isinstance, raise_TypeError +import ipa_types + + +RULE_FLAG = 'validation_rule' + +def rule(obj): + assert not hasattr(obj, RULE_FLAG) + setattr(obj, RULE_FLAG, True) + return obj + +def is_rule(obj): + return callable(obj) and getattr(obj, RULE_FLAG, False) is True + + +class DefaultFrom(plugable.ReadOnly): + """ + Derives a default for one value using other supplied values. + + Here is an example that constructs a user's initials from his first + and last name: + + >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') + >>> df(first='John', last='Doe') # Both keys + 'JD' + >>> df() is None # Returns None if any key is missing + True + >>> df(first='John', middle='Q') is None # Still returns None + True + """ + def __init__(self, callback, *keys): + """ + :param callback: The callable to call when all ``keys`` are present. + :param keys: The keys used to map from keyword to position arguments. + """ + assert callable(callback), 'not a callable: %r' % callback + assert len(keys) > 0, 'must have at least one key' + for key in keys: + assert type(key) is str, 'not an str: %r' % key + self.callback = callback + self.keys = 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 None + try: + return self.callback(*vals) + except Exception: + return None + + +class Param(plugable.ReadOnly): + def __init__(self, name, type_, + doc='', + required=False, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + normalize=None): + self.name = check_name(name) + self.doc = check_type(doc, str, 'doc') + self.type = check_isinstance(type_, ipa_types.Type, 'type_') + self.required = check_type(required, bool, 'required') + self.multivalue = check_type(multivalue, bool, 'multivalue') + self.default = default + self.default_from = check_type(default_from, + DefaultFrom, 'default_from', allow_none=True) + self.__normalize = normalize + self.rules = (type_.validate,) + rules + lock(self) + + def __convert_scalar(self, value, index=None): + if value is None: + raise TypeError('value cannot be None') + converted = self.type(value) + if converted is None: + raise errors.ConversionError( + self.name, value, self.type, index=index + ) + return converted + + def convert(self, value): + if self.multivalue: + if type(value) in (tuple, list): + return tuple( + self.__convert_scalar(v, i) for (i, v) in enumerate(value) + ) + return (self.__convert_scalar(value, 0),) # tuple + return self.__convert_scalar(value) + + def __normalize_scalar(self, value): + if not isinstance(value, basestring): + raise_TypeError(value, basestring, 'value') + try: + return self.__normalize(value) + except Exception: + return value + + def normalize(self, value): + if self.__normalize is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple(self.__normalize_scalar(v) for v in value) + return (self.__normalize_scalar(value),) # tuple + return self.__normalize_scalar(value) + + def __validate_scalar(self, value, index=None): + if type(value) is not self.type.type: + raise_TypeError(value, self.type.type, 'value') + for rule in self.rules: + error = rule(value) + if error is not None: + raise errors.RuleError( + self.name, value, error, rule, index=index + ) + + def validate(self, value): + if self.multivalue: + if type(value) is not tuple: + raise_TypeError(value, tuple, 'value') + for (i, v) in enumerate(value): + self.__validate_scalar(v, i) + else: + self.__validate_scalar(value) + + def get_default(self, **kw): + if self.default_from is not None: + default = self.default_from(**kw) + if default is not None: + try: + return self.convert(self.normalize(default)) + except errors.ValidationError: + return None + return self.default + + def get_values(self): + if self.type.name in ('Enum', 'CallbackEnum'): + return self.type.values + return tuple() + + def __call__(self, value, **kw): + if value in ('', tuple(), []): + value = None + if value is None: + value = self.get_default(**kw) + if value is None: + if self.required: + raise errors.RequirementError(self.name) + return None + else: + value = self.convert(self.normalize(value)) + self.validate(value) + return value + + def __repr__(self): + return '%s(%r, %s())' % ( + self.__class__.__name__, + self.name, + self.type.name, + ) + + +def create_param(spec): + """ + Create a `Param` instance from a param spec string. + + If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. + + If ``spec`` is an str instance, then ``spec`` is parsed and an + appropriate `Param` instance is created and returned. + + The spec string determines the param name, whether the param is required, + and whether the param is multivalue according the following syntax: + + name => required=True, multivalue=False + name? => required=False, multivalue=False + name+ => required=True, multivalue=True + name* => required=False, multivalue=True + + :param spec: A spec string or a `Param` instance. + """ + if type(spec) is Param: + return spec + if type(spec) is not str: + raise TypeError( + 'create_param() takes %r or %r; got %r' % (str, Param, spec) + ) + if spec.endswith('?'): + kw = dict(required=False, multivalue=False) + name = spec[:-1] + elif spec.endswith('*'): + kw = dict(required=False, multivalue=True) + name = spec[:-1] + elif spec.endswith('+'): + kw = dict(required=True, multivalue=True) + name = spec[:-1] + else: + kw = dict(required=True, multivalue=False) + name = spec + return Param(name, ipa_types.Unicode(), **kw) + + +class Command(plugable.Plugin): + __public__ = frozenset(( + 'get_default', + 'convert', + 'normalize', + 'validate', + 'execute', + '__call__', + 'smart_option_order', + 'args', + 'options', + 'params', + 'args_to_kw', + 'kw_to_args', + )) + takes_options = tuple() + takes_args = tuple() + args = None + options = None + params = None + can_forward = True + + def finalize(self): + self.args = plugable.NameSpace(self.__create_args(), sort=False) + 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(self.__create_options(), sort=False) + self.params = plugable.NameSpace( + tuple(self.args()) + tuple(self.options()), sort=False + ) + super(Command, self).finalize() + + def get_args(self): + return self.takes_args + + def get_options(self): + return self.takes_options + + def __create_args(self): + optional = False + multivalue = False + for arg in self.get_args(): + arg = create_param(arg) + if optional and arg.required: + raise ValueError( + '%s: required argument after optional' % arg.name + ) + if multivalue: + raise ValueError( + '%s: only final argument can be multivalue' % arg.name + ) + if not arg.required: + optional = True + if arg.multivalue: + multivalue = True + yield arg + + def __create_options(self): + for option in self.get_options(): + yield create_param(option) + + def __convert_iter(self, kw): + for (key, value) in kw.iteritems(): + if key in self.params: + yield (key, self.params[key].convert(value)) + else: + yield (key, value) + + def convert(self, **kw): + return dict(self.__convert_iter(kw)) + + def __normalize_iter(self, kw): + for (key, value) in kw.iteritems(): + if key in self.params: + yield (key, self.params[key].normalize(value)) + else: + yield (key, value) + + def normalize(self, **kw): + return dict(self.__normalize_iter(kw)) + + def __get_default_iter(self, kw): + for param in self.params(): + if param.name not in kw: + value = param.get_default(**kw) + if value is not None: + yield(param.name, value) + + def get_default(self, **kw): + return dict(self.__get_default_iter(kw)) + + def validate(self, **kw): + for param in self.params(): + value = kw.get(param.name, None) + if value is not None: + param.validate(value) + elif param.required: + raise errors.RequirementError(param.name) + + def execute(self, *args, **kw): + print '%s.execute():' % self.name + print ' args =', args + print ' kw =', kw + + def __call__(self, *args, **kw): + if len(args) > 0: + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) + kw = self.normalize(**kw) + kw = self.convert(**kw) + kw.update(self.get_default(**kw)) + self.validate(**kw) + args = tuple(kw.pop(name) for name in self.args) + self.execute(*args, **kw) + + def args_to_kw(self, *values): + if self.max_args is not None and len(values) > self.max_args: + if self.max_args == 0: + raise errors.ArgumentError(self, 'takes no arguments') + if self.max_args == 1: + raise errors.ArgumentError(self, 'takes at most 1 argument') + raise errors.ArgumentError(self, + 'takes at most %d arguments' % len(self.args) + ) + return dict(self.__args_to_kw_iter(values)) + + def __args_to_kw_iter(self, values): + multivalue = False + for (i, arg) in enumerate(self.args()): + assert not multivalue + if len(values) > i: + if arg.multivalue: + multivalue = True + yield (arg.name, values[i:]) + else: + yield (arg.name, values[i]) + else: + break + + def kw_to_args(self, **kw): + return tuple(kw.get(name, None) for name in self.args) + + +class Object(plugable.Plugin): + __public__ = frozenset(( + 'Method', + 'Property', + 'params' + )) + __Method = None + __Property = None + takes_params = tuple() + + def __init__(self): + self.params = plugable.NameSpace( + (create_param(p) for p in self.takes_params), sort=False + ) + + def __create_params(self): + for param in self.takes_params: + yield create_param(param) + + def __get_Method(self): + return self.__Method + Method = property(__get_Method) + + def __get_Property(self): + return self.__Property + Property = property(__get_Property) + + def set_api(self, api): + super(Object, self).set_api(api) + self.__Method = self.__create_namespace('Method') + self.__Property = self.__create_namespace('Property') + + def __create_namespace(self, name): + return plugable.NameSpace(self.__filter_members(name)) + + def __filter_members(self, name): + namespace = getattr(self.api, name) + assert type(namespace) is plugable.NameSpace + for proxy in namespace(): # Equivalent to dict.itervalues() + if proxy.obj_name == self.name: + yield proxy.__clone__('attr_name') + + +class Attribute(plugable.Plugin): + __public__ = frozenset(( + 'obj', + 'obj_name', + )) + __obj = None + + def __init__(self): + m = re.match( + '^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', + self.__class__.__name__ + ) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) + + def __get_obj_name(self): + return self.__obj_name + obj_name = property(__get_obj_name) + + def __get_attr_name(self): + return self.__attr_name + attr_name = property(__get_attr_name) + + def __get_obj(self): + """ + Returns the obj instance this attribute is associated with, or None + if no association has been set. + """ + return self.__obj + obj = property(__get_obj) + + def set_api(self, api): + self.__obj = api.Object[self.obj_name] + super(Attribute, self).set_api(api) + + +class Method(Attribute, Command): + __public__ = Attribute.__public__.union(Command.__public__) + + def __init__(self): + Attribute.__init__(self) + Command.__init__(self) + + def get_options(self): + for option in self.takes_options: + yield option + if self.obj is not None and self.obj.Property is not None: + def get_key(p): + if p.param.required: + if p.param.default_from is None: + return 0 + return 1 + return 2 + for prop in sorted(self.obj.Property(), key=get_key): + yield prop.param + + +class Property(Attribute): + __public__ = frozenset(( + 'rules', + 'param', + 'type', + )).union(Attribute.__public__) + + type = ipa_types.Unicode() + required = False + multivalue = False + default = None + default_from = None + normalize = None + + def __init__(self): + super(Property, self).__init__() + self.rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + self.param = Param(self.attr_name, self.type, + doc=self.doc, + required=self.required, + multivalue=self.multivalue, + default=self.default, + default_from=self.default_from, + rules=self.rules, + normalize=self.normalize, + ) + + def __rules_iter(self): + """ + Iterates through the attributes in this instance to retrieve the + methods implementing validation rules. + """ + for name in dir(self.__class__): + if name.startswith('_'): + continue + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr + + +class Application(Command): + """ + Base class for commands register by an external application. + + Special commands that only apply to a particular application built atop + `ipalib` should subclass from ``Application``. + + Because ``Application`` subclasses from `Command`, plugins that subclass + from ``Application`` with be available in both the ``api.Command`` and + ``api.Application`` namespaces. + """ + + __public__ = frozenset(( + 'application', + 'set_application' + )).union(Command.__public__) + __application = None + + def __get_application(self): + """ + Returns external ``application`` object. + """ + return self.__application + application = property(__get_application) + + def set_application(self, application): + """ + Sets the external application object to ``application``. + """ + if self.__application is not None: + raise AttributeError( + '%s.application can only be set once' % self.name + ) + if application is None: + raise TypeError( + '%s.application cannot be None' % self.name + ) + object.__setattr__(self, '_Application__application', application) + assert self.application is application -- cgit From 81de10f176437053ac47bfee8f5ec81e38f2cf57 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 00:12:35 +0000 Subject: 319: Added new backend and tests.test_backend modules; added place-holder Backend class and corresponding unit tests --- ipalib/frontend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 678bd2de..489b874c 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -18,8 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Base classes for the public plugable.API instance, which the XML-RPC, CLI, -and UI all use. +Base classes for all front-end plugins. """ import re -- cgit From 19bbc48eb601bb942ed93776c05bf0c326970832 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 02:52:19 +0000 Subject: 323: Added Command.run() method that dispatches to execute() or forward(); added corresponding unit tests --- ipalib/frontend.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 489b874c..11d05d5f 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -343,6 +343,11 @@ class Command(plugable.Plugin): print ' args =', args print ' kw =', kw + def forward(self, *args, **kw): + print '%s.execute():' % self.name + print ' args =', args + print ' kw =', kw + def __call__(self, *args, **kw): if len(args) > 0: arg_kw = self.args_to_kw(*args) @@ -353,7 +358,15 @@ class Command(plugable.Plugin): kw.update(self.get_default(**kw)) self.validate(**kw) args = tuple(kw.pop(name) for name in self.args) - self.execute(*args, **kw) + return self.run(*args, **kw) + + def run(self, *args, **kw): + if self.api.env.in_server_context: + target = self.execute + else: + target = self.forward + object.__setattr__(self, 'run', target) + return target(*args, **kw) def args_to_kw(self, *values): if self.max_args is not None and len(values) > self.max_args: -- cgit From 3bf2da571488b6f1ef27527fa3bff0133b44c2f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 03:10:35 +0000 Subject: 324: Removed 'smart_option_order' from Command.__public__; cli commands help, console, and show_plugins now override Command.run() instead of Command.__call__() --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 11d05d5f..dbc3a62d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -245,7 +245,6 @@ class Command(plugable.Plugin): 'validate', 'execute', '__call__', - 'smart_option_order', 'args', 'options', 'params', -- cgit From eaf15d5a52b8438d1a0a5b59a9ace9660a703dce Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:35:40 +0000 Subject: 327: Improved formatting on show-api cli command --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index dbc3a62d..d4550f87 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -356,7 +356,7 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) + args = tuple(kw.pop(name, None) for name in self.args) return self.run(*args, **kw) def run(self, *args, **kw): -- cgit From 126b31de5581f4107d7d863f606a9adfa782f88a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:42:31 +0000 Subject: 328: Command.get_default() now returns defaults for all values not present, not just defaults that aren't None --- ipalib/frontend.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d4550f87..cb18a07a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -322,9 +322,7 @@ class Command(plugable.Plugin): def __get_default_iter(self, kw): for param in self.params(): if param.name not in kw: - value = param.get_default(**kw) - if value is not None: - yield(param.name, value) + yield (param.name, param.get_default(**kw)) def get_default(self, **kw): return dict(self.__get_default_iter(kw)) @@ -356,7 +354,7 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name, None) for name in self.args) + args = tuple(kw.pop(name) for name in self.args) return self.run(*args, **kw) def run(self, *args, **kw): -- cgit From 15b83ab1bf9d94e4f5cf292623d194b6a8094616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:46:49 +0000 Subject: 329: Command.convert() now converts all keys, not just keys in params --- ipalib/frontend.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index cb18a07a..fc397530 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -299,15 +299,10 @@ class Command(plugable.Plugin): for option in self.get_options(): yield create_param(option) - def __convert_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].convert(value)) - else: - yield (key, value) - def convert(self, **kw): - return dict(self.__convert_iter(kw)) + return dict( + (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() + ) def __normalize_iter(self, kw): for (key, value) in kw.iteritems(): -- cgit From 95abdcd7147399c9bb10adc2a04e41ddc97b2302 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:49:30 +0000 Subject: 330: Command.normalize() now normalizes all keys, not just keys in params --- ipalib/frontend.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index fc397530..f3264d81 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -304,15 +304,10 @@ class Command(plugable.Plugin): (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() ) - def __normalize_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].normalize(value)) - else: - yield (key, value) - def normalize(self, **kw): - return dict(self.__normalize_iter(kw)) + return dict( + (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() + ) def __get_default_iter(self, kw): for param in self.params(): -- cgit From d56f4c643b486bfbcb6523a0fe80252343fa594e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:11:46 +0000 Subject: 331: Param.normalize() no longer raises a TypeError when value in not a basestring --- ipalib/frontend.py | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f3264d81..59cdf69f 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,6 +105,38 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) + def __normalize_scalar(self, value): + if not isinstance(value, basestring): + return value + try: + return self.__normalize(value) + except StandardError: + return value + + def normalize(self, value): + """ + Normalize ``value`` using normalize callback. + + If this `Param` instance does not have a normalize callback, + ``value`` is returned unchanged. + + If this `Param` instance has a normalize callback and ``value`` is + a basestring, the normalize callback is called and its return value + is returned. + + If ``value`` is not a basestring, or if an exception is caught + when calling the normalize callback, ``value`` is returned unchanged. + + :param value: A proposed value for this parameter. + """ + if self.__normalize is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple(self.__normalize_scalar(v) for v in value) + return (self.__normalize_scalar(value),) # tuple + return self.__normalize_scalar(value) + def __convert_scalar(self, value, index=None): if value is None: raise TypeError('value cannot be None') @@ -124,22 +156,7 @@ class Param(plugable.ReadOnly): return (self.__convert_scalar(value, 0),) # tuple return self.__convert_scalar(value) - def __normalize_scalar(self, value): - if not isinstance(value, basestring): - raise_TypeError(value, basestring, 'value') - try: - return self.__normalize(value) - except Exception: - return value - def normalize(self, value): - if self.__normalize is None: - return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple(self.__normalize_scalar(v) for v in value) - return (self.__normalize_scalar(value),) # tuple - return self.__normalize_scalar(value) def __validate_scalar(self, value, index=None): if type(value) is not self.type.type: -- cgit From 6bedb15674ba941c15832ac84387a40ecd2a2879 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:25:12 +0000 Subject: 332: Param.normalize() now returns None if multivalue and len() == 0 --- ipalib/frontend.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 59cdf69f..5971f9df 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,6 +105,15 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) + def __if_multivalue(self, value, scalar): + if self.multivalue: + if type(value) in (tuple, list): + if len(value) == 0: + return None + return tuple(scalar(v) for v in value) + return (scalar(value),) # tuple + return scalar(value) + def __normalize_scalar(self, value): if not isinstance(value, basestring): return value @@ -131,11 +140,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple(self.__normalize_scalar(v) for v in value) - return (self.__normalize_scalar(value),) # tuple - return self.__normalize_scalar(value) + return self.__if_multivalue(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: -- cgit From 1125d420bdf453a0b51e58a85d447009dd1a99ff Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:35:19 +0000 Subject: 333: Param.convert() now uses name Param.__multivalue() helper method as Param.normalize() --- ipalib/frontend.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5971f9df..4a84ce98 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,16 +105,18 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def __if_multivalue(self, value, scalar): + def __multivalue(self, value, scalar): if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: return None - return tuple(scalar(v) for v in value) - return (scalar(value),) # tuple + return tuple( + scalar(v, i) for (i, v) in enumerate(value) + ) + return (scalar(value, 0),) # tuple return scalar(value) - def __normalize_scalar(self, value): + def __normalize_scalar(self, value, index=None): if not isinstance(value, basestring): return value try: @@ -140,7 +142,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - return self.__if_multivalue(value, self.__normalize_scalar) + return self.__multivalue(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: @@ -153,13 +155,7 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - self.__convert_scalar(v, i) for (i, v) in enumerate(value) - ) - return (self.__convert_scalar(value, 0),) # tuple - return self.__convert_scalar(value) + return self.__multivalue(value, self.__convert_scalar) -- cgit From 4215da30ad9de6467abe2c56f7a56f73001060b3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:36:48 +0000 Subject: 334: Renamed Command.__multivalue() helper method to Command.dispatch() --- ipalib/frontend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4a84ce98..d53a7d4d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,7 +105,7 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def __multivalue(self, value, scalar): + def __dispatch(self, value, scalar): if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: @@ -142,7 +142,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - return self.__multivalue(value, self.__normalize_scalar) + return self.__dispatch(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: @@ -155,7 +155,7 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - return self.__multivalue(value, self.__convert_scalar) + return self.__dispatch(value, self.__convert_scalar) -- cgit From e63c462f31bc34c5b19d243492c7644f423d55d0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:48:27 +0000 Subject: 335: If Command.__convert_scalar() is called with None, it now returns None instead of raising TypeError --- ipalib/frontend.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d53a7d4d..7d75fa17 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -106,6 +106,8 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): + if value is None: + return None if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: @@ -146,7 +148,7 @@ class Param(plugable.ReadOnly): def __convert_scalar(self, value, index=None): if value is None: - raise TypeError('value cannot be None') + return None converted = self.type(value) if converted is None: raise errors.ConversionError( @@ -155,9 +157,12 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - return self.__dispatch(value, self.__convert_scalar) - + """ + Convert/coerce ``value`` to Python type for this parameter. + :param value: A proposed value for this parameter. + """ + return self.__dispatch(value, self.__convert_scalar) def __validate_scalar(self, value, index=None): if type(value) is not self.type.type: -- cgit From fb57b919376322160df94aefd84bbebc52a6e53f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 07:05:43 +0000 Subject: 336: Param.__dispatch() now returns None for any in (None, '', u'', tuple(), []) regardless whether Param is multivalue --- ipalib/frontend.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7d75fa17..80c4050e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -106,12 +106,10 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): - if value is None: - return None + if value in (None, '', tuple(), []): + return if self.multivalue: if type(value) in (tuple, list): - if len(value) == 0: - return None return tuple( scalar(v, i) for (i, v) in enumerate(value) ) @@ -148,7 +146,7 @@ class Param(plugable.ReadOnly): def __convert_scalar(self, value, index=None): if value is None: - return None + return converted = self.type(value) if converted is None: raise errors.ConversionError( @@ -158,7 +156,12 @@ class Param(plugable.ReadOnly): def convert(self, value): """ - Convert/coerce ``value`` to Python type for this parameter. + Convert/coerce ``value`` to Python type for this `Param`. + + If ``value`` can not be converted, ConversionError is raised. + + If ``value`` is None, conversion is not attempted and None is + returned. :param value: A proposed value for this parameter. """ -- cgit From 744406958df66fb46ec43d80f9d788429953fda4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 07:56:31 +0000 Subject: 337: Some cleanup in Params; added docstrings for most all Param methods --- ipalib/frontend.py | 67 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 12 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 80c4050e..82d96643 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -85,6 +85,8 @@ class DefaultFrom(plugable.ReadOnly): class Param(plugable.ReadOnly): + __nones = (None, '', tuple(), []) + def __init__(self, name, type_, doc='', required=False, @@ -106,7 +108,10 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): - if value in (None, '', tuple(), []): + """ + Helper method used by `normalize` and `convert`. + """ + if value in self.__nones: return if self.multivalue: if type(value) in (tuple, list): @@ -117,6 +122,11 @@ class Param(plugable.ReadOnly): return scalar(value) def __normalize_scalar(self, value, index=None): + """ + Normalize a scalar value. + + This method is called once with each value in multivalue. + """ if not isinstance(value, basestring): return value try: @@ -145,7 +155,12 @@ class Param(plugable.ReadOnly): return self.__dispatch(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): - if value is None: + """ + Convert a scalar value. + + This method is called once with each value in multivalue. + """ + if value in self.__nones: return converted = self.type(value) if converted is None: @@ -158,7 +173,8 @@ class Param(plugable.ReadOnly): """ Convert/coerce ``value`` to Python type for this `Param`. - If ``value`` can not be converted, ConversionError is raised. + If ``value`` can not be converted, ConversionError is raised, which + is as subclass of ValidationError. If ``value`` is None, conversion is not attempted and None is returned. @@ -168,6 +184,11 @@ class Param(plugable.ReadOnly): return self.__dispatch(value, self.__convert_scalar) def __validate_scalar(self, value, index=None): + """ + Validate a scalar value. + + This method is called once with each value in multivalue. + """ if type(value) is not self.type.type: raise_TypeError(value, self.type.type, 'value') for rule in self.rules: @@ -178,6 +199,18 @@ class Param(plugable.ReadOnly): ) def validate(self, value): + """ + Check validity of a value. + + Each validation rule is called in turn and if any returns and error, + RuleError is raised, which is a subclass of ValidationError. + + :param value: A proposed value for this parameter. + """ + if value is None: + if self.required: + raise errors.RequirementError(self.name) + return if self.multivalue: if type(value) is not tuple: raise_TypeError(value, tuple, 'value') @@ -187,6 +220,22 @@ class Param(plugable.ReadOnly): self.__validate_scalar(value) def get_default(self, **kw): + """ + Return a default value for this parameter. + + If this `Param` instance does not have a default_from() callback, this + method always returns the static Param.default instance attribute. + + On the other hand, if this `Param` instance has a default_from() + callback, the callback is called and its return value is returned + (assuming that value is not None). + + If the default_from() callback returns None, or if an exception is + caught when calling the default_from() callback, the static + Param.default instance attribute is returned. + + :param kw: Optional keyword arguments to pass to default_from(). + """ if self.default_from is not None: default = self.default_from(**kw) if default is not None: @@ -202,18 +251,12 @@ class Param(plugable.ReadOnly): return tuple() def __call__(self, value, **kw): - if value in ('', tuple(), []): - value = None - if value is None: + if value in self.__nones: value = self.get_default(**kw) - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return None else: value = self.convert(self.normalize(value)) - self.validate(value) - return value + self.validate(value) + return value def __repr__(self): return '%s(%r, %s())' % ( -- cgit From 11a07008b896ac995755b2f2a90e6089ca1344a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 17:55:29 +0000 Subject: 339: Added parse_param_spec() function and corresponding unit tests --- ipalib/frontend.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 82d96643..1ff62023 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -84,6 +84,38 @@ class DefaultFrom(plugable.ReadOnly): return None +def parse_param_spec(spec): + """ + Parse param spec to get name, required, and multivalue. + + The ``spec`` string determines the param name, whether the param is + required, and whether the param is multivalue according the following + syntax: + + name => required=True, multivalue=False + name? => required=False, multivalue=False + name+ => required=True, multivalue=True + name* => required=False, multivalue=True + + :param spec: A spec string. + """ + if type(spec) is not str: + raise_TypeError(spec, str, 'spec') + if len(spec) < 2: + raise ValueError( + 'param spec must be at least 2 characters; got %r' % spec + ) + _map = { + '?': dict(required=False, multivalue=False), + '*': dict(required=False, multivalue=True), + '+': dict(required=True, multivalue=True), + } + end = spec[-1] + if end in _map: + return (spec[:-1], _map[end]) + return (spec, dict(required=True, multivalue=False)) + + class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) -- cgit From 792bf7b1d0f295290aa30bd358d67ecfc7233588 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:02:00 +0000 Subject: 340: Changed default for Param.required to True --- ipalib/frontend.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 1ff62023..92c610c4 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -118,10 +118,20 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) + __default = dict( + type=ipa_types.Unicode(), + doc='', + required=True, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + normalize=None + ) def __init__(self, name, type_, doc='', - required=False, + required=True, multivalue=False, default=None, default_from=None, -- cgit From 06d7fb42ec071974592b35eaab2868c1df8722a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:27:14 +0000 Subject: 341: Param now only takes type_=ipa_types.Unicode() as an optional positional arg, and the rest as pure kwargs --- ipalib/frontend.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 92c610c4..a880adf6 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -119,7 +119,6 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) __default = dict( - type=ipa_types.Unicode(), doc='', required=True, multivalue=False, @@ -129,26 +128,38 @@ class Param(plugable.ReadOnly): normalize=None ) - def __init__(self, name, type_, - doc='', - required=True, - multivalue=False, - default=None, - default_from=None, - rules=tuple(), - normalize=None): + def __init__(self, name, type_=ipa_types.Unicode(), **kw): + if 'required' not in kw and 'multivalue' not in kw: + (name, kw_from_spec) = parse_param_spec(name) + kw.update(kw_from_spec) + default = dict(self.__default) + if not set(default).issuperset(kw): + raise TypeError( + 'no such kwargs: %r' % list(set(kw) - set(default)) + ) + default.update(kw) + self.__kw = default self.name = check_name(name) - self.doc = check_type(doc, str, 'doc') self.type = check_isinstance(type_, ipa_types.Type, 'type_') - self.required = check_type(required, bool, 'required') - self.multivalue = check_type(multivalue, bool, 'multivalue') - self.default = default - self.default_from = check_type(default_from, - DefaultFrom, 'default_from', allow_none=True) - self.__normalize = normalize - self.rules = (type_.validate,) + rules + self.doc = self.__check_type(str, 'doc') + self.required = self.__check_type(bool, 'required') + self.multivalue = self.__check_type(bool, 'multivalue') + self.default = self.__kw['default'] + self.default_from = self.__check_type(DefaultFrom, 'default_from', + allow_none=True + ) + self.__normalize = self.__kw['normalize'] + self.rules = (type_.validate,) + self.__kw['rules'] lock(self) + def __check_type(self, type_, name, allow_none=False): + value = self.__kw[name] + return check_type(value, type_, name, allow_none) + + def __check_isinstance(self, type_, name, allow_none=False): + value = self.__kw[name] + return check_isinstance(value, type_, name, allow_none) + def __dispatch(self, value, scalar): """ Helper method used by `normalize` and `convert`. -- cgit From 97f0310a4c1648f84b3fbb3ee10043c48975a456 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:33:25 +0000 Subject: 342: Added unit test that TypeError is raised when Param() is created with extra kw args --- ipalib/frontend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index a880adf6..f6626973 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -134,8 +134,9 @@ class Param(plugable.ReadOnly): kw.update(kw_from_spec) default = dict(self.__default) if not set(default).issuperset(kw): + extra = sorted(set(kw) - set(default)) raise TypeError( - 'no such kwargs: %r' % list(set(kw) - set(default)) + 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) default.update(kw) self.__kw = default -- cgit From a79434584eaab5692d716368b54572aa2b6be70c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:44:43 +0000 Subject: 343: create_param() function no longer parses the param spec itself but relies on Param.__init__() to do it --- ipalib/frontend.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f6626973..ce43ecea 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -86,7 +86,7 @@ class DefaultFrom(plugable.ReadOnly): def parse_param_spec(spec): """ - Parse param spec to get name, required, and multivalue. + Parse a param spec into to (name, kw). The ``spec`` string determines the param name, whether the param is required, and whether the param is multivalue according the following @@ -322,20 +322,14 @@ class Param(plugable.ReadOnly): def create_param(spec): """ - Create a `Param` instance from a param spec string. + Create a `Param` instance from a param spec. If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. If ``spec`` is an str instance, then ``spec`` is parsed and an appropriate `Param` instance is created and returned. - The spec string determines the param name, whether the param is required, - and whether the param is multivalue according the following syntax: - - name => required=True, multivalue=False - name? => required=False, multivalue=False - name+ => required=True, multivalue=True - name* => required=False, multivalue=True + See `parse_param_spec` for the definition of the spec syntax. :param spec: A spec string or a `Param` instance. """ @@ -345,19 +339,7 @@ def create_param(spec): raise TypeError( 'create_param() takes %r or %r; got %r' % (str, Param, spec) ) - if spec.endswith('?'): - kw = dict(required=False, multivalue=False) - name = spec[:-1] - elif spec.endswith('*'): - kw = dict(required=False, multivalue=True) - name = spec[:-1] - elif spec.endswith('+'): - kw = dict(required=True, multivalue=True) - name = spec[:-1] - else: - kw = dict(required=True, multivalue=False) - name = spec - return Param(name, ipa_types.Unicode(), **kw) + return Param(spec) class Command(plugable.Plugin): -- cgit From f8bb60f02dc3cbb48c2cc6305e095e6936f5a0d6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 19:45:46 +0000 Subject: 344: Added Param.__clone__() method; added corresponding unit tests --- ipalib/frontend.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce43ecea..ff0d3492 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -117,6 +117,9 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): + """ + A parameter accepted by a `Command`. + """ __nones = (None, '', tuple(), []) __default = dict( doc='', @@ -150,9 +153,18 @@ class Param(plugable.ReadOnly): allow_none=True ) self.__normalize = self.__kw['normalize'] - self.rules = (type_.validate,) + self.__kw['rules'] + self.rules = self.__check_type(tuple, 'rules') + self.all_rules = (type_.validate,) + self.rules lock(self) + def __clone__(self, **override): + """ + Return a new `Param` instance similar to this one. + """ + kw = dict(self.__kw) + kw.update(override) + return self.__class__(self.name, self.type, **kw) + def __check_type(self, type_, name, allow_none=False): value = self.__kw[name] return check_type(value, type_, name, allow_none) -- cgit From 566d5ea02a5dfdf6f0da0ce1a3f0bb656604c233 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:29:15 +0000 Subject: 347: Added primary_key instance attribute to Param and corresponding kwarg; expanded unit tests for Param.__init__() --- ipalib/frontend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ff0d3492..7e22a9fa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -128,7 +128,8 @@ class Param(plugable.ReadOnly): default=None, default_from=None, rules=tuple(), - normalize=None + normalize=None, + primary_key=False, ) def __init__(self, name, type_=ipa_types.Unicode(), **kw): @@ -155,6 +156,7 @@ class Param(plugable.ReadOnly): self.__normalize = self.__kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (type_.validate,) + self.rules + self.primary_key = self.__check_type(bool, 'primary_key') lock(self) def __clone__(self, **override): -- cgit From 250a01b5b7ee81b19b5da80f1ef47f1ab9174a64 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:46:37 +0000 Subject: 348: If no keys are passed to DefaultFrom.__init__(), the keys from callback.func_code.co_varnames are used; updated DefaultFrom unit tests to test this usage --- ipalib/frontend.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7e22a9fa..1e27d93b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -61,12 +61,16 @@ class DefaultFrom(plugable.ReadOnly): :param callback: The callable to call when all ``keys`` are present. :param keys: The keys used to map from keyword to position arguments. """ - assert callable(callback), 'not a callable: %r' % callback - assert len(keys) > 0, 'must have at least one key' - for key in keys: - assert type(key) is str, 'not an str: %r' % key + if not callable(callback): + raise TypeError('callback must be callable; got %r' % callback) self.callback = callback - self.keys = keys + if len(keys) == 0: + self.keys = callback.func_code.co_varnames + 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): @@ -77,11 +81,11 @@ class DefaultFrom(plugable.ReadOnly): """ vals = tuple(kw.get(k, None) for k in self.keys) if None in vals: - return None + return try: return self.callback(*vals) - except Exception: - return None + except StandardError: + pass def parse_param_spec(spec): -- cgit From 755ea8d0c26afcd1909994a6d381014d79997a33 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:57:34 +0000 Subject: 349: Improved clarity of local variables in Param.__init__() --- ipalib/frontend.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 1e27d93b..6d71a667 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -136,28 +136,28 @@ class Param(plugable.ReadOnly): primary_key=False, ) - def __init__(self, name, type_=ipa_types.Unicode(), **kw): - if 'required' not in kw and 'multivalue' not in kw: + def __init__(self, name, type_=ipa_types.Unicode(), **override): + if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) - kw.update(kw_from_spec) - default = dict(self.__default) - if not set(default).issuperset(kw): - extra = sorted(set(kw) - set(default)) + override.update(kw_from_spec) + kw = dict(self.__default) + if not set(kw).issuperset(override): + extra = sorted(set(override) - set(kw)) raise TypeError( 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) - default.update(kw) - self.__kw = default + kw.update(override) + self.__kw = kw self.name = check_name(name) self.type = check_isinstance(type_, ipa_types.Type, 'type_') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') - self.default = self.__kw['default'] + self.default = kw['default'] self.default_from = self.__check_type(DefaultFrom, 'default_from', allow_none=True ) - self.__normalize = self.__kw['normalize'] + self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (type_.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') -- cgit From e2a680d7c9ca7416e9e3cffe25835fdee967c995 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 22:05:01 +0000 Subject: 350: If Param default_from kwarg is callable but not a DefaltFrom instances, the instance is created implicity --- ipalib/frontend.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6d71a667..80579b7b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -125,7 +125,7 @@ class Param(plugable.ReadOnly): A parameter accepted by a `Command`. """ __nones = (None, '', tuple(), []) - __default = dict( + __defaults = dict( doc='', required=True, multivalue=False, @@ -140,7 +140,7 @@ class Param(plugable.ReadOnly): if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) - kw = dict(self.__default) + kw = dict(self.__defaults) if not set(kw).issuperset(override): extra = sorted(set(override) - set(kw)) raise TypeError( @@ -154,7 +154,10 @@ class Param(plugable.ReadOnly): self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') self.default = kw['default'] - self.default_from = self.__check_type(DefaultFrom, 'default_from', + df = kw['default_from'] + if callable(df) and not isinstance(df, DefaultFrom): + df = DefaultFrom(df) + self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) self.__normalize = kw['normalize'] -- cgit From 3d6ab69b46e5be32af94ecdfb5a696973eeaf7c4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 22:19:43 +0000 Subject: 351: Removed Object.Method property and added in its place Object.methods instance attribute --- ipalib/frontend.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 80579b7b..c3b1707b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,11 +511,11 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( - 'Method', + 'methods', 'Property', 'params' )) - __Method = None + methods = None __Property = None takes_params = tuple() @@ -528,9 +528,6 @@ class Object(plugable.Plugin): for param in self.takes_params: yield create_param(param) - def __get_Method(self): - return self.__Method - Method = property(__get_Method) def __get_Property(self): return self.__Property @@ -538,7 +535,7 @@ class Object(plugable.Plugin): def set_api(self, api): super(Object, self).set_api(api) - self.__Method = self.__create_namespace('Method') + self.methods = self.__create_namespace('Method') self.__Property = self.__create_namespace('Property') def __create_namespace(self, name): -- cgit From c3b09b2116dcbab36098f11c6b3684a6d0e47c08 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:19:34 +0000 Subject: 352: Now removed Object.Property property and added in its place Object.properties instance attribute --- ipalib/frontend.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c3b1707b..132e3039 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -512,11 +512,11 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( 'methods', - 'Property', + 'properties', 'params' )) methods = None - __Property = None + properties = None takes_params = tuple() def __init__(self): @@ -528,15 +528,10 @@ class Object(plugable.Plugin): for param in self.takes_params: yield create_param(param) - - def __get_Property(self): - return self.__Property - Property = property(__get_Property) - def set_api(self, api): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') - self.__Property = self.__create_namespace('Property') + self.properties = self.__create_namespace('Property') def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) @@ -596,14 +591,14 @@ class Method(Attribute, Command): def get_options(self): for option in self.takes_options: yield option - if self.obj is not None and self.obj.Property is not None: + if self.obj is not None and self.obj.properties is not None: def get_key(p): if p.param.required: if p.param.default_from is None: return 0 return 1 return 2 - for prop in sorted(self.obj.Property(), key=get_key): + for prop in sorted(self.obj.properties(), key=get_key): yield prop.param -- cgit From be2e323bbf3f036777acd6e5e16e03f9e66b2ee8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:29:15 +0000 Subject: 353: The Object.parms instance attribute is now created in Object.set_api() instead of in Object.__init__() --- ipalib/frontend.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 132e3039..bcd610a5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -517,13 +517,9 @@ class Object(plugable.Plugin): )) methods = None properties = None + params = None takes_params = tuple() - def __init__(self): - self.params = plugable.NameSpace( - (create_param(p) for p in self.takes_params), sort=False - ) - def __create_params(self): for param in self.takes_params: yield create_param(param) @@ -532,6 +528,9 @@ class Object(plugable.Plugin): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') self.properties = self.__create_namespace('Property') + self.params = plugable.NameSpace( + (create_param(p) for p in self.takes_params), sort=False + ) def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) -- cgit From f531f7da81864f135ff1a5f7d69e15fbe8a27210 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:49:44 +0000 Subject: 354: Added NameSpace.__todict__() method that returns copy of NameSpace.__map; updated NameSpace unit test to also test __todict__() --- ipalib/frontend.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index bcd610a5..6c5f8c76 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -521,8 +521,13 @@ class Object(plugable.Plugin): takes_params = tuple() def __create_params(self): - for param in self.takes_params: - yield create_param(param) + props = self.properties.__todict__() + for spec in self.takes_params: + if type(spec) is str and spec.rstrip('?*+') in props: + yield props.pop(spec.rstrip('?*+')).param + else: + yield create_param(spec) + def set_api(self, api): super(Object, self).set_api(api) -- cgit From 79b33ad3663b91ad7816cf55737faa28603fca70 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 00:00:58 +0000 Subject: 355: Object.set_api() now creates Object.params namespace by merging takes_params and properties together intelegintly --- ipalib/frontend.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6c5f8c76..6aa21eb8 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -520,21 +520,12 @@ class Object(plugable.Plugin): params = None takes_params = tuple() - def __create_params(self): - props = self.properties.__todict__() - for spec in self.takes_params: - if type(spec) is str and spec.rstrip('?*+') in props: - yield props.pop(spec.rstrip('?*+')).param - else: - yield create_param(spec) - - def set_api(self, api): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') self.properties = self.__create_namespace('Property') self.params = plugable.NameSpace( - (create_param(p) for p in self.takes_params), sort=False + self.__create_params(), sort=False ) def __create_namespace(self, name): @@ -547,6 +538,22 @@ class Object(plugable.Plugin): if proxy.obj_name == self.name: yield proxy.__clone__('attr_name') + def __create_params(self): + props = self.properties.__todict__() + for spec in self.takes_params: + if type(spec) is str and spec.rstrip('?*+') in props: + yield props.pop(spec.rstrip('?*+')).param + else: + yield create_param(spec) + def get_key(p): + if p.param.required: + if p.param.default_from is None: + return 0 + return 1 + return 2 + for prop in sorted(props.itervalues(), key=get_key): + yield prop.param + class Attribute(plugable.Plugin): __public__ = frozenset(( -- cgit From 4747563a802a08863d2195222b2f428e52af8502 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 00:42:38 +0000 Subject: 356: Modified Method.get_options() to now pull from self.obj.params(); updated unit tests for Method.get_options() --- ipalib/frontend.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6aa21eb8..9f4f5295 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -541,8 +541,13 @@ class Object(plugable.Plugin): def __create_params(self): props = self.properties.__todict__() for spec in self.takes_params: - if type(spec) is str and spec.rstrip('?*+') in props: - yield props.pop(spec.rstrip('?*+')).param + if type(spec) is str: + key = spec.rstrip('?*+') + else: + assert type(spec) is Param + key = spec.name + if key in props: + yield props.pop(key).param else: yield create_param(spec) def get_key(p): @@ -602,15 +607,9 @@ class Method(Attribute, Command): def get_options(self): for option in self.takes_options: yield option - if self.obj is not None and self.obj.properties is not None: - def get_key(p): - if p.param.required: - if p.param.default_from is None: - return 0 - return 1 - return 2 - for prop in sorted(self.obj.properties(), key=get_key): - yield prop.param + if self.obj is not None and self.obj.params is not None: + for param in self.obj.params(): + yield param class Property(Attribute): -- cgit From 426742279348765d27ad66c69bea874398ed0ef4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:04:10 +0000 Subject: 358: Cleaned up private methods in Object --- ipalib/frontend.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 9f4f5295..948b047d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -522,23 +522,24 @@ class Object(plugable.Plugin): def set_api(self, api): super(Object, self).set_api(api) - self.methods = self.__create_namespace('Method') - self.properties = self.__create_namespace('Property') + self.methods = plugable.NameSpace( + self.__get_attrs('Method'), sort=False + ) + self.properties = plugable.NameSpace( + self.__get_attrs('Property'), sort=False + ) self.params = plugable.NameSpace( - self.__create_params(), sort=False + self.__get_params(), sort=False ) - def __create_namespace(self, name): - return plugable.NameSpace(self.__filter_members(name)) - - def __filter_members(self, name): + def __get_attrs(self, name): namespace = getattr(self.api, name) assert type(namespace) is plugable.NameSpace for proxy in namespace(): # Equivalent to dict.itervalues() if proxy.obj_name == self.name: yield proxy.__clone__('attr_name') - def __create_params(self): + def __get_params(self): props = self.properties.__todict__() for spec in self.takes_params: if type(spec) is str: -- cgit From 54c97b494880a3d276e2da69ffde55a3ee475616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:44:53 +0000 Subject: 359: Added Object.primary_key instance attribute; added corresponding unit tests --- ipalib/frontend.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 948b047d..40220074 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -514,10 +514,12 @@ class Object(plugable.Plugin): 'methods', 'properties', 'params' + 'primary_key', )) methods = None properties = None params = None + primary_key = None takes_params = tuple() def set_api(self, api): @@ -531,6 +533,16 @@ class Object(plugable.Plugin): self.params = plugable.NameSpace( self.__get_params(), sort=False ) + pkeys = filter(lambda p: p.primary_key, self.params()) + if len(pkeys) > 1: + raise ValueError( + '%s (Object) has multiple primary keys: %s' % ( + self.name, + ', '.join(p.name for p in pkeys), + ) + ) + if len(pkeys) == 1: + self.primary_key = pkeys[0] def __get_attrs(self, name): namespace = getattr(self.api, name) -- cgit From 9f704e001daf760de92c590f69582fc7ffd0c0f2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:52:34 +0000 Subject: 360: Removed Method.get_options() default implementation; cleaned up unit tests for Method --- ipalib/frontend.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 40220074..5573e944 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -617,13 +617,6 @@ class Method(Attribute, Command): Attribute.__init__(self) Command.__init__(self) - def get_options(self): - for option in self.takes_options: - yield option - if self.obj is not None and self.obj.params is not None: - for param in self.obj.params(): - yield param - class Property(Attribute): __public__ = frozenset(( -- cgit From 023f612921b4d9cbd15e3148d09c02932a61d73e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 02:13:16 +0000 Subject: 361: Implemented crud.Add.get_options() method; added corresponding unit tests --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5573e944..6cf9b5d7 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -513,7 +513,7 @@ class Object(plugable.Plugin): __public__ = frozenset(( 'methods', 'properties', - 'params' + 'params', 'primary_key', )) methods = None -- cgit From 152f3089e15eec0ce9f7af07450785114a3fcb6e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:27:40 +0000 Subject: 363: Added Object.params_minus_pk instance attribute --- ipalib/frontend.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6cf9b5d7..c95397aa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -515,11 +515,13 @@ class Object(plugable.Plugin): 'properties', 'params', 'primary_key', + 'params_minus_pk', )) methods = None properties = None params = None primary_key = None + params_minus_pk = None takes_params = tuple() def set_api(self, api): @@ -543,6 +545,9 @@ class Object(plugable.Plugin): ) if len(pkeys) == 1: self.primary_key = pkeys[0] + self.params_minus_pk = plugable.NameSpace( + filter(lambda p: not p.primary_key, self.params()), sort=False + ) def __get_attrs(self, name): namespace = getattr(self.api, name) -- cgit From aa45ec616a0c49a9cedd32fb24aa4a56f69a6586 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 02:43:11 +0000 Subject: 369: Added Object.backend attribute used to associated it with a particular backend component --- ipalib/frontend.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c95397aa..da04cd7a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,17 +511,22 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( + 'backend', 'methods', 'properties', 'params', 'primary_key', 'params_minus_pk', )) + backend = None methods = None properties = None params = None primary_key = None params_minus_pk = None + + # Can override in subclasses: + backend_name = None takes_params = tuple() def set_api(self, api): @@ -549,8 +554,13 @@ class Object(plugable.Plugin): filter(lambda p: not p.primary_key, self.params()), sort=False ) + if 'Backend' in self.api and self.backend_name in self.api.Backend: + self.backend = self.api.Backend[self.backend_name] + def __get_attrs(self, name): - namespace = getattr(self.api, name) + if name not in self.api: + return + namespace = self.api[name] assert type(namespace) is plugable.NameSpace for proxy in namespace(): # Equivalent to dict.itervalues() if proxy.obj_name == self.name: -- cgit From 7bbd81d83171c4711a78616688349622ac309b0b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 22:52:15 +0000 Subject: 370: Added detailed examples to decstring for DefaultFrom class --- ipalib/frontend.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 10 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index da04cd7a..afc02066 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -43,23 +43,79 @@ def is_rule(obj): class DefaultFrom(plugable.ReadOnly): """ - Derives a default for one value using other supplied values. + Derive a default value from other supplied values. - Here is an example that constructs a user's initials from his first - and last name: + 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: - >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') - >>> df(first='John', last='Doe') # Both keys - 'JD' - >>> df() is None # Returns None if any key is missing + >>> 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 + at 0x7fdd225cd7d0> + >>> 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 - >>> df(first='John', middle='Q') is None # Still returns None + >>> 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: The keys used to map from keyword to position arguments. + :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) -- cgit From 031daabcc4bb023ff54bd76dd1418bbe3bcff022 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 23:41:51 +0000 Subject: 371: Added examples to parse_param_spec() docstring and changed syntax guide into a reStructuredText table --- ipalib/frontend.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index afc02066..5fd27116 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -152,10 +152,25 @@ def parse_param_spec(spec): required, and whether the param is multivalue according the following syntax: - name => required=True, multivalue=False - name? => required=False, multivalue=False - name+ => required=True, multivalue=True - name* => required=False, multivalue=True + ====== ===== ======== ========== + Spec Name Required Multivalue + ====== ===== ======== ========== + 'var' 'var' True False + 'var?' 'var' False False + 'var*' 'var' False True + 'var+' 'var' True True + ====== ===== ======== ========== + + For example, + + >>> parse_param_spec('login') + ('login', {'required': True, 'multivalue': False}) + >>> parse_param_spec('gecos?') + ('gecos', {'required': False, 'multivalue': False}) + >>> parse_param_spec('telephone_numbers*') + ('telephone_numbers', {'required': False, 'multivalue': True}) + >>> parse_param_spec('group+') + ('group', {'required': True, 'multivalue': True}) :param spec: A spec string. """ -- cgit From 8901b9a8379c37e6243a24eec9648afa05638785 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 27 Sep 2008 00:31:59 +0000 Subject: 372: Started work on docstring for Param class --- ipalib/frontend.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5fd27116..77518a96 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -194,17 +194,30 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): """ A parameter accepted by a `Command`. + + ============ ================= ================== + Keyword Type Default + ============ ================= ================== + type ipa_type.Type ipa_type.Unicode() + doc str '' + required bool True + multivalue bool False + primary_key bool False + normalize callable None + default same as type.type None + default_from callable None + ============ ================= ================== """ __nones = (None, '', tuple(), []) __defaults = dict( doc='', required=True, multivalue=False, + primary_key=False, + normalize=None, default=None, default_from=None, rules=tuple(), - normalize=None, - primary_key=False, ) def __init__(self, name, type_=ipa_types.Unicode(), **override): -- cgit From d77907d2d0ecc33ef4ee4121e10cfef385172b0d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 27 Sep 2008 01:30:39 +0000 Subject: 373: Replaced type_ optional arg to Param.__init__() with pure kw arg type; updated unit tests and related code --- ipalib/frontend.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 77518a96..289f9eec 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -210,6 +210,7 @@ class Param(plugable.ReadOnly): """ __nones = (None, '', tuple(), []) __defaults = dict( + type=ipa_types.Unicode(), doc='', required=True, multivalue=False, @@ -220,7 +221,7 @@ class Param(plugable.ReadOnly): rules=tuple(), ) - def __init__(self, name, type_=ipa_types.Unicode(), **override): + def __init__(self, name, **override): if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) @@ -233,7 +234,7 @@ class Param(plugable.ReadOnly): kw.update(override) self.__kw = kw self.name = check_name(name) - self.type = check_isinstance(type_, ipa_types.Type, 'type_') + self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') @@ -246,7 +247,7 @@ class Param(plugable.ReadOnly): ) self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') - self.all_rules = (type_.validate,) + self.rules + self.all_rules = (self.type.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') lock(self) @@ -256,7 +257,7 @@ class Param(plugable.ReadOnly): """ kw = dict(self.__kw) kw.update(override) - return self.__class__(self.name, self.type, **kw) + return self.__class__(self.name, **kw) def __check_type(self, type_, name, allow_none=False): value = self.__kw[name] @@ -737,7 +738,8 @@ class Property(Attribute): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - self.param = Param(self.attr_name, self.type, + self.param = Param(self.attr_name, + type=self.type, doc=self.doc, required=self.required, multivalue=self.multivalue, -- cgit From afdc72103847fc27efd00f8cc97a7320909ff6a0 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Mon, 29 Sep 2008 17:41:30 +0200 Subject: Add support for environment variables, change tests accordingly --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 289f9eec..30f5942b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -559,7 +559,7 @@ class Command(plugable.Plugin): return self.run(*args, **kw) def run(self, *args, **kw): - if self.api.env.in_server_context: + if self.api.env.server_context: target = self.execute else: target = self.forward -- cgit From 6000b6b5c62181d25783b6d45adb2ed6f3928480 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:02:24 -0600 Subject: Implemented basic Command.forward() method --- ipalib/frontend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 30f5942b..6decb17d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -542,9 +542,9 @@ class Command(plugable.Plugin): print ' kw =', kw def forward(self, *args, **kw): - print '%s.execute():' % self.name - print ' args =', args - print ' kw =', kw + xmlrpc_client = self.api.Backend.xmlrpc.get_client() + return getattr(xmlrpc_client, self.name)(kw, *args) + def __call__(self, *args, **kw): if len(args) > 0: -- cgit From 993b9f4f63c9868042c96db8c5797a5005331d12 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:46:48 -0600 Subject: Command.get_default() now only returns a defaults for required values --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6decb17d..ed28a4ac 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -522,7 +522,7 @@ class Command(plugable.Plugin): def __get_default_iter(self, kw): for param in self.params(): - if param.name not in kw: + if param.required and kw.get(param.name, None) is None: yield (param.name, param.get_default(**kw)) def get_default(self, **kw): -- cgit From ed3a5855f310d3782bea706c58780f5dc6e96d5d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:51:50 -0600 Subject: -m --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ed28a4ac..7abb8fb0 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -467,7 +467,6 @@ class Command(plugable.Plugin): args = None options = None params = None - can_forward = True def finalize(self): self.args = plugable.NameSpace(self.__create_args(), sort=False) -- cgit From 3ffbaac64cc3a9ab704c707112f59e041986576c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 19:42:06 -0600 Subject: Backend.xmlrpc and simple-server.py now use the xmlrpc_marshal() and xmlrpc_unmarshal() functions respectively --- ipalib/frontend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7abb8fb0..651e4642 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -541,8 +541,10 @@ class Command(plugable.Plugin): print ' kw =', kw def forward(self, *args, **kw): - xmlrpc_client = self.api.Backend.xmlrpc.get_client() - return getattr(xmlrpc_client, self.name)(kw, *args) + """ + Forward call over XML-RPC. + """ + return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) def __call__(self, *args, **kw): -- cgit From b7fe92f44f88cb22b9e229ff7fde5309dfbdd778 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 8 Oct 2008 18:01:22 -0600 Subject: Reorganized Command methods so it is easier to understand and added lots of docstrings --- ipalib/frontend.py | 308 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 223 insertions(+), 85 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 651e4642..ce92cf53 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -449,6 +449,27 @@ def create_param(spec): class Command(plugable.Plugin): + """ + A public IPA atomic operation. + + All plugins that subclass from `Command` will be automatically available + as a CLI command and as an XML-RPC method. + + Plugins that subclass from Command are registered in the ``api.Command`` + namespace. For example: + + >>> api = plugable.API(Command) + >>> class my_command(Command): + ... pass + ... + >>> api.register(my_command) + >>> api.finalize() + >>> list(api.Command) + ['my_command'] + >>> api.Command.my_command + PluginProxy(Command, __main__.my_command()) + """ + __public__ = frozenset(( 'get_default', 'convert', @@ -468,66 +489,134 @@ class Command(plugable.Plugin): options = None params = None - def finalize(self): - self.args = plugable.NameSpace(self.__create_args(), sort=False) - 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(self.__create_options(), sort=False) - self.params = plugable.NameSpace( - tuple(self.args()) + tuple(self.options()), sort=False - ) - super(Command, self).finalize() + def __call__(self, *args, **kw): + """ + Perform validation and then execute the command. - def get_args(self): - return self.takes_args + If not in a server context, the call will be forwarded over + XML-RPC and the executed an the nearest IPA server. + """ + if len(args) > 0: + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) + kw = self.normalize(**kw) + kw = self.convert(**kw) + kw.update(self.get_default(**kw)) + self.validate(**kw) + args = tuple(kw.pop(name) for name in self.args) + return self.run(*args, **kw) - def get_options(self): - return self.takes_options + def args_to_kw(self, *values): + """ + Map positional into keyword arguments. + """ + if self.max_args is not None and len(values) > self.max_args: + if self.max_args == 0: + raise errors.ArgumentError(self, 'takes no arguments') + if self.max_args == 1: + raise errors.ArgumentError(self, 'takes at most 1 argument') + raise errors.ArgumentError(self, + 'takes at most %d arguments' % len(self.args) + ) + return dict(self.__args_to_kw_iter(values)) - def __create_args(self): - optional = False + def __args_to_kw_iter(self, values): + """ + Generator used by `Command.args_to_kw` method. + """ multivalue = False - for arg in self.get_args(): - arg = create_param(arg) - if optional and arg.required: - raise ValueError( - '%s: required argument after optional' % arg.name - ) - if multivalue: - raise ValueError( - '%s: only final argument can be multivalue' % arg.name - ) - if not arg.required: - optional = True - if arg.multivalue: - multivalue = True - yield arg + for (i, arg) in enumerate(self.args()): + assert not multivalue + if len(values) > i: + if arg.multivalue: + multivalue = True + yield (arg.name, values[i:]) + else: + yield (arg.name, values[i]) + else: + break + + def kw_to_args(self, **kw): + """ + Map keyword into positional arguments. + """ + return tuple(kw.get(name, None) for name in self.args) - def __create_options(self): - for option in self.get_options(): - yield create_param(option) + def normalize(self, **kw): + """ + Return a dictionary of normalized values. - def convert(self, **kw): + For example: + + >>> class my_command(Command): + ... takes_options = ( + ... Param('first', normalize=lambda value: value.lower()), + ... Param('last'), + ... ) + ... + >>> c = my_command() + >>> c.finalize() + >>> c.normalize(first='JOHN', last='DOE') + {'last': 'DOE', 'first': 'john'} + """ return dict( - (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() + (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() ) - def normalize(self, **kw): + def convert(self, **kw): + """ + Return a dictionary of values converted to correct type. + + >>> from ipalib import ipa_types + >>> class my_command(Command): + ... takes_args = ( + ... Param('one', type=ipa_types.Int()), + ... 'two', + ... ) + ... + >>> c = my_command() + >>> c.finalize() + >>> c.convert(one=1, two=2) + {'two': u'2', 'one': 1} + """ return dict( - (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() + (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() ) + def get_default(self, **kw): + """ + Return a dictionary of defaults for all missing required values. + + For example: + + >>> class my_command(Command): + ... takes_args = [Param('color', default='Red')] + ... + >>> c = my_command() + >>> c.finalize() + >>> c.get_default() + {'color': 'Red'} + >>> c.get_default(color='Yellow') + {} + """ + return dict(self.__get_default_iter(kw)) + def __get_default_iter(self, kw): + """ + Generator method used by `Command.get_default`. + """ for param in self.params(): if param.required and kw.get(param.name, None) is None: yield (param.name, param.get_default(**kw)) - def get_default(self, **kw): - return dict(self.__get_default_iter(kw)) - def validate(self, **kw): + """ + Validate all values. + + If any value fails the validation, `ipalib.errors.ValidationError` + (or a subclass thereof) will be raised. + """ for param in self.params(): value = kw.get(param.name, None) if value is not None: @@ -535,64 +624,113 @@ class Command(plugable.Plugin): elif param.required: raise errors.RequirementError(param.name) + def run(self, *args, **kw): + """ + Dispatch to `Command.execute` or `Command.forward`. + + If running in a server context, `Command.execute` is called and the + actually work this command performs is executed locally. + + If running in a non-server context, `Command.forward` is called, + which forwards this call over XML-RPC to the exact same command + on the nearest IPA server and the actual work this command + performs is executed remotely. + """ + if self.api.env.server_context: + target = self.execute + else: + target = self.forward + object.__setattr__(self, 'run', target) + return target(*args, **kw) + def execute(self, *args, **kw): + """ + Perform the actual work this command does. + + This method should be implemented only against functionality + in self.api.Backend. For example, a hypothetical + user_add.execute() might be implemented like this: + + >>> class user_add(Command): + ... def execute(self, **kw): + ... return self.api.Backend.ldap.add(**kw) + ... + """ print '%s.execute():' % self.name print ' args =', args print ' kw =', kw def forward(self, *args, **kw): """ - Forward call over XML-RPC. + Forward call over XML-RPC to this same command on server. """ return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) + def finalize(self): + """ + Finalize plugin initialization. - def __call__(self, *args, **kw): - if len(args) > 0: - arg_kw = self.args_to_kw(*args) - assert set(arg_kw).intersection(kw) == set() - kw.update(arg_kw) - kw = self.normalize(**kw) - kw = self.convert(**kw) - kw.update(self.get_default(**kw)) - self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) - return self.run(*args, **kw) - - def run(self, *args, **kw): - if self.api.env.server_context: - target = self.execute + This method creates the ``args``, ``options``, and ``params`` + namespaces. This is not done in `Command.__init__` because + subclasses (like `crud.Add`) might need to access other plugins + 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) + if len(self.args) == 0 or not self.args[-1].multivalue: + self.max_args = len(self.args) else: - target = self.forward - object.__setattr__(self, 'run', target) - return target(*args, **kw) + self.max_args = None + self.options = plugable.NameSpace( + (create_param(spec) for spec in self.get_options()), + sort=False + ) + self.params = plugable.NameSpace( + tuple(self.args()) + tuple(self.options()), sort=False + ) + super(Command, self).finalize() - def args_to_kw(self, *values): - if self.max_args is not None and len(values) > self.max_args: - if self.max_args == 0: - raise errors.ArgumentError(self, 'takes no arguments') - if self.max_args == 1: - raise errors.ArgumentError(self, 'takes at most 1 argument') - raise errors.ArgumentError(self, - 'takes at most %d arguments' % len(self.args) - ) - return dict(self.__args_to_kw_iter(values)) + def get_args(self): + """ + Return iterable with arguments for Command.args namespace. - def __args_to_kw_iter(self, values): - multivalue = False - for (i, arg) in enumerate(self.args()): - assert not multivalue - if len(values) > i: - if arg.multivalue: - multivalue = True - yield (arg.name, values[i:]) - else: - yield (arg.name, values[i]) - else: - break + Subclasses can override this to customize how the arguments + are determined. For an example of why this can be useful, + see `ipalib.crud.Mod`. + """ + return self.takes_args - def kw_to_args(self, **kw): - return tuple(kw.get(name, None) for name in self.args) + def get_options(self): + """ + Return iterable with options for Command.options 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`. + """ + return self.takes_options + + def __create_args(self): + """ + Generator used to create args namespace. + """ + optional = False + multivalue = False + for arg in self.get_args(): + arg = create_param(arg) + if optional and arg.required: + raise ValueError( + '%s: required argument after optional' % arg.name + ) + if multivalue: + raise ValueError( + '%s: only final argument can be multivalue' % arg.name + ) + if not arg.required: + optional = True + if arg.multivalue: + multivalue = True + yield arg class Object(plugable.Plugin): -- cgit From 887016e69d6678892a2ff53735623ce5d413b074 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 8 Oct 2008 18:18:13 -0600 Subject: Base Command.execute() method now raises NotImplementedError; updated unit tests --- ipalib/frontend.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce92cf53..639160c1 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -656,9 +656,7 @@ class Command(plugable.Plugin): ... return self.api.Backend.ldap.add(**kw) ... """ - print '%s.execute():' % self.name - print ' args =', args - print ' kw =', kw + raise NotImplementedError('%s.execute()' % self.name) def forward(self, *args, **kw): """ -- cgit From 8674086b8536f64947ca8cdb97d7a1cd3bf1c684 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 17:24:23 -0600 Subject: Param now takes cli_name kwarg that sets Param.cli_name attribute --- ipalib/frontend.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 639160c1..4c6a9c8d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -198,6 +198,7 @@ class Param(plugable.ReadOnly): ============ ================= ================== Keyword Type Default ============ ================= ================== + cli_name str defaults to name type ipa_type.Type ipa_type.Unicode() doc str '' required bool True @@ -210,6 +211,7 @@ class Param(plugable.ReadOnly): """ __nones = (None, '', tuple(), []) __defaults = dict( + cli_name=None, type=ipa_types.Unicode(), doc='', required=True, @@ -226,6 +228,7 @@ class Param(plugable.ReadOnly): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) kw = dict(self.__defaults) + kw['cli_name'] = name if not set(kw).issuperset(override): extra = sorted(set(override) - set(kw)) raise TypeError( @@ -234,6 +237,7 @@ class Param(plugable.ReadOnly): kw.update(override) self.__kw = kw self.name = check_name(name) + self.cli_name = check_name(kw.get('cli_name', name)) self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') -- cgit From 2357360e2a077f56f01e9ce8bc5a21d87fea7675 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 21:53:03 -0600 Subject: Command.params are now sorted the same way as Object.params (make user-add prompt for first, last before login) --- ipalib/frontend.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4c6a9c8d..50e2dd3e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -687,8 +687,15 @@ class Command(plugable.Plugin): (create_param(spec) for spec in self.get_options()), sort=False ) + def get_key(p): + if p.required: + if p.default_from is None: + return 0 + return 1 + return 2 self.params = plugable.NameSpace( - tuple(self.args()) + tuple(self.options()), sort=False + sorted(tuple(self.args()) + tuple(self.options()), key=get_key), + sort=False ) super(Command, self).finalize() -- cgit From 446037fd60d59f671b9a402b9111ab041c1c1439 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 23:26:24 -0600 Subject: Added Object.get_dn() method; added corresponding unit tests --- ipalib/frontend.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 50e2dd3e..65b053e6 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -750,6 +750,7 @@ class Object(plugable.Plugin): 'params', 'primary_key', 'params_minus_pk', + 'get_dn', )) backend = None methods = None @@ -790,6 +791,12 @@ class Object(plugable.Plugin): if 'Backend' in self.api and self.backend_name in self.api.Backend: self.backend = self.api.Backend[self.backend_name] + def get_dn(self, primary_key): + """ + Construct an LDAP DN from a primary_key. + """ + raise NotImplementedError('%s.get_dn()' % self.name) + def __get_attrs(self, name): if name not in self.api: return -- cgit From 1480224724864cb7cf34c9be755b905c61f885b9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 14 Oct 2008 01:45:30 -0600 Subject: Started roughing out user_add() using api.Backend.ldap; added Command.output_for_cli() to take care of formatting print output --- ipalib/frontend.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 65b053e6..da4fd00b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -486,6 +486,7 @@ class Command(plugable.Plugin): 'params', 'args_to_kw', 'kw_to_args', + 'output_for_cli', )) takes_options = tuple() takes_args = tuple() @@ -741,6 +742,14 @@ class Command(plugable.Plugin): multivalue = True yield arg + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + assert type(ret) is dict, 'base output_for_cli() only works with dict' + for key in sorted(ret): + print '%s = %r' % (key, ret[key]) + class Object(plugable.Plugin): __public__ = frozenset(( -- cgit From 8322138f38a4f9c826e4ab148a4fee7df5e93b34 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 19:34:26 -0600 Subject: Added new Param.flags attribute (set with flags=foo kwarg) --- ipalib/frontend.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index da4fd00b..bf3eb7f2 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -207,6 +207,7 @@ class Param(plugable.ReadOnly): normalize callable None default same as type.type None default_from callable None + flags frozenset frozenset() ============ ================= ================== """ __nones = (None, '', tuple(), []) @@ -220,6 +221,7 @@ class Param(plugable.ReadOnly): normalize=None, default=None, default_from=None, + flags=frozenset(), rules=tuple(), ) @@ -249,6 +251,7 @@ class Param(plugable.ReadOnly): self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) + self.flags = frozenset(kw['flags']) self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (self.type.validate,) + self.rules -- cgit From f1eb74e22cadf3a9f4ac991e0f8b922f6fb56d1e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 20:50:34 -0600 Subject: make-test now runs doctests also; fixed several broken doctests --- ipalib/frontend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index bf3eb7f2..2c34b972 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -63,8 +63,8 @@ class DefaultFrom(plugable.ReadOnly): The callback is available through the ``DefaultFrom.callback`` instance attribute, like this: - >>> login.callback - at 0x7fdd225cd7d0> + >>> login.callback # doctest:+ELLIPSIS + at 0x...> >>> login.callback.func_code.co_varnames # The keys ('first', 'last') @@ -473,8 +473,8 @@ class Command(plugable.Plugin): >>> api.finalize() >>> list(api.Command) ['my_command'] - >>> api.Command.my_command - PluginProxy(Command, __main__.my_command()) + >>> api.Command.my_command # doctest:+ELLIPSIS + PluginProxy(Command, ...my_command()) """ __public__ = frozenset(( -- cgit From 721982870ed6dd5507a634d09dd06309abc3778a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 21:05:03 -0600 Subject: Removed generic Command.output_for_cli() method; CLI.run_interactive() now only calls output_for_cli() if it has been implemented --- ipalib/frontend.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 2c34b972..af640cb5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -496,6 +496,7 @@ class Command(plugable.Plugin): args = None options = None params = None + output_for_cli = None def __call__(self, *args, **kw): """ @@ -745,14 +746,6 @@ class Command(plugable.Plugin): multivalue = True yield arg - def output_for_cli(self, ret): - """ - Output result of this command to command line interface. - """ - assert type(ret) is dict, 'base output_for_cli() only works with dict' - for key in sorted(ret): - print '%s = %r' % (key, ret[key]) - class Object(plugable.Plugin): __public__ = frozenset(( -- cgit From 5c5641e8c2e988dff8b81308775c048fb7178929 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 18 Oct 2008 00:16:22 -0600 Subject: Added some more examples to Param docstrings --- ipalib/frontend.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index af640cb5..d2985fa7 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -200,7 +200,7 @@ class Param(plugable.ReadOnly): ============ ================= ================== cli_name str defaults to name type ipa_type.Type ipa_type.Unicode() - doc str '' + doc str "" required bool True multivalue bool False primary_key bool False @@ -305,6 +305,14 @@ class Param(plugable.ReadOnly): """ Normalize ``value`` using normalize callback. + For example: + + >>> param = Param('telephone', + ... normalize=lambda value: value.replace('.', '-') + ... ) + >>> param.normalize('800.123.4567') + '800-123-4567' + If this `Param` instance does not have a normalize callback, ``value`` is returned unchanged. @@ -340,6 +348,14 @@ class Param(plugable.ReadOnly): """ Convert/coerce ``value`` to Python type for this `Param`. + For example: + + >>> param = Param('an_int', type=ipa_types.Int()) + >>> param.convert(7.2) + 7 + >>> param.convert(" 7 ") + 7 + If ``value`` can not be converted, ConversionError is raised, which is as subclass of ValidationError. @@ -413,6 +429,12 @@ class Param(plugable.ReadOnly): return self.default def get_values(self): + """ + Return a tuple of possible values. + + For enumerable types, a tuple containing the possible values is + returned. For all other types, an empty tuple is returned. + """ if self.type.name in ('Enum', 'CallbackEnum'): return self.type.values return tuple() -- cgit From bb978e591b08b3388345c848fb866c22239094ac Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 16:45:32 -0600 Subject: Fixed bug in DefaultFrom where impleied keys were using entire func_code.co_varnames instead of an approprate slice --- ipalib/frontend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d2985fa7..d70a725e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -121,7 +121,8 @@ class DefaultFrom(plugable.ReadOnly): raise TypeError('callback must be callable; got %r' % callback) self.callback = callback if len(keys) == 0: - self.keys = callback.func_code.co_varnames + fc = callback.func_code + self.keys = fc.co_varnames[:fc.co_argcount] else: self.keys = keys for key in self.keys: -- cgit From c818fe1d2d9ab87c5291ead043784a9d68f95448 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 19:57:02 -0600 Subject: Added docstring (with examples) to frontend.Method class --- ipalib/frontend.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d70a725e..dbbb2bc5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -894,6 +894,68 @@ class Attribute(plugable.Plugin): class Method(Attribute, Command): + """ + A command with an associated object. + + A `Method` plugin must have a corresponding `Object` plugin. The + association between object and method is done through a simple naming + convention: the first part of the method name (up to the first under + score) is the object name, as the examples in this table show: + + ============= =========== ============== + Method name Object name Attribute name + ============= =========== ============== + user_add user add + noun_verb noun verb + door_open_now door open_door + ============= =========== ============== + + There are three different places a method can be accessed. For example, + say you created a `Method` plugin and its corresponding `Object` plugin + like this: + + >>> api = plugable.API(Command, Object, Method, Property) + >>> class user_add(Method): + ... def run(self): + ... return 'Added the user!' + ... + >>> class user(Object): + ... pass + ... + >>> api.register(user_add) + >>> api.register(user) + >>> api.finalize() + + First, the ``user_add`` plugin can be accessed through the ``api.Method`` + namespace: + + >>> list(api.Method) + ['user_add'] + >>> api.Method.user_add() # Will call user_add.run() + 'Added the user!' + + Second, because `Method` is a subclass of `Command`, the ``user_add`` + plugin can also be accessed through the ``api.Command`` namespace: + + >>> list(api.Command) + ['user_add'] + >>> api.Command.user_add() # Will call user_add.run() + 'Added the user!' + + And third, ``user_add`` can be accessed as an attribute on the ``user`` + `Object`: + + >>> list(api.Object) + ['user'] + >>> list(api.Object.user.methods) + ['add'] + >>> api.Object.user.methods.add() # Will call user_add.run() + 'Added the user!' + + The `Attribute` base class implements the naming convention for the + attribute-to-object association. Also see the `Object` and the + `Property` classes. + """ __public__ = Attribute.__public__.union(Command.__public__) def __init__(self): -- cgit From 461f547e6ae29df72534cce65eb490a7898c1f0a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 20:28:24 -0600 Subject: Added docstring (with example) to frontend.Attribute class --- ipalib/frontend.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index dbbb2bc5..e0d6fa78 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -857,6 +857,40 @@ class Object(plugable.Plugin): class Attribute(plugable.Plugin): + """ + Base class implementing the attribute-to-object association. + + `Attribute` plugins are associated with an `Object` plugin to group + a common set of commands that operate on a common set of parameters. + + The association between attribute and object is done using a simple + naming convention: the first part of the plugin class name (up to the + first underscore) is the object name, and rest is the attribute name, + as this table shows: + + ============= =========== ============== + Class name Object name Attribute name + ============= =========== ============== + user_add user add + noun_verb noun verb + door_open_now door open_door + ============= =========== ============== + + For example: + + >>> class user_add(Attribute): + ... pass + ... + >>> instance = user_add() + >>> instance.obj_name + 'user' + >>> instance.attr_name + 'add' + + In practice the `Attribute` class is not used directly, but rather is + only the base class for the `Method` and `Property` classes. Also see + the `Object` class. + """ __public__ = frozenset(( 'obj', 'obj_name', -- cgit From 603baf6b1051ea38a969ac59be334ff38d66998c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 21 Oct 2008 08:42:52 -0600 Subject: Fixed typos in tables in docstrings for Attribute and Method --- ipalib/frontend.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index e0d6fa78..d918dd83 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -868,13 +868,13 @@ class Attribute(plugable.Plugin): first underscore) is the object name, and rest is the attribute name, as this table shows: - ============= =========== ============== - Class name Object name Attribute name - ============= =========== ============== - user_add user add - noun_verb noun verb - door_open_now door open_door - ============= =========== ============== + =============== =========== ============== + Class name Object name Attribute name + =============== =========== ============== + noun_verb noun verb + user_add user add + user_first_name user first_name + =============== =========== ============== For example: @@ -941,7 +941,7 @@ class Method(Attribute, Command): ============= =========== ============== user_add user add noun_verb noun verb - door_open_now door open_door + door_open_now door open_now ============= =========== ============== There are three different places a method can be accessed. For example, -- cgit From d76202fea37e63fbc660ed2cf2059f455b8e2213 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 01:35:40 -0600 Subject: API.env is now an Env instance rather than an Environment instance --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d918dd83..62a503cc 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -668,7 +668,7 @@ class Command(plugable.Plugin): on the nearest IPA server and the actual work this command performs is executed remotely. """ - if self.api.env.server_context: + if self.api.env.in_server: target = self.execute else: target = self.forward -- cgit From 09161e399a61e2a548e9efb3c3abb2c7b47d5520 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 01:47:37 -0700 Subject: Command.get_default() will now fill-in None for all missing non-required params --- ipalib/frontend.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 62a503cc..ce4168bc 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -639,8 +639,11 @@ class Command(plugable.Plugin): Generator method used by `Command.get_default`. """ for param in self.params(): - if param.required and kw.get(param.name, None) is None: - yield (param.name, param.get_default(**kw)) + if kw.get(param.name, None) is None: + if param.required: + yield (param.name, param.get_default(**kw)) + else: + yield (param.name, None) def validate(self, **kw): """ @@ -694,7 +697,7 @@ class Command(plugable.Plugin): """ Forward call over XML-RPC to this same command on server. """ - return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) + return self.Backend.xmlrpc.forward_call(self.name, *args, **kw) def finalize(self): """ -- cgit From f04aaff97c9c8c22b36706f2c6d4de6f23d06b95 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 09:55:11 -0700 Subject: output_for_cli signature is now output_for_cli(textui, result, *args, **options) --- ipalib/frontend.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce4168bc..56c4ea01 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,7 +511,7 @@ class Command(plugable.Plugin): 'options', 'params', 'args_to_kw', - 'kw_to_args', + 'params_2_args_options', 'output_for_cli', )) takes_options = tuple() @@ -536,8 +536,8 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) - return self.run(*args, **kw) + (args, options) = self.params_2_args_options(kw) + return self.run(*args, **options) def args_to_kw(self, *values): """ @@ -569,11 +569,15 @@ class Command(plugable.Plugin): else: break - def kw_to_args(self, **kw): + def params_2_args_options(self, params): """ - Map keyword into positional arguments. + Split params into (args, kw). """ - return tuple(kw.get(name, None) for name in self.args) + args = tuple(params.get(name, None) for name in self.args) + options = dict( + (name, params.get(name, None)) for name in self.options + ) + return (args, options) def normalize(self, **kw): """ -- cgit From 8ad5502354a364db606b72455c5514cb56e918ba Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 21:07:47 -0700 Subject: Added util.make_repr() function; added corresponding unit tests --- ipalib/frontend.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 56c4ea01..87126a44 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -227,6 +227,7 @@ class Param(plugable.ReadOnly): ) def __init__(self, name, **override): + self.__override = override if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) -- cgit From 1f635269e8c0253230c3d20b6b41ccd91e02f361 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 21:17:33 -0700 Subject: Param.__repr__() now uses util.make_repr() --- ipalib/frontend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 87126a44..f3c0f0fa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,6 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import ipa_types +from util import make_repr RULE_FLAG = 'validation_rule' @@ -450,11 +451,10 @@ class Param(plugable.ReadOnly): return value def __repr__(self): - return '%s(%r, %s())' % ( - self.__class__.__name__, - self.name, - self.type.name, - ) + """ + Return an expresion that could construct this `Param` instance. + """ + return make_repr(self.__class__.__name__, self.name, **self.__override) def create_param(spec): -- cgit From 860d391f3e905e20ba3f409c92d98e68450f3137 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 22:16:04 -0700 Subject: Change Param.__repr__() so it returns the exact expression that could create it; added unit test for Param.__repre__() --- ipalib/frontend.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f3c0f0fa..3e04db51 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -228,34 +228,34 @@ class Param(plugable.ReadOnly): ) def __init__(self, name, **override): + self.__param_spec = name self.__override = override + self.__kw = dict(self.__defaults) if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) - override.update(kw_from_spec) - kw = dict(self.__defaults) - kw['cli_name'] = name - if not set(kw).issuperset(override): - extra = sorted(set(override) - set(kw)) + self.__kw.update(kw_from_spec) + self.__kw['cli_name'] = name + if not set(self.__kw).issuperset(override): + extra = sorted(set(override) - set(self.__kw)) raise TypeError( 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) - kw.update(override) - self.__kw = kw + self.__kw.update(override) self.name = check_name(name) - self.cli_name = check_name(kw.get('cli_name', name)) + self.cli_name = check_name(self.__kw.get('cli_name', name)) self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') - self.default = kw['default'] - df = kw['default_from'] + self.default = self.__kw['default'] + df = self.__kw['default_from'] if callable(df) and not isinstance(df, DefaultFrom): df = DefaultFrom(df) self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) - self.flags = frozenset(kw['flags']) - self.__normalize = kw['normalize'] + self.flags = frozenset(self.__kw['flags']) + self.__normalize = self.__kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (self.type.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') @@ -454,7 +454,11 @@ class Param(plugable.ReadOnly): """ Return an expresion that could construct this `Param` instance. """ - return make_repr(self.__class__.__name__, self.name, **self.__override) + return make_repr( + self.__class__.__name__, + self.__param_spec, + **self.__override + ) def create_param(spec): -- cgit From 36737c2d913716eb99aece5cc1f6a21234abe46a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 21:29:46 -0700 Subject: Added frontend.LocalOrRemote command base class for commands like env --- ipalib/frontend.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 3e04db51..446384a3 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -782,6 +782,37 @@ class Command(plugable.Plugin): yield arg +class LocalOrRemote(Command): + """ + A command that is explicitly executed locally or remotely. + + This is for commands that makes sense to execute either locally or + remotely to return a perhaps different result. The best example of + this is the `ipalib.plugins.f_misc.env` plugin which returns the + key/value pairs describing the configuration state: it can be + """ + + takes_options = ( + Param('server', type=ipa_types.Bool(), default=False, + doc='Forward to server instead of running locally', + ), + ) + + def run(self, *args, **options): + """ + Dispatch to forward() or execute() based on ``server`` option. + + When running in a client context, this command is executed remotely if + ``options['server']`` is true; otherwise it is executed locally. + + When running in a server context, this command is always executed + locally and the value of ``options['server']`` is ignored. + """ + if options['server'] and not self.env.in_server: + return self.forward(*args, **options) + return self.execute(*args, **options) + + class Object(plugable.Plugin): __public__ = frozenset(( 'backend', -- cgit From 9de56d43f054bc5e509e38bda1a048e5af6d73d7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 21:58:39 -0700 Subject: env plugin now subclasses from RemoteOrLocal --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 446384a3..3ae143ef 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -793,7 +793,7 @@ class LocalOrRemote(Command): """ takes_options = ( - Param('server', type=ipa_types.Bool(), default=False, + Param('server?', type=ipa_types.Bool(), default=False, doc='Forward to server instead of running locally', ), ) -- cgit From 8474bd01da13b9b72ba06e832d4c35ef6ccf5c9e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 18:50:30 -0700 Subject: Command.get_defaults() now returns param.default if param.type is a Bool --- ipalib/frontend.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 3ae143ef..61cba513 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -651,6 +651,8 @@ class Command(plugable.Plugin): if kw.get(param.name, None) is None: if param.required: yield (param.name, param.get_default(**kw)) + elif isinstance(param.type, ipa_types.Bool): + yield (param.name, param.default) else: yield (param.name, None) -- cgit From 4afee15d4b523a641552bee9993882bb1ae6e2cc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 18 Nov 2008 13:43:43 -0700 Subject: Calling 'passwd' command now prompts for password using textui.prompt_password() --- ipalib/frontend.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 61cba513..db399ba5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -261,6 +261,13 @@ class Param(plugable.ReadOnly): self.primary_key = self.__check_type(bool, 'primary_key') lock(self) + def ispassword(self): + """ + Return ``True`` is this Param is a password. + """ + # FIXME: add unit test + return 'password' in self.flags + def __clone__(self, **override): """ Return a new `Param` instance similar to this one. -- cgit From 500b8166811e39ac63bdbaee8dcf01eb0643d868 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 18 Nov 2008 16:29:08 -0700 Subject: Added unit test for Param.ispassword() method --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index db399ba5..6e79e539 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -265,7 +265,6 @@ class Param(plugable.ReadOnly): """ Return ``True`` is this Param is a password. """ - # FIXME: add unit test return 'password' in self.flags def __clone__(self, **override): -- cgit From 2db738e8996528502293b8cc6861efedcba22c9a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 24 Nov 2008 10:09:30 -0700 Subject: Some changes to make reading dubugging output easier --- ipalib/frontend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6e79e539..6dcbea69 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -539,6 +539,7 @@ class Command(plugable.Plugin): If not in a server context, the call will be forwarded over XML-RPC and the executed an the nearest IPA server. """ + self.debug(make_repr(self.name, *args, **kw)) if len(args) > 0: arg_kw = self.args_to_kw(*args) assert set(arg_kw).intersection(kw) == set() @@ -548,7 +549,9 @@ class Command(plugable.Plugin): kw.update(self.get_default(**kw)) self.validate(**kw) (args, options) = self.params_2_args_options(kw) - return self.run(*args, **options) + result = self.run(*args, **options) + self.debug('%s result: %r', self.name, result) + return result def args_to_kw(self, *values): """ -- cgit From 29d680b211021fe755522f4453853344233bc78e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 25 Nov 2008 13:52:40 -0700 Subject: Continued work on xmlrpc.dispatch() unit tests; fixed bug in Command.args_to_kw() --- ipalib/frontend.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6dcbea69..e4dd7637 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -577,12 +577,18 @@ class Command(plugable.Plugin): if len(values) > i: if arg.multivalue: multivalue = True - yield (arg.name, values[i:]) + if len(values) == i + 1 and type(values[i]) in (list, tuple): + yield (arg.name, values[i]) + else: + yield (arg.name, values[i:]) else: yield (arg.name, values[i]) else: break + def args_options_2_params(self, args, options): + pass + def params_2_args_options(self, params): """ Split params into (args, kw). -- cgit From 69041c3b1b2494d89097e490048c23292c8cbc52 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 21:47:43 -0700 Subject: Removed Plugin.name property and replaced with instance attribute created in Plugin.__init__() --- ipalib/frontend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index e4dd7637..4ff77c59 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -965,6 +965,7 @@ class Attribute(plugable.Plugin): assert m self.__obj_name = m.group(1) self.__attr_name = m.group(2) + super(Attribute, self).__init__() def __get_obj_name(self): return self.__obj_name @@ -1053,8 +1054,7 @@ class Method(Attribute, Command): __public__ = Attribute.__public__.union(Command.__public__) def __init__(self): - Attribute.__init__(self) - Command.__init__(self) + super(Method, self).__init__() class Property(Attribute): @@ -1087,6 +1087,7 @@ class Property(Attribute): rules=self.rules, normalize=self.normalize, ) + super(Property, self).__init__() def __rules_iter(self): """ -- cgit From 9d091c98f1f1bf7bacf49e9eaaa18ba8bb1bfd70 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Dec 2008 19:34:32 -0700 Subject: Plugin.__init__() now checks that subclass hasn't defined attributes that conflict with the logger methods; added corresponding unit test --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4ff77c59..c614e547 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -1087,7 +1087,6 @@ class Property(Attribute): rules=self.rules, normalize=self.normalize, ) - super(Property, self).__init__() def __rules_iter(self): """ -- cgit From 2b2e73e7df90d38175e035d6ada4d752120dc0ec Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 11:39:29 -0700 Subject: Removed depreciated code from frontend.py; frontend.py no longer imports ipa_types --- ipalib/frontend.py | 453 +---------------------------------------------------- 1 file changed, 3 insertions(+), 450 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c614e547..baa37b17 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,7 +27,7 @@ import plugable from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError -import ipa_types +import parameters from util import make_repr @@ -42,453 +42,6 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True -class DefaultFrom(plugable.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 - 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 - - -def parse_param_spec(spec): - """ - Parse a param spec into to (name, kw). - - The ``spec`` string determines the param name, whether the param is - required, and whether the param is multivalue according the following - syntax: - - ====== ===== ======== ========== - Spec Name Required Multivalue - ====== ===== ======== ========== - 'var' 'var' True False - 'var?' 'var' False False - 'var*' 'var' False True - 'var+' 'var' True True - ====== ===== ======== ========== - - For example, - - >>> parse_param_spec('login') - ('login', {'required': True, 'multivalue': False}) - >>> parse_param_spec('gecos?') - ('gecos', {'required': False, 'multivalue': False}) - >>> parse_param_spec('telephone_numbers*') - ('telephone_numbers', {'required': False, 'multivalue': True}) - >>> parse_param_spec('group+') - ('group', {'required': True, 'multivalue': True}) - - :param spec: A spec string. - """ - if type(spec) is not str: - raise_TypeError(spec, str, 'spec') - if len(spec) < 2: - raise ValueError( - 'param spec must be at least 2 characters; got %r' % spec - ) - _map = { - '?': dict(required=False, multivalue=False), - '*': dict(required=False, multivalue=True), - '+': dict(required=True, multivalue=True), - } - end = spec[-1] - if end in _map: - return (spec[:-1], _map[end]) - return (spec, dict(required=True, multivalue=False)) - - -class Param(plugable.ReadOnly): - """ - A parameter accepted by a `Command`. - - ============ ================= ================== - Keyword Type Default - ============ ================= ================== - cli_name str defaults to name - type ipa_type.Type ipa_type.Unicode() - doc str "" - required bool True - multivalue bool False - primary_key bool False - normalize callable None - default same as type.type None - default_from callable None - flags frozenset frozenset() - ============ ================= ================== - """ - __nones = (None, '', tuple(), []) - __defaults = dict( - cli_name=None, - type=ipa_types.Unicode(), - doc='', - required=True, - multivalue=False, - primary_key=False, - normalize=None, - default=None, - default_from=None, - flags=frozenset(), - rules=tuple(), - ) - - def __init__(self, name, **override): - self.__param_spec = name - self.__override = override - self.__kw = dict(self.__defaults) - if not ('required' in override or 'multivalue' in override): - (name, kw_from_spec) = parse_param_spec(name) - self.__kw.update(kw_from_spec) - self.__kw['cli_name'] = name - if not set(self.__kw).issuperset(override): - extra = sorted(set(override) - set(self.__kw)) - raise TypeError( - 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) - ) - self.__kw.update(override) - self.name = check_name(name) - self.cli_name = check_name(self.__kw.get('cli_name', name)) - self.type = self.__check_isinstance(ipa_types.Type, 'type') - self.doc = self.__check_type(str, 'doc') - self.required = self.__check_type(bool, 'required') - self.multivalue = self.__check_type(bool, 'multivalue') - self.default = self.__kw['default'] - df = self.__kw['default_from'] - if callable(df) and not isinstance(df, DefaultFrom): - df = DefaultFrom(df) - self.default_from = check_type(df, DefaultFrom, 'default_from', - allow_none=True - ) - self.flags = frozenset(self.__kw['flags']) - self.__normalize = self.__kw['normalize'] - self.rules = self.__check_type(tuple, 'rules') - self.all_rules = (self.type.validate,) + self.rules - self.primary_key = self.__check_type(bool, 'primary_key') - lock(self) - - def ispassword(self): - """ - Return ``True`` is this Param is a password. - """ - return 'password' in self.flags - - def __clone__(self, **override): - """ - Return a new `Param` instance similar to this one. - """ - kw = dict(self.__kw) - kw.update(override) - return self.__class__(self.name, **kw) - - def __check_type(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_type(value, type_, name, allow_none) - - def __check_isinstance(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_isinstance(value, type_, name, allow_none) - - def __dispatch(self, value, scalar): - """ - Helper method used by `normalize` and `convert`. - """ - if value in self.__nones: - return - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - scalar(v, i) for (i, v) in enumerate(value) - ) - return (scalar(value, 0),) # tuple - return scalar(value) - - def __normalize_scalar(self, value, index=None): - """ - Normalize a scalar value. - - This method is called once with each value in multivalue. - """ - if not isinstance(value, basestring): - return value - try: - return self.__normalize(value) - except StandardError: - return value - - def normalize(self, value): - """ - Normalize ``value`` using normalize callback. - - For example: - - >>> param = Param('telephone', - ... normalize=lambda value: value.replace('.', '-') - ... ) - >>> param.normalize('800.123.4567') - '800-123-4567' - - If this `Param` instance does not have a normalize callback, - ``value`` is returned unchanged. - - If this `Param` instance has a normalize callback and ``value`` is - a basestring, the normalize callback is called and its return value - is returned. - - If ``value`` is not a basestring, or if an exception is caught - when calling the normalize callback, ``value`` is returned unchanged. - - :param value: A proposed value for this parameter. - """ - if self.__normalize is None: - return value - return self.__dispatch(value, self.__normalize_scalar) - - def __convert_scalar(self, value, index=None): - """ - Convert a scalar value. - - This method is called once with each value in multivalue. - """ - if value in self.__nones: - return - converted = self.type(value) - if converted is None: - raise errors.ConversionError( - self.name, value, self.type, index=index - ) - return converted - - def convert(self, value): - """ - Convert/coerce ``value`` to Python type for this `Param`. - - For example: - - >>> param = Param('an_int', type=ipa_types.Int()) - >>> param.convert(7.2) - 7 - >>> param.convert(" 7 ") - 7 - - If ``value`` can not be converted, ConversionError is raised, which - is as subclass of ValidationError. - - If ``value`` is None, conversion is not attempted and None is - returned. - - :param value: A proposed value for this parameter. - """ - return self.__dispatch(value, self.__convert_scalar) - - def __validate_scalar(self, value, index=None): - """ - Validate a scalar value. - - This method is called once with each value in multivalue. - """ - if type(value) is not self.type.type: - raise_TypeError(value, self.type.type, 'value') - for rule in self.rules: - error = rule(value) - if error is not None: - raise errors.RuleError( - self.name, value, error, rule, index=index - ) - - def validate(self, value): - """ - Check validity of a value. - - Each validation rule is called in turn and if any returns and error, - RuleError is raised, which is a subclass of ValidationError. - - :param value: A proposed value for this parameter. - """ - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return - if self.multivalue: - if type(value) is not tuple: - raise_TypeError(value, tuple, 'value') - for (i, v) in enumerate(value): - self.__validate_scalar(v, i) - else: - self.__validate_scalar(value) - - def get_default(self, **kw): - """ - Return a default value for this parameter. - - If this `Param` instance does not have a default_from() callback, this - method always returns the static Param.default instance attribute. - - On the other hand, if this `Param` instance has a default_from() - callback, the callback is called and its return value is returned - (assuming that value is not None). - - If the default_from() callback returns None, or if an exception is - caught when calling the default_from() callback, the static - Param.default instance attribute is returned. - - :param kw: Optional keyword arguments to pass to default_from(). - """ - if self.default_from is not None: - default = self.default_from(**kw) - if default is not None: - try: - return self.convert(self.normalize(default)) - except errors.ValidationError: - return None - return self.default - - def get_values(self): - """ - Return a tuple of possible values. - - For enumerable types, a tuple containing the possible values is - returned. For all other types, an empty tuple is returned. - """ - if self.type.name in ('Enum', 'CallbackEnum'): - return self.type.values - return tuple() - - def __call__(self, value, **kw): - if value in self.__nones: - value = self.get_default(**kw) - else: - value = self.convert(self.normalize(value)) - self.validate(value) - return value - - def __repr__(self): - """ - Return an expresion that could construct this `Param` instance. - """ - return make_repr( - self.__class__.__name__, - self.__param_spec, - **self.__override - ) - - -def create_param(spec): - """ - Create a `Param` instance from a param spec. - - If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. - - If ``spec`` is an str instance, then ``spec`` is parsed and an - appropriate `Param` instance is created and returned. - - See `parse_param_spec` for the definition of the spec syntax. - - :param spec: A spec string or a `Param` instance. - """ - if type(spec) is Param: - return spec - if type(spec) is not str: - raise TypeError( - 'create_param() takes %r or %r; got %r' % (str, Param, spec) - ) - return Param(spec) - - class Command(plugable.Plugin): """ A public IPA atomic operation. @@ -810,7 +363,7 @@ class LocalOrRemote(Command): """ takes_options = ( - Param('server?', type=ipa_types.Bool(), default=False, + parameters.Flag('server?', doc='Forward to server instead of running locally', ), ) @@ -1064,7 +617,7 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - type = ipa_types.Unicode() + type = parameters.Str required = False multivalue = False default = None -- cgit From 69acff450c043bdd7d70da473c3adafdd9d3fe03 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 12:00:47 -0700 Subject: New Param: removed more depreciated 'import ipa_types' --- ipalib/frontend.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index baa37b17..64323d0a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,6 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import parameters +from parameters import create_param from util import make_repr -- cgit From 09e2f5d615a17943ba572fd02a2e0d9b15ca1076 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 13:17:30 -0700 Subject: New Param: got most of unit tests ported (still have 6 errors); haven't ported doctests yet --- ipalib/frontend.py | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 64323d0a..98ecc46b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,7 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import parameters -from parameters import create_param +from parameters import create_param, Param from util import make_repr @@ -217,13 +217,12 @@ class Command(plugable.Plugin): Generator method used by `Command.get_default`. """ for param in self.params(): - if kw.get(param.name, None) is None: - if param.required: - yield (param.name, param.get_default(**kw)) - elif isinstance(param.type, ipa_types.Bool): - yield (param.name, param.default) - else: - yield (param.name, None) + if param.name in kw: + continue + if param.required or param.autofill: + default = param.get_default(**kw) + if default is not None: + yield (param.name, default) def validate(self, **kw): """ @@ -454,7 +453,7 @@ class Object(plugable.Plugin): if type(spec) is str: key = spec.rstrip('?*+') else: - assert type(spec) is Param + assert isinstance(spec, Param) key = spec.name if key in props: yield props.pop(key).param @@ -618,29 +617,26 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - type = parameters.Str - required = False - multivalue = False + klass = parameters.Str default = None default_from = None - normalize = None + normalizer = None def __init__(self): super(Property, self).__init__() - self.rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - self.param = Param(self.attr_name, - type=self.type, - doc=self.doc, - required=self.required, - multivalue=self.multivalue, - default=self.default, - default_from=self.default_from, - rules=self.rules, - normalize=self.normalize, + self.rules = tuple( + sorted(self.__rules_iter(), key=lambda f: getattr(f, '__name__')) ) + self.kwargs = tuple( + sorted(self.__kw_iter(), key=lambda keyvalue: keyvalue[0]) + ) + kw = dict(self.kwargs) + self.param = self.klass(self.attr_name, *self.rules, **kw) + + def __kw_iter(self): + for (key, kind, default) in self.klass.kwargs: + if getattr(self, key, None) is not None: + yield (key, getattr(self, key)) def __rules_iter(self): """ -- cgit From 0327b83899389e38aebde9de4219f64a716e611d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 20:36:17 -0700 Subject: New Param: all docstring examples now pass under doctests --- ipalib/frontend.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) (limited to 'ipalib/frontend.py') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 98ecc46b..b30205fe 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,8 +27,7 @@ import plugable from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError -import parameters -from parameters import create_param, Param +from parameters import create_param, Param, Str, Flag from util import make_repr @@ -53,7 +52,8 @@ class Command(plugable.Plugin): Plugins that subclass from Command are registered in the ``api.Command`` namespace. For example: - >>> api = plugable.API(Command) + >>> from ipalib import create_api + >>> api = create_api() >>> class my_command(Command): ... pass ... @@ -161,14 +161,14 @@ class Command(plugable.Plugin): >>> class my_command(Command): ... takes_options = ( - ... Param('first', normalize=lambda value: value.lower()), + ... Param('first', normalizer=lambda value: value.lower()), ... Param('last'), ... ) ... >>> c = my_command() >>> c.finalize() - >>> c.normalize(first='JOHN', last='DOE') - {'last': 'DOE', 'first': 'john'} + >>> c.normalize(first=u'JOHN', last=u'DOE') + {'last': u'DOE', 'first': u'john'} """ return dict( (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() @@ -178,10 +178,10 @@ class Command(plugable.Plugin): """ Return a dictionary of values converted to correct type. - >>> from ipalib import ipa_types + >>> from ipalib import Int >>> class my_command(Command): ... takes_args = ( - ... Param('one', type=ipa_types.Int()), + ... Int('one'), ... 'two', ... ) ... @@ -200,14 +200,15 @@ class Command(plugable.Plugin): For example: + >>> from ipalib import Str >>> class my_command(Command): - ... takes_args = [Param('color', default='Red')] + ... takes_args = [Str('color', default=u'Red')] ... >>> c = my_command() >>> c.finalize() >>> c.get_default() - {'color': 'Red'} - >>> c.get_default(color='Yellow') + {'color': u'Red'} + >>> c.get_default(color=u'Yellow') {} """ return dict(self.__get_default_iter(kw)) @@ -363,7 +364,7 @@ class LocalOrRemote(Command): """ takes_options = ( - parameters.Flag('server?', + Flag('server?', doc='Forward to server instead of running locally', ), ) @@ -562,7 +563,8 @@ class Method(Attribute, Command): say you created a `Method` plugin and its corresponding `Object` plugin like this: - >>> api = plugable.API(Command, Object, Method, Property) + >>> from ipalib import create_api + >>> api = create_api() >>> class user_add(Method): ... def run(self): ... return 'Added the user!' @@ -617,7 +619,7 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - klass = parameters.Str + klass = Str default = None default_from = None normalizer = None -- cgit