summaryrefslogtreecommitdiffstats
path: root/ipalib/parameters.py
diff options
context:
space:
mode:
authorJason Gerard DeRose <jderose@redhat.com>2009-01-14 10:25:33 -0700
committerJason Gerard DeRose <jderose@redhat.com>2009-01-14 10:25:33 -0700
commit5d1e5a0aa9f79e179a5538dcf1bcec5426369951 (patch)
tree5cdd9fd7500076e12187d606a4fec39f05cb7668 /ipalib/parameters.py
parentcc5d7e8adb5882573a47883208e6ac8e60f35002 (diff)
downloadfreeipa-5d1e5a0aa9f79e179a5538dcf1bcec5426369951.tar.gz
freeipa-5d1e5a0aa9f79e179a5538dcf1bcec5426369951.tar.xz
freeipa-5d1e5a0aa9f79e179a5538dcf1bcec5426369951.zip
New Param: renamed parameter.py to parameters.py
Diffstat (limited to 'ipalib/parameters.py')
-rw-r--r--ipalib/parameters.py882
1 files changed, 882 insertions, 0 deletions
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
new file mode 100644
index 000000000..bfe601a93
--- /dev/null
+++ b/ipalib/parameters.py
@@ -0,0 +1,882 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# 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
+
+"""
+Parameter system for command plugins.
+"""
+
+from types import NoneType
+from util import make_repr
+from request import ugettext
+from plugable import ReadOnly, lock, check_name
+from errors2 import ConversionError, RequirementError, ValidationError
+from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR
+
+
+class DefaultFrom(ReadOnly):
+ """
+ Derive a default value from other supplied values.
+
+ For example, say you wanted to create a default for the user's login from
+ the user's first and last names. It could be implemented like this:
+
+ >>> login = DefaultFrom(lambda first, last: first[0] + last)
+ >>> login(first='John', last='Doe')
+ 'JDoe'
+
+ If you do not explicitly provide keys when you create a `DefaultFrom`
+ instance, the keys are implicitly derived from your callback by
+ inspecting ``callback.func_code.co_varnames``. The keys are available
+ through the ``DefaultFrom.keys`` instance attribute, like this:
+
+ >>> login.keys
+ ('first', 'last')
+
+ The callback is available through the ``DefaultFrom.callback`` instance
+ attribute, like this:
+
+ >>> login.callback # doctest:+ELLIPSIS
+ <function <lambda> at 0x...>
+ >>> login.callback.func_code.co_varnames # The keys
+ ('first', 'last')
+
+ The keys can be explicitly provided as optional positional arguments after
+ the callback. For example, this is equivalent to the ``login`` instance
+ above:
+
+ >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last')
+ >>> login2.keys
+ ('first', 'last')
+ >>> login2.callback.func_code.co_varnames # Not the keys
+ ('a', 'b')
+ >>> login2(first='John', last='Doe')
+ 'JDoe'
+
+ If any keys are missing when calling your `DefaultFrom` instance, your
+ callback is not called and ``None`` is returned. For example:
+
+ >>> login(first='John', lastname='Doe') is None
+ True
+ >>> login() is None
+ True
+
+ Any additional keys are simply ignored, like this:
+
+ >>> login(last='Doe', first='John', middle='Whatever')
+ 'JDoe'
+
+ As above, because `DefaultFrom.__call__` takes only pure keyword
+ arguments, they can be supplied in any order.
+
+ Of course, the callback need not be a ``lambda`` expression. This third
+ example is equivalent to both the ``login`` and ``login2`` instances
+ above:
+
+ >>> def get_login(first, last):
+ ... return first[0] + last
+ ...
+ >>> login3 = DefaultFrom(get_login)
+ >>> login3.keys
+ ('first', 'last')
+ >>> login3.callback.func_code.co_varnames
+ ('first', 'last')
+ >>> login3(first='John', last='Doe')
+ 'JDoe'
+ """
+
+ def __init__(self, callback, *keys):
+ """
+ :param callback: The callable to call when all keys are present.
+ :param keys: Optional keys used for source values.
+ """
+ if not callable(callback):
+ raise TypeError(
+ CALLABLE_ERROR % ('callback', callback, type(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(
+ TYPE_ERROR % ('keys', str, key, type(key))
+ )
+ lock(self)
+
+ def __call__(self, **kw):
+ """
+ Call the callback if all keys are present.
+
+ If all keys are present, the callback is called and its return value is
+ returned. If any keys are missing, ``None`` is returned.
+
+ :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 shorthand ``spec`` into to ``(name, kw)``.
+
+ The ``spec`` string determines the parameter name, whether the parameter is
+ required, and whether the parameter 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(
+ TYPE_ERROR % ('spec', str, spec, type(spec))
+ )
+ if len(spec) < 2:
+ raise ValueError(
+ '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))
+
+
+__messages = set()
+
+def _(message):
+ __messages.add(message)
+ return message
+
+
+class Param(ReadOnly):
+ """
+ Base class for all parameters.
+ """
+
+ # This is a dummy type so that most of the functionality of Param can be
+ # unit tested directly without always creating a subclass; however, a real
+ # (direct) subclass must *always* override this class attribute:
+ type = NoneType # Ouch, this wont be very useful in the real world!
+
+ # Subclasses should override this with something more specific:
+ type_error = _('incorrect type')
+
+ kwargs = (
+ ('cli_name', str, None),
+ ('label', callable, None),
+ ('doc', str, ''),
+ ('required', bool, True),
+ ('multivalue', bool, False),
+ ('primary_key', bool, False),
+ ('normalizer', callable, None),
+ ('default_from', DefaultFrom, None),
+ ('create_default', callable, None),
+ ('autofill', bool, False),
+ ('flags', frozenset, frozenset()),
+
+ # The 'default' kwarg gets appended in Param.__init__():
+ # ('default', self.type, None),
+ )
+
+ def __init__(self, name, *rules, **kw):
+ # We keep these values to use in __repr__():
+ self.param_spec = name
+ self.__kw = dict(kw)
+
+ # Merge in kw from parse_param_spec():
+ if not ('required' in kw or 'multivalue' in kw):
+ (name, kw_from_spec) = parse_param_spec(name)
+ kw.update(kw_from_spec)
+ self.name = check_name(name)
+ self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec)
+
+ # Add 'default' to self.kwargs and makes sure no unknown kw were given:
+ assert type(self.type) is type
+ self.kwargs += (('default', self.type, None),)
+ if not set(t[0] for t in self.kwargs).issuperset(self.__kw):
+ extra = set(kw) - set(t[0] for t in self.kwargs)
+ raise TypeError(
+ '%s: takes no such kwargs: %s' % (self.nice,
+ ', '.join(repr(k) for k in sorted(extra))
+ )
+ )
+
+ # Merge in default for 'cli_name' if not given:
+ if kw.get('cli_name', None) is None:
+ kw['cli_name'] = self.name
+
+ # Wrap 'default_from' in a DefaultFrom if not already:
+ df = kw.get('default_from', None)
+ if callable(df) and not isinstance(df, DefaultFrom):
+ kw['default_from'] = DefaultFrom(df)
+
+ # We keep this copy with merged values also to use when cloning:
+ self.__clonekw = kw
+
+ # Perform type validation on kw, add in class rules:
+ class_rules = []
+ for (key, kind, default) in self.kwargs:
+ value = kw.get(key, default)
+ if value is not None:
+ if kind is frozenset:
+ if type(value) in (list, tuple):
+ value = frozenset(value)
+ elif type(value) is str:
+ value = frozenset([value])
+ if (
+ type(kind) is type and type(value) is not kind
+ or
+ type(kind) is tuple and not isinstance(value, kind)
+ ):
+ raise TypeError(
+ TYPE_ERROR % (key, kind, value, type(value))
+ )
+ elif kind is callable and not callable(value):
+ raise TypeError(
+ CALLABLE_ERROR % (key, value, type(value))
+ )
+ if hasattr(self, key):
+ raise ValueError('kwarg %r conflicts with attribute on %s' % (
+ key, self.__class__.__name__)
+ )
+ setattr(self, key, value)
+ rule_name = '_rule_%s' % key
+ if value is not None and hasattr(self, rule_name):
+ class_rules.append(getattr(self, rule_name))
+ check_name(self.cli_name)
+
+ # Check that only default_from or create_default was provided:
+ assert not hasattr(self, '_get_default'), self.nice
+ if callable(self.default_from):
+ if callable(self.create_default):
+ raise ValueError(
+ '%s: cannot have both %r and %r' % (
+ self.nice, 'default_from', 'create_default')
+ )
+ self._get_default = self.default_from
+ elif callable(self.create_default):
+ self._get_default = self.create_default
+ else:
+ self._get_default = None
+
+ # Check that all the rules are callable
+ self.class_rules = tuple(class_rules)
+ self.rules = rules
+ self.all_rules = self.class_rules + self.rules
+ for rule in self.all_rules:
+ if not callable(rule):
+ raise TypeError(
+ '%s: rules must be callable; got %r' % (self.nice, rule)
+ )
+
+ # And we're done.
+ lock(self)
+
+ def __repr__(self):
+ """
+ Return an expresion that could construct this `Param` instance.
+ """
+ return make_repr(
+ self.__class__.__name__,
+ self.param_spec,
+ **self.__kw
+ )
+
+ def __call__(self, value, **kw):
+ """
+ One stop shopping.
+ """
+ if value in NULLS:
+ value = self.get_default(**kw)
+ else:
+ value = self.convert(self.normalize(value))
+ self.validate(value)
+ return value
+
+ def clone(self, **overrides):
+ """
+ Return a new `Param` instance similar to this one.
+ """
+ kw = dict(self.__clonekw)
+ kw.update(overrides)
+ return self.__class__(self.name, **kw)
+
+ def get_label(self):
+ """
+ Return translated label using `request.ugettext`.
+ """
+ if self.label is None:
+ return self.cli_name.decode('UTF-8')
+ return self.label(ugettext)
+
+ def normalize(self, value):
+ """
+ Normalize ``value`` using normalizer callback.
+
+ For example:
+
+ >>> param = Param('telephone',
+ ... normalizer=lambda value: value.replace('.', '-')
+ ... )
+ >>> param.normalize(u'800.123.4567')
+ u'800-123-4567'
+
+ If this `Param` instance was created with a normalizer callback and
+ ``value`` is a unicode instance, the normalizer callback is called and
+ *its* return value is returned.
+
+ On the other hand, if this `Param` instance was *not* created with a
+ normalizer callback, if ``value`` is *not* a unicode instance, or if an
+ exception is caught when calling the normalizer callback, ``value`` is
+ returned unchanged.
+
+ :param value: A proposed value for this parameter.
+ """
+ if self.normalizer 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),) # Return a tuple
+ return self._normalize_scalar(value)
+
+ def _normalize_scalar(self, value):
+ """
+ Normalize a scalar value.
+
+ This method is called once for each value in a multivalue.
+ """
+ if type(value) is not unicode:
+ return value
+ try:
+ return self.normalizer(value)
+ except StandardError:
+ return value
+
+ def convert(self, value):
+ """
+ Convert ``value`` to the Python type required by this parameter.
+
+ For example:
+
+ >>> scalar = Str('my_scalar')
+ >>> scalar.type
+ <type 'unicode'>
+ >>> scalar.convert(43.2)
+ u'43.2'
+
+ (Note that `Str` is a subclass of `Param`.)
+
+ All values in `constants.NULLS` will be converted to ``None``. For
+ example:
+
+ >>> scalar.convert(u'') is None # An empty string
+ True
+ >>> scalar.convert([]) is None # An empty list
+ True
+
+ Likewise, values in `constants.NULLS` will be filtered out of a
+ multivalue parameter. For example:
+
+ >>> multi = Str('my_multi', multivalue=True)
+ >>> multi.convert([1.5, '', 17, None, u'Hello'])
+ (u'1.5', u'17', u'Hello')
+ >>> multi.convert([None, u'']) is None # Filters to an empty list
+ True
+
+ Lastly, multivalue parameters will always return a ``tuple`` (assuming
+ they don't return ``None`` as in the last example above). For example:
+
+ >>> multi.convert(42) # Called with a scalar value
+ (u'42',)
+ >>> multi.convert([0, 1]) # Called with a list value
+ (u'0', u'1')
+
+ Note that how values are converted (and from what types they will be
+ converted) completely depends upon how a subclass implements its
+ `Param._convert_scalar()` method. For example, see
+ `Str._convert_scalar()`.
+
+ :param value: A proposed value for this parameter.
+ """
+ if value in NULLS:
+ return
+ if self.multivalue:
+ if type(value) not in (tuple, list):
+ value = (value,)
+ values = tuple(
+ self._convert_scalar(v, i) for (i, v) in filter(
+ lambda iv: iv[1] not in NULLS, enumerate(value)
+ )
+ )
+ if len(values) == 0:
+ return
+ return values
+ return self._convert_scalar(value)
+
+ def _convert_scalar(self, value, index=None):
+ """
+ Convert a single scalar value.
+ """
+ if type(value) is self.type:
+ return value
+ raise ConversionError(name=self.name, index=index,
+ error=ugettext(self.type_error),
+ )
+
+ def validate(self, value):
+ """
+ Check validity of ``value``.
+
+ :param value: A proposed value for this parameter.
+ """
+ if value is None:
+ if self.required:
+ raise RequirementError(name=self.name)
+ return
+ if self.multivalue:
+ if type(value) is not tuple:
+ raise TypeError(
+ TYPE_ERROR % ('value', tuple, value, type(value))
+ )
+ if len(value) < 1:
+ raise ValueError('value: empty tuple must be converted to None')
+ for (i, v) in enumerate(value):
+ self._validate_scalar(v, i)
+ else:
+ self._validate_scalar(value)
+
+ def _validate_scalar(self, value, index=None):
+ if type(value) is not self.type:
+ if index is None:
+ name = 'value'
+ else:
+ name = 'value[%d]' % index
+ raise TypeError(
+ TYPE_ERROR % (name, self.type, value, type(value))
+ )
+ if index is not None and type(index) is not int:
+ raise TypeError(
+ TYPE_ERROR % ('index', int, index, type(index))
+ )
+ for rule in self.all_rules:
+ error = rule(ugettext, value)
+ if error is not None:
+ raise ValidationError(name=self.name, error=error, index=index)
+
+ def get_default(self, **kw):
+ """
+ Return the static default or construct and return a dynamic default.
+
+ (In these examples, we will use the `Str` and `Bytes` classes, which
+ both subclass from `Param`.)
+
+ The *default* static default is ``None``. For example:
+
+ >>> s = Str('my_str')
+ >>> s.default is None
+ True
+ >>> s.get_default() is None
+ True
+
+ However, you can provide your own static default via the ``default``
+ keyword argument when you create your `Param` instance. For example:
+
+ >>> s = Str('my_str', default=u'My Static Default')
+ >>> s.default
+ u'My Static Default'
+ >>> s.get_default()
+ u'My Static Default'
+
+ If you need to generate a dynamic default from other supplied parameter
+ values, provide a callback via the ``default_from`` keyword argument.
+ This callback will be automatically wrapped in a `DefaultFrom` instance
+ if it isn't one already (see the `DefaultFrom` class for all the gory
+ details). For example:
+
+ >>> login = Str('login', default=u'my-static-login-default',
+ ... default_from=lambda first, last: (first[0] + last).lower(),
+ ... )
+ >>> isinstance(login.default_from, DefaultFrom)
+ True
+ >>> login.default_from.keys
+ ('first', 'last')
+
+ Then when all the keys needed by the `DefaultFrom` instance are present,
+ the dynamic default is constructed and returned. For example:
+
+ >>> kw = dict(last=u'Doe', first=u'John')
+ >>> login.get_default(**kw)
+ u'jdoe'
+
+ Or if any keys are missing, your *static* default is returned.
+ For example:
+
+ >>> kw = dict(first=u'John', department=u'Engineering')
+ >>> login.get_default(**kw)
+ u'my-static-login-default'
+
+ The second, less common way to construct a dynamic default is to provide
+ a callback via the ``create_default`` keyword argument. Unlike a
+ ``default_from`` callback, your ``create_default`` callback will not get
+ wrapped in any dispatcher. Instead, it will be called directly, which
+ means your callback must accept arbitrary keyword arguments, although
+ whether your callback utilises these values is up to your
+ implementation. For example:
+
+ >>> def make_csr(**kw):
+ ... print ' make_csr(%r)' % (kw,) # Note output below
+ ... return 'Certificate Signing Request'
+ ...
+ >>> csr = Bytes('csr', create_default=make_csr)
+
+ Your ``create_default`` callback will be called with whatever keyword
+ arguments are passed to `Param.get_default()`. For example:
+
+ >>> kw = dict(arbitrary='Keyword', arguments='Here')
+ >>> csr.get_default(**kw)
+ make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'})
+ 'Certificate Signing Request'
+
+ And your ``create_default`` callback is called even if
+ `Param.get_default()` is called with *zero* keyword arguments.
+ For example:
+
+ >>> csr.get_default()
+ make_csr({})
+ 'Certificate Signing Request'
+
+ The ``create_default`` callback will most likely be used as a
+ pre-execute hook to perform some special client-side operation. For
+ example, the ``csr`` parameter above might make a call to
+ ``/usr/bin/openssl``. However, often a ``create_default`` callback
+ could also be implemented as a ``default_from`` callback. When this is
+ the case, a ``default_from`` callback should be used as they are more
+ structured and therefore less error-prone.
+
+ The ``default_from`` and ``create_default`` keyword arguments are
+ mutually exclusive. If you provide both, a ``ValueError`` will be
+ raised. For example:
+
+ >>> homedir = Str('home',
+ ... default_from=lambda login: '/home/%s' % login,
+ ... create_default=lambda **kw: '/lets/use/this',
+ ... )
+ Traceback (most recent call last):
+ ...
+ ValueError: Str('home'): cannot have both 'default_from' and 'create_default'
+ """
+ if self._get_default is not None:
+ default = self._get_default(**kw)
+ if default is not None:
+ try:
+ return self.convert(self.normalize(default))
+ except StandardError:
+ pass
+ return self.default
+
+
+class Bool(Param):
+ """
+ A parameter for boolean values (stored in the ``bool`` type).
+ """
+
+ type = bool
+ type_error = _('must be True or False')
+
+
+class Flag(Bool):
+ """
+ A boolean parameter that always gets filled in with a default value.
+
+ This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. If no
+ default is provided, it also fills in a default value of ``False``.
+ Lastly, unlike the `Bool` class, the default must be either ``True`` or
+ ``False`` and cannot be ``None``.
+
+ For example:
+
+ >>> flag = Flag('my_flag')
+ >>> (flag.autofill, flag.default)
+ (True, False)
+
+ To have a default value of ``True``, create your `Flag` intance with
+ ``default=True``. For example:
+
+ >>> flag = Flag('my_flag', default=True)
+ >>> (flag.autofill, flag.default)
+ (True, True)
+
+ Also note that creating a `Flag` instance with ``autofill=False`` will have
+ no effect. For example:
+
+ >>> flag = Flag('my_flag', autofill=False)
+ >>> flag.autofill
+ True
+ """
+
+ def __init__(self, name, *rules, **kw):
+ kw['autofill'] = True
+ if 'default' not in kw:
+ kw['default'] = False
+ if type(kw['default']) is not bool:
+ default = kw['default']
+ raise TypeError(
+ TYPE_ERROR % ('default', bool, default, type(default))
+ )
+ super(Flag, self).__init__(name, *rules, **kw)
+
+
+class Int(Param):
+ """
+ A parameter for integer values (stored in the ``int`` type).
+ """
+
+ type = int
+ type_error = _('must be an integer')
+
+
+class Float(Param):
+ """
+ A parameter for floating-point values (stored in the ``float`` type).
+ """
+
+ type = float
+ type_error = _('must be a decimal number')
+
+
+class Bytes(Param):
+ """
+ A parameter for binary data (stored in the ``str`` type).
+
+ This class is named *Bytes* instead of *Str* so it's aligned with the
+ Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See:
+
+ http://docs.python.org/3.0/whatsnew/3.0.html
+ """
+
+ type = str
+ type_error = _('must be binary data')
+
+ kwargs = Param.kwargs + (
+ ('minlength', int, None),
+ ('maxlength', int, None),
+ ('length', int, None),
+ ('pattern', str, None),
+
+ )
+
+ def __init__(self, name, *rules, **kw):
+ super(Bytes, self).__init__(name, *rules, **kw)
+
+ if not (
+ self.length is None or
+ (self.minlength is None and self.maxlength is None)
+ ):
+ raise ValueError(
+ '%s: cannot mix length with minlength or maxlength' % self.nice
+ )
+
+ if self.minlength is not None and self.minlength < 1:
+ raise ValueError(
+ '%s: minlength must be >= 1; got %r' % (self.nice, self.minlength)
+ )
+
+ if self.maxlength is not None and self.maxlength < 1:
+ raise ValueError(
+ '%s: maxlength must be >= 1; got %r' % (self.nice, self.maxlength)
+ )
+
+ if None not in (self.minlength, self.maxlength):
+ if self.minlength > self.maxlength:
+ raise ValueError(
+ '%s: minlength > maxlength (minlength=%r, maxlength=%r)' % (
+ self.nice, self.minlength, self.maxlength)
+ )
+ elif self.minlength == self.maxlength:
+ raise ValueError(
+ '%s: minlength == maxlength; use length=%d instead' % (
+ self.nice, self.minlength)
+ )
+
+ def _rule_minlength(self, _, value):
+ """
+ Check minlength constraint.
+ """
+ assert type(value) is str
+ if len(value) < self.minlength:
+ return _('must be at least %(minlength)d bytes') % dict(
+ minlength=self.minlength,
+ )
+
+ def _rule_maxlength(self, _, value):
+ """
+ Check maxlength constraint.
+ """
+ assert type(value) is str
+ if len(value) > self.maxlength:
+ return _('can be at most %(maxlength)d bytes') % dict(
+ maxlength=self.maxlength,
+ )
+
+ def _rule_length(self, _, value):
+ """
+ Check length constraint.
+ """
+ assert type(value) is str
+ if len(value) != self.length:
+ return _('must be exactly %(length)d bytes') % dict(
+ length=self.length,
+ )
+
+
+class Str(Bytes):
+ """
+ A parameter for Unicode text (stored in the ``unicode`` type).
+
+ This class is named *Str* instead of *Unicode* so it's aligned with the
+ Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See:
+
+ http://docs.python.org/3.0/whatsnew/3.0.html
+ """
+
+ type = unicode
+ type_error = _('must be Unicode text')
+
+ kwargs = Bytes.kwargs[:-1] + (
+ ('pattern', unicode, None),
+ )
+
+ def _convert_scalar(self, value, index=None):
+ """
+ Convert a single scalar value.
+ """
+ if type(value) is self.type:
+ return value
+ if type(value) in (int, float):
+ return self.type(value)
+ raise ConversionError(name=self.name, index=index,
+ error=ugettext(self.type_error),
+ )
+
+ def _rule_minlength(self, _, value):
+ """
+ Check minlength constraint.
+ """
+ assert type(value) is unicode
+ if len(value) < self.minlength:
+ return _('must be at least %(minlength)d characters') % dict(
+ minlength=self.minlength,
+ )
+
+ def _rule_maxlength(self, _, value):
+ """
+ Check maxlength constraint.
+ """
+ assert type(value) is unicode
+ if len(value) > self.maxlength:
+ return _('can be at most %(maxlength)d characters') % dict(
+ maxlength=self.maxlength,
+ )
+
+ def _rule_length(self, _, value):
+ """
+ Check length constraint.
+ """
+ assert type(value) is unicode
+ if len(value) != self.length:
+ return _('must be exactly %(length)d characters') % dict(
+ length=self.length,
+ )
+
+
+def create_param(spec):
+ """
+ Create an `Str` instance from the shorthand ``spec``.
+
+ This function allows you to create `Str` parameters (the most common) from
+ a convenient shorthand that defines the parameter name, whether it is
+ required, and whether it is multivalue. (For the definition of the
+ shorthand syntax, see the `parse_param_spec()` function.)
+
+ If ``spec`` is an ``str`` instance, it will be used to create a new `Str`
+ parameter, which will be returned. For example:
+
+ >>> s = create_param('hometown?')
+ >>> s
+ Str('hometown?')
+ >>> (s.name, s.required, s.multivalue)
+ ('hometown', False, False)
+
+ On the other hand, if ``spec`` is already a `Param` instance, it is
+ returned unchanged. For example:
+
+ >>> b = Bytes('cert')
+ >>> create_param(b) is b
+ True
+
+ As a plugin author, you will not call this function directly (which would
+ be no more convenient than simply creating the `Str` instance). Instead,
+ `frontend.Command` will call it for you when it evaluates the
+ ``takes_args`` and ``takes_options`` attributes, and `frontend.Object`
+ will call it for you when it evaluates the ``takes_params`` attribute.
+
+ :param spec: A spec string or a `Param` instance.
+ """
+ if isinstance(spec, Param):
+ return spec
+ if type(spec) is not str:
+ raise TypeError(
+ TYPE_ERROR % ('spec', (str, Param), spec, type(spec))
+ )
+ return Str(spec)