123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- from __future__ import absolute_import, division, print_function
- """
- Python attributes without boilerplate.
- """
- import sys
- import warnings
- __version__ = "14.1.0"
- __author__ = "Hynek Schlawack"
- __license__ = "MIT"
- __copyright__ = "Copyright 2014 Hynek Schlawack"
- __all__ = [
- "Attribute",
- "NOTHING",
- "attributes",
- "immutable",
- "strip_leading_underscores",
- "with_cmp",
- "with_init",
- "with_repr",
- ]
- class _Nothing(object):
- """
- Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
- .. versionadded:: 14.0
- """
- def __repr__(self):
- return "NOTHING"
- NOTHING = _Nothing()
- """
- Sentinel to indicate the lack of a value when ``None`` is ambiguous.
- .. versionadded:: 14.0
- """
- def strip_leading_underscores(attribute_name):
- """
- Strip leading underscores from *attribute_name*.
- Used by default by the ``init_aliaser`` argument of :class:`Attribute`.
- :param attribute_name: The original attribute name to mangle.
- :type attribute_name: str
- :rtype: str
- """
- return attribute_name.lstrip("_")
- class Attribute(object):
- """
- A representation of an attribute.
- In the simplest case, it only consists of a name but more advanced
- properties like default values are possible too.
- All attributes on the Attribute class are *read-only*.
- :param name: Name of the attribute.
- :type name: str
- :param exclude_from_cmp: Ignore attribute in :func:`with_cmp`.
- :type exclude_from_cmp: bool
- :param exclude_from_init: Ignore attribute in :func:`with_init`.
- :type exclude_from_init: bool
- :param exclude_from_repr: Ignore attribute in :func:`with_repr`.
- :type exclude_from_repr: bool
- :param exclude_from_immutable: Ignore attribute in :func:`immutable`.
- :type exclude_from_immutable: bool
- :param default_value: A value that is used whenever this attribute isn't
- passed as an keyword argument to a class that is decorated using
- :func:`with_init` (or :func:`attributes` with
- ``apply_with_init=True``).
- Therefore, setting this makes an attribute *optional*.
- Since a default value of `None` would be ambiguous, a special sentinel
- :data:`NOTHING` is used. Passing it means the lack of a default value.
- :param default_factory: A factory that is used for generating default
- values whenever this attribute isn't passed as an keyword
- argument to a class that is decorated using :func:`with_init` (or
- :func:`attributes` with ``apply_with_init=True``).
- Therefore, setting this makes an attribute *optional*.
- :type default_factory: callable
- :param instance_of: If used together with :func:`with_init` (or
- :func:`attributes` with ``apply_with_init=True``), the passed value is
- checked whether it's an instance of the type passed here. The
- initializer then raises :exc:`TypeError` on mismatch.
- :type instance_of: type
- :param init_aliaser: A callable that is invoked with the name of the
- attribute and whose return value is used as the keyword argument name
- for the ``__init__`` created by :func:`with_init` (or
- :func:`attributes` with ``apply_with_init=True``). Uses
- :func:`strip_leading_underscores` by default to change ``_foo`` to
- ``foo``. Set to ``None`` to disable aliasing.
- :type init_aliaser: callable
- :raises ValueError: If both ``default_value`` and ``default_factory`` have
- been passed.
- .. versionadded:: 14.0
- """
- __slots__ = [
- "name", "exclude_from_cmp", "exclude_from_init", "exclude_from_repr",
- "exclude_from_immutable", "default_value", "default_factory",
- "instance_of", "init_aliaser", "_default", "_kw_name",
- ]
- def __init__(self,
- name,
- exclude_from_cmp=False,
- exclude_from_init=False,
- exclude_from_repr=False,
- exclude_from_immutable=False,
- default_value=NOTHING,
- default_factory=None,
- instance_of=None,
- init_aliaser=strip_leading_underscores):
- if (
- default_value is not NOTHING
- and default_factory is not None
- ):
- raise ValueError(
- "Passing both default_value and default_factory is "
- "ambiguous."
- )
- self.name = name
- self.exclude_from_cmp = exclude_from_cmp
- self.exclude_from_init = exclude_from_init
- self.exclude_from_repr = exclude_from_repr
- self.exclude_from_immutable = exclude_from_immutable
- self.default_value = default_value
- self.default_factory = default_factory
- if default_value is not NOTHING:
- self._default = default_value
- elif default_factory is None:
- self._default = NOTHING
- self.instance_of = instance_of
- self.init_aliaser = init_aliaser
- if init_aliaser is not None:
- self._kw_name = init_aliaser(name)
- else:
- self._kw_name = name
- def __getattr__(self, name):
- """
- If no value has been set to _default, we need to call a factory.
- """
- if name == "_default" and self.default_factory:
- return self.default_factory()
- else:
- raise AttributeError
- def __repr__(self):
- return (
- "<Attribute(name={name!r}, exclude_from_cmp={exclude_from_cmp!r}, "
- "exclude_from_init={exclude_from_init!r}, exclude_from_repr="
- "{exclude_from_repr!r}, exclude_from_immutable="
- "{exclude_from_immutable!r}, default_value={default_value!r}, "
- "default_factory={default_factory!r}, instance_of={instance_of!r},"
- " init_aliaser={init_aliaser!r})>"
- ).format(
- name=self.name, exclude_from_cmp=self.exclude_from_cmp,
- exclude_from_init=self.exclude_from_init,
- exclude_from_repr=self.exclude_from_repr,
- exclude_from_immutable=self.exclude_from_immutable,
- default_value=self.default_value,
- default_factory=self.default_factory, instance_of=self.instance_of,
- init_aliaser=self.init_aliaser,
- )
- def _ensure_attributes(attrs, defaults):
- """
- Return a list of :class:`Attribute` generated by creating new instances for
- all non-Attributes.
- """
- if defaults is not NOTHING:
- defaults = defaults or {}
- warnings.warn(
- "`defaults` has been deprecated in 14.0, please use the "
- "`Attribute` class instead.",
- DeprecationWarning,
- stacklevel=3,
- )
- else:
- defaults = {}
- rv = []
- for attr in attrs:
- if isinstance(attr, Attribute):
- if defaults != {}:
- raise ValueError(
- "Mixing of the 'defaults' keyword argument and passing "
- "instances of Attribute for 'attrs' is prohibited. "
- "Please don't use 'defaults' anymore, it has been "
- "deprecated in 14.0."
- )
- else:
- rv.append(attr)
- else:
- rv.append(
- Attribute(
- attr,
- init_aliaser=None,
- default_value=defaults.get(attr, NOTHING)
- )
- )
- return rv
- def with_cmp(attrs):
- """
- A class decorator that adds comparison methods based on *attrs*.
- For that, each class is treated like a ``tuple`` of the values of *attrs*.
- But only instances of *identical* classes are compared!
- :param attrs: Attributes to work with.
- :type attrs: :class:`list` of :class:`str` or :class:`Attribute`\ s.
- """
- def attrs_to_tuple(obj):
- """
- Create a tuple of all values of *obj*'s *attrs*.
- """
- return tuple(getattr(obj, a.name) for a in attrs)
- def eq(self, other):
- """
- Automatically created by characteristic.
- """
- if other.__class__ is self.__class__:
- return attrs_to_tuple(self) == attrs_to_tuple(other)
- else:
- return NotImplemented
- def ne(self, other):
- """
- Automatically created by characteristic.
- """
- result = eq(self, other)
- if result is NotImplemented:
- return NotImplemented
- else:
- return not result
- def lt(self, other):
- """
- Automatically created by characteristic.
- """
- if other.__class__ is self.__class__:
- return attrs_to_tuple(self) < attrs_to_tuple(other)
- else:
- return NotImplemented
- def le(self, other):
- """
- Automatically created by characteristic.
- """
- if other.__class__ is self.__class__:
- return attrs_to_tuple(self) <= attrs_to_tuple(other)
- else:
- return NotImplemented
- def gt(self, other):
- """
- Automatically created by characteristic.
- """
- if other.__class__ is self.__class__:
- return attrs_to_tuple(self) > attrs_to_tuple(other)
- else:
- return NotImplemented
- def ge(self, other):
- """
- Automatically created by characteristic.
- """
- if other.__class__ is self.__class__:
- return attrs_to_tuple(self) >= attrs_to_tuple(other)
- else:
- return NotImplemented
- def hash_(self):
- """
- Automatically created by characteristic.
- """
- return hash(attrs_to_tuple(self))
- def wrap(cl):
- cl.__eq__ = eq
- cl.__ne__ = ne
- cl.__lt__ = lt
- cl.__le__ = le
- cl.__gt__ = gt
- cl.__ge__ = ge
- cl.__hash__ = hash_
- return cl
- attrs = [a
- for a in _ensure_attributes(attrs, NOTHING)
- if a.exclude_from_cmp is False]
- return wrap
- def with_repr(attrs):
- """
- A class decorator that adds a human readable ``__repr__`` method to your
- class using *attrs*.
- :param attrs: Attributes to work with.
- :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
- """
- def repr_(self):
- """
- Automatically created by characteristic.
- """
- return "<{0}({1})>".format(
- self.__class__.__name__,
- ", ".join(a.name + "=" + repr(getattr(self, a.name))
- for a in attrs)
- )
- def wrap(cl):
- cl.__repr__ = repr_
- return cl
- attrs = [a
- for a in _ensure_attributes(attrs, NOTHING)
- if a.exclude_from_repr is False]
- return wrap
- def with_init(attrs, **kw):
- """
- A class decorator that wraps the ``__init__`` method of a class and sets
- *attrs* using passed *keyword arguments* before calling the original
- ``__init__``.
- Those keyword arguments that are used, are removed from the `kwargs` that
- is passed into your original ``__init__``. Optionally, a dictionary of
- default values for some of *attrs* can be passed too.
- Attributes that are defined using :class:`Attribute` and start with
- underscores will get them stripped for the initializer arguments by default
- (this behavior is changeable on per-attribute basis when instantiating
- :class:`Attribute`.
- :param attrs: Attributes to work with.
- :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
- :raises ValueError: If the value for a non-optional attribute hasn't been
- passed as a keyword argument.
- :raises ValueError: If both *defaults* and an instance of
- :class:`Attribute` has been passed.
- .. deprecated:: 14.0
- Use :class:`Attribute` instead of ``defaults``.
- :param defaults: Default values if attributes are omitted on instantiation.
- :type defaults: ``dict`` or ``None``
- """
- def characteristic_init(self, *args, **kw):
- """
- Attribute initializer automatically created by characteristic.
- The original `__init__` method is renamed to `__original_init__` and
- is called at the end with the initialized attributes removed from the
- keyword arguments.
- """
- for a in attrs:
- v = kw.pop(a._kw_name, NOTHING)
- if v is NOTHING:
- # Since ``a._default`` could be a property that calls
- # a factory, we make this a separate step.
- v = a._default
- if v is NOTHING:
- raise ValueError(
- "Missing keyword value for '{0}'.".format(a._kw_name)
- )
- if (
- a.instance_of is not None
- and not isinstance(v, a.instance_of)
- ):
- raise TypeError(
- "Attribute '{0}' must be an instance of '{1}'."
- .format(a.name, a.instance_of.__name__)
- )
- self.__characteristic_setattr__(a.name, v)
- self.__original_init__(*args, **kw)
- def wrap(cl):
- cl.__original_init__ = cl.__init__
- cl.__init__ = characteristic_init
- # Sidestep immutability sentry completely if possible..
- cl.__characteristic_setattr__ = getattr(
- cl, "__original_setattr__", cl.__setattr__
- )
- return cl
- attrs = [attr
- for attr in _ensure_attributes(attrs,
- defaults=kw.get("defaults",
- NOTHING))
- if attr.exclude_from_init is False]
- return wrap
- _VALID_INITS = frozenset(["characteristic_init", "__init__"])
- def immutable(attrs):
- """
- Class decorator that makes *attrs* of a class immutable.
- That means that *attrs* can only be set from an initializer. If anyone
- else tries to set one of them, an :exc:`AttributeError` is raised.
- .. versionadded:: 14.0
- """
- # In this case, we just want to compare (native) strings.
- attrs = frozenset(attr.name if isinstance(attr, Attribute) else attr
- for attr in _ensure_attributes(attrs, NOTHING)
- if attr.exclude_from_immutable is False)
- def characteristic_immutability_sentry(self, attr, value):
- """
- Immutability sentry automatically created by characteristic.
- If an attribute is attempted to be set from any other place than an
- initializer, a TypeError is raised. Else the original __setattr__ is
- called.
- """
- prev = sys._getframe().f_back
- if (
- attr not in attrs
- or
- prev is not None and prev.f_code.co_name in _VALID_INITS
- ):
- self.__original_setattr__(attr, value)
- else:
- raise AttributeError(
- "Attribute '{0}' of class '{1}' is immutable."
- .format(attr, self.__class__.__name__)
- )
- def wrap(cl):
- cl.__original_setattr__ = cl.__setattr__
- cl.__setattr__ = characteristic_immutability_sentry
- return cl
- return wrap
- def attributes(attrs, apply_with_cmp=True, apply_with_init=True,
- apply_with_repr=True, apply_immutable=False, **kw):
- """
- A convenience class decorator that allows to *selectively* apply
- :func:`with_cmp`, :func:`with_repr`, :func:`with_init`, and
- :func:`immutable` to avoid code duplication.
- :param attrs: Attributes to work with.
- :type attrs: ``list`` of :class:`str` or :class:`Attribute`\ s.
- :param apply_with_cmp: Apply :func:`with_cmp`.
- :type apply_with_cmp: bool
- :param apply_with_init: Apply :func:`with_init`.
- :type apply_with_init: bool
- :param apply_with_repr: Apply :func:`with_repr`.
- :type apply_with_repr: bool
- :param apply_immutable: Apply :func:`immutable`. The only one that is off
- by default.
- :type apply_immutable: bool
- :raises ValueError: If both *defaults* and an instance of
- :class:`Attribute` has been passed.
- .. versionadded:: 14.0
- Added possibility to pass instances of :class:`Attribute` in ``attrs``.
- .. versionadded:: 14.0
- Added ``apply_*``.
- .. deprecated:: 14.0
- Use :class:`Attribute` instead of ``defaults``.
- :param defaults: Default values if attributes are omitted on instantiation.
- :type defaults: ``dict`` or ``None``
- .. deprecated:: 14.0
- Use ``apply_with_init`` instead of ``create_init``. Until removal, if
- *either* if `False`, ``with_init`` is not applied.
- :param create_init: Apply :func:`with_init`.
- :type create_init: bool
- """
- if "create_init" in kw:
- apply_with_init = kw["create_init"]
- warnings.warn(
- "`create_init` has been deprecated in 14.0, please use "
- "`apply_with_init`.", DeprecationWarning,
- stacklevel=2,
- )
- attrs = _ensure_attributes(attrs, defaults=kw.get("defaults", NOTHING))
- def wrap(cl):
- if apply_with_repr is True:
- cl = with_repr(attrs)(cl)
- if apply_with_cmp is True:
- cl = with_cmp(attrs)(cl)
- # Order matters here because with_init can optimize and side-step
- # immutable's sentry function.
- if apply_immutable is True:
- cl = immutable(attrs)(cl)
- if apply_with_init is True:
- cl = with_init(attrs)(cl)
- return cl
- return wrap
|