characteristic-14.3.0/0000755000076500000240000000000012445026300015024 5ustar hynekstaff00000000000000characteristic-14.3.0/.coveragerc0000644000076500000240000000002412423261361017145 0ustar hynekstaff00000000000000[run] branch = True characteristic-14.3.0/.travis.yml0000644000076500000240000000052512370411110017131 0ustar hynekstaff00000000000000language: python python: 2.7 env: - TOX_ENV=py26 - TOX_ENV=py27 - TOX_ENV=py33 - TOX_ENV=py34 - TOX_ENV=pypy - TOX_ENV=docs - TOX_ENV=flake8 - TOX_ENV=manifest install: - pip install tox coveralls script: - tox --hashseed 0 -e $TOX_ENV after_success: - coveralls notifications: email: false characteristic-14.3.0/AUTHORS.rst0000644000076500000240000000156512445024466016725 0ustar hynekstaff00000000000000Authors ------- ``characteristic`` is written and maintained by `Hynek Schlawack `_. The development is kindly supported by `Variomedia AG `_. It’s inspired by Twisted’s `FancyEqMixin `_ but is implemented using class decorators because `sub-classing is bad for you `_, m’kay? The following folks helped forming ``characteristic`` into what it is now: - `Adam Dangoor `_ - `Glyph `_ - `Itamar Turner-Trauring `_ - `Jean-Paul Calderone `_ - `Julian Berman `_ - `Richard Wall `_ - `Tom Prince `_ characteristic-14.3.0/characteristic.egg-info/0000755000076500000240000000000012445026300021506 5ustar hynekstaff00000000000000characteristic-14.3.0/characteristic.egg-info/dependency_links.txt0000644000076500000240000000000112445026277025571 0ustar hynekstaff00000000000000 characteristic-14.3.0/characteristic.egg-info/PKG-INFO0000644000076500000240000000767312445026277022635 0ustar hynekstaff00000000000000Metadata-Version: 1.1 Name: characteristic Version: 14.3.0 Summary: Python attributes without boilerplate. Home-page: https://characteristic.readthedocs.org/ Author: Hynek Schlawack Author-email: hs@ox.cx License: MIT Description: characteristic: Python attributes without boilerplate. ====================================================== .. image:: https://pypip.in/version/characteristic/badge.svg :target: https://pypi.python.org/pypi/characteristic/ :alt: Latest Version .. image:: https://travis-ci.org/hynek/characteristic.svg :target: https://travis-ci.org/hynek/characteristic :alt: CI status .. image:: https://coveralls.io/repos/hynek/characteristic/badge.png?branch=master :target: https://coveralls.io/r/hynek/characteristic?branch=master :alt: Current coverage .. begin ``characteristic`` is an `MIT `_-licensed Python package with class decorators that ease the chores of implementing the most common attribute-related object protocols. You just specify the attributes to work with and ``characteristic`` gives you any or all of: - a nice human-readable ``__repr__``, - a complete set of comparison methods, - immutability for attributes, - and a kwargs-based initializer (that cooperates with your existing one and optionally even checks the types of the arguments) *without* writing dull boilerplate code again and again. This gives you the power to use actual classes with actual types in your code instead of confusing ``tuple``\ s or confusingly behaving ``namedtuple``\ s. So put down that type-less data structures and welcome some class into your life! ``characteristic``\ ’s documentation lives at `Read the Docs `_, the code on `GitHub `_. It’s rigorously tested on Python 2.6, 2.7, 3.3+, and PyPy. Authors ------- ``characteristic`` is written and maintained by `Hynek Schlawack `_. The development is kindly supported by `Variomedia AG `_. It’s inspired by Twisted’s `FancyEqMixin `_ but is implemented using class decorators because `sub-classing is bad for you `_, m’kay? The following folks helped forming ``characteristic`` into what it is now: - `Adam Dangoor `_ - `Glyph `_ - `Itamar Turner-Trauring `_ - `Jean-Paul Calderone `_ - `Julian Berman `_ - `Richard Wall `_ - `Tom Prince `_ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules characteristic-14.3.0/characteristic.egg-info/SOURCES.txt0000644000076500000240000000072412445026277023412 0ustar hynekstaff00000000000000.coveragerc .travis.yml AUTHORS.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst characteristic.py dev-requirements.txt setup.cfg setup.py test_characteristic.py tox.ini characteristic.egg-info/PKG-INFO characteristic.egg-info/SOURCES.txt characteristic.egg-info/dependency_links.txt characteristic.egg-info/top_level.txt docs/Makefile docs/api.rst docs/changelog.rst docs/conf.py docs/contributing.rst docs/examples.rst docs/index.rst docs/license.rst docs/why.rstcharacteristic-14.3.0/characteristic.egg-info/top_level.txt0000644000076500000240000000004312445026277024252 0ustar hynekstaff00000000000000characteristic test_characteristic characteristic-14.3.0/characteristic.py0000644000076500000240000005202512445026036020400 0ustar hynekstaff00000000000000""" Python attributes without boilerplate. """ from __future__ import absolute_import, division, print_function import hashlib import linecache import sys import warnings __version__ = "14.3.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", ] PY26 = sys.version_info[0:2] == (2, 6) # I'm sorry. :( if sys.version_info[0] == 2: def exec_(code, locals_, globals_): exec("exec code in locals_, globals_") else: # pragma: no cover def exec_(code, locals_, globals_): exec(code, locals_, globals_) 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", "_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 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 __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented return ( self.name == other.name and self.exclude_from_cmp == other.exclude_from_cmp and self.exclude_from_init == other.exclude_from_init and self.exclude_from_repr == other.exclude_from_repr and self.exclude_from_immutable == other.exclude_from_immutable and self.default_value == other.default_value and self.default_factory == other.default_factory and self.instance_of == other.instance_of ) def __ne__(self, other): return not self == other def __repr__(self): return ( "" ).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`` """ attrs = [attr for attr in _ensure_attributes(attrs, defaults=kw.get("defaults", NOTHING)) if attr.exclude_from_init is False] # We cache the generated init methods for the same kinds of attributes. sha1 = hashlib.sha1() sha1.update(repr(attrs).encode("utf-8")) unique_filename = "".format( sha1.hexdigest() ) script = _attrs_to_script(attrs) locs = {} bytecode = compile(script, unique_filename, "exec") exec_(bytecode, {"NOTHING": NOTHING, "attrs": attrs}, locs) init = locs["characteristic_init"] def wrap(cl): cl.__original_init__ = cl.__init__ # In order of debuggers like PDB being able to step through the code, # we add a fake linecache entry. linecache.cache[unique_filename] = ( len(script), None, script.splitlines(True), unique_filename ) cl.__init__ = init return cl 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 _default_store_attributes(cls, attrs): """ Store attributes in :attr:`characteristic_attributes` on the class. """ cls.characteristic_attributes = attrs def attributes(attrs, apply_with_cmp=True, apply_with_init=True, apply_with_repr=True, apply_immutable=False, store_attributes=_default_store_attributes, **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 :param store_attributes: Store the given ``attr``\ s on the class. Should accept two arguments, the class and the attributes, in that order. Note that attributes passed in will always be instances of :class:`Attribute`\ , (so simple string attributes will already have been converted). By default if unprovided, attributes are stored in a ``characteristic_attributes`` attribute on the class. :type store_attributes: callable :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_*``. .. versionadded:: 14.2 Added ``store_attributes``. .. 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 """ create_init = kw.pop("create_init", None) if create_init is not None: apply_with_init = 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.pop("defaults", NOTHING)) if kw: raise TypeError( "attributes() got an unexpected keyword argument {0!r}".format( next(iter(kw)), ) ) def wrap(cl): store_attributes(cl, attrs) if apply_with_repr is True: cl = with_repr(attrs)(cl) if apply_with_cmp is True: cl = with_cmp(attrs)(cl) if apply_immutable is True: cl = immutable(attrs)(cl) if apply_with_init is True: cl = with_init(attrs)(cl) return cl return wrap def _attrs_to_script(attrs): """ Return a valid Python script of an initializer for *attrs*. """ if all(a.default_value is NOTHING and a.default_factory is None and a.instance_of is None for a in attrs) and not PY26: # Simple version does not work with Python 2.6 because of # http://bugs.python.org/issue10221 lines = _simple_init(attrs) else: lines = _verbose_init(attrs) return """\ 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. ''' {setters} self.__original_init__(*args, **kw) """.format(setters="\n ".join(lines)) def _simple_init(attrs): """ Create an init for *attrs* that doesn't care about defaults, default factories, or argument validators. This is a common case thus it's worth optimizing for. """ lines = ["try:"] for a in attrs: lines.append(" self.{a.name} = kw.pop('{a._kw_name}')".format(a=a)) lines += [ # We include "pass" here in case attrs is empty. Otherwise the "try" # suite is empty. " pass", "except KeyError as e:", " raise ValueError(\"Missing keyword value for " "'%s'.\" % (e.args[0],))" ] return lines def _verbose_init(attrs): """ Create return a list of lines that initialize *attrs* while honoring default values. """ lines = [] for i, a in enumerate(attrs): # attrs is passed into the the exec later to enable default_value # and default_factory. To find it, enumerate and 'i' are used. lines.append( "self.{a.name} = kw.pop('{a._kw_name}', {default})" .format( a=a, # Save a lookup for the common case of no default value. default="attrs[{i}].default_value".format(i=i) if a.default_value is not NOTHING else "NOTHING" ) ) if a.default_value is NOTHING: lines.append("if self.{a.name} is NOTHING:".format(a=a)) if a.default_factory is None: lines.append( " raise ValueError(\"Missing keyword value for " "'{a._kw_name}'.\")".format(a=a), ) else: lines.append( " self.{a.name} = attrs[{i}].default_factory()" .format(a=a, i=i) ) if a.instance_of: lines.append( "if not isinstance(self.{a.name}, attrs[{i}].instance_of):\n" .format(a=a, i=i) ) lines.append( " raise TypeError(\"Attribute '{a.name}' must be an" " instance of '{type_name}'.\")" .format(a=a, type_name=a.instance_of.__name__) ) return lines characteristic-14.3.0/CONTRIBUTING.rst0000644000076500000240000000377312423261361017503 0ustar hynekstaff00000000000000How To Contribute ================= Every open source project lives from the generous help by contributors that sacrifice their time and ``characteristic`` is no different. To make participation as pleasant as possible, this project adheres to the `Code of Conduct`_ by the Python Software Foundation. Here are a few guidelines to get you started: - Add yourself to the AUTHORS.rst_ file in an alphabetical fashion. Every contribution is valuable and shall be credited. - If your change is noteworthy, add an entry to the changelog_. - No contribution is too small; please submit as many fixes for typos and grammar bloopers as you can! - Don’t *ever* break backward compatibility. If it ever *has* to happen for higher reasons, ``characteristic`` will follow the proven procedures_ of the Twisted project. - *Always* add tests and docs for your code. This is a hard rule; patches with missing tests or documentation won’t be merged. If a feature is not tested or documented, it doesn’t exist. - Obey `PEP 8`_ and `PEP 257`_. - Write `good commit messages`_. .. note:: If you have something great but aren’t sure whether it adheres -- or even can adhere -- to the rules above: **please submit a pull request anyway**! In the best case, we can mold it into something, in the worst case the pull request gets politely closed. There’s absolutely nothing to fear. Thank you for considering to contribute to ``characteristic``! If you have any question or concerns, feel free to reach out to me. .. _`PEP 8`: http://legacy.python.org/dev/peps/pep-0008/ .. _`PEP 257`: http://legacy.python.org/dev/peps/pep-0257/ .. _`good commit messages`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html .. _`Code of Conduct`: https://www.python.org/psf/codeofconduct/ .. _changelog: https://github.com/hynek/characteristic/blob/master/docs/changelog.rst .. _AUTHORS.rst: https://github.com/hynek/characteristic/blob/master/AUTHORS.rst .. _procedures: http://twistedmatrix.com/trac/wiki/CompatibilityPolicy characteristic-14.3.0/dev-requirements.txt0000644000076500000240000000003312333712550021065 0ustar hynekstaff00000000000000-e . pytest pytest-cov tox characteristic-14.3.0/docs/0000755000076500000240000000000012445026300015754 5ustar hynekstaff00000000000000characteristic-14.3.0/docs/api.rst0000644000076500000240000001050712423261361017266 0ustar hynekstaff00000000000000.. _api: API === .. currentmodule:: characteristic ``characteristic`` consists of several class decorators that add features to your classes. There are four that add *one* feature each to your class. And then there's the helper ``@attributes`` that combines them all into one decorator so you don't have to repeat the attribute list multiple times. Generally the decorators take a list of attributes as their first positional argument. This list can consists of either native strings\ [*]_ for simple cases or instances of :class:`Attribute` that allow for more customization of ``characteristic``\ 's behavior. The easiest way to get started is to have a look at the :doc:`examples` to get a feeling for ``characteristic`` and return later for details! .. [*] Byte strings on Python 2 and Unicode strings on Python 3. .. note:: Every argument except for ``attrs`` for decorators and ``name`` for :class:`Attribute` is a **keyword argument**. Their positions are coincidental and not guaranteed to remain stable. .. autofunction:: attributes .. autofunction:: with_repr .. doctest:: >>> from characteristic import with_repr >>> @with_repr(["a", "b"]) ... class RClass(object): ... def __init__(self, a, b): ... self.a = a ... self.b = b >>> c = RClass(42, "abc") >>> print c .. autofunction:: with_cmp .. doctest:: >>> from characteristic import with_cmp >>> @with_cmp(["a", "b"]) ... class CClass(object): ... def __init__(self, a, b): ... self.a = a ... self.b = b >>> o1 = CClass(1, "abc") >>> o2 = CClass(1, "abc") >>> o1 == o2 # o1.a == o2.a and o1.b == o2.b True >>> o1.c = 23 >>> o2.c = 42 >>> o1 == o2 # attributes that are not passed to with_cmp are ignored True >>> o3 = CClass(2, "abc") >>> o1 < o3 # because 1 < 2 True >>> o4 = CClass(1, "bca") >>> o1 < o4 # o1.a == o4.a, but o1.b < o4.b True .. autofunction:: with_init .. doctest:: >>> from characteristic import with_init, Attribute >>> @with_init(["a", ... Attribute("b", default_factory=lambda: 2), ... Attribute("_c")]) ... class IClass(object): ... def __init__(self): ... if self.b != 2: ... raise ValueError("'b' must be 2!") >>> o1 = IClass(a=1, b=2, c=3) >>> o2 = IClass(a=1, c=3) >>> o1._c 3 >>> o1.a == o2.a True >>> o1.b == o2.b True >>> IClass() Traceback (most recent call last): ... ValueError: Missing keyword value for 'a'. >>> IClass(a=1, b=3) # the custom __init__ is called after the attributes are initialized Traceback (most recent call last): ... ValueError: 'b' must be 2! .. note:: The generated initializer explicitly does *not* support positional arguments. Those are *always* passed to the existing ``__init__`` unaltered. Used keyword arguments will *not* be passed to the original ``__init__`` method and have to be accessed on the class (i.e. ``self.a``). .. autofunction:: immutable .. doctest:: >>> from characteristic import immutable >>> @immutable([Attribute("foo")]) ... class ImmutableClass(object): ... foo = "bar" >>> ic = ImmutableClass() >>> ic.foo 'bar' >>> ic.foo = "not bar" Traceback (most recent call last): ... AttributeError: Attribute 'foo' of class 'ImmutableClass' is immutable. Please note, that that doesn't mean that the attributes themselves are immutable too: .. doctest:: >>> @immutable(["foo"]) ... class C(object): ... foo = [] >>> i = C() >>> i.foo = [42] Traceback (most recent call last): ... AttributeError: Attribute 'foo' of class 'C' is immutable. >>> i.foo.append(42) >>> i.foo [42] .. autoclass:: Attribute .. autofunction:: strip_leading_underscores .. doctest:: >>> from characteristic import strip_leading_underscores >>> strip_leading_underscores("_foo") 'foo' >>> strip_leading_underscores("__bar") 'bar' >>> strip_leading_underscores("___qux") 'qux' .. autodata:: NOTHING characteristic-14.3.0/docs/changelog.rst0000644000076500000240000000576412445026023020453 0ustar hynekstaff00000000000000.. currentmodule:: characteristic .. :changelog: Changelog ========= Versions are year-based with a strict backwards-compatibility policy. The third digit is only for regressions. 14.3.0 (2014-12-19) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - All decorators now gracefully accept empty attribute lists. [`22 `_]. ---- 14.2.0 (2014-10-30) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - Attributes set by :func:`characteristic.attributes` are now stored on the class as well. [`20 `_] - ``__init__`` methods that are created by :func:`characteristic.with_init` are now generated on the fly and optimized for each class. [`9 `_] ---- 14.1.0 (2014-08-22) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ *none* Changes: ^^^^^^^^ - Fix stray deprecation warnings. - Don't rely on warnings being switched on by command line. [`17 `_] ---- 14.0.0 (2014-08-21) ------------------- Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *none* Deprecations: ^^^^^^^^^^^^^ - The ``defaults`` argument of :func:`~characteristic.with_init` and :func:`~characteristic.attributes` has been deprecated in favor of the new explicit :class:`~characteristic.Attribute` class and it's superior ``default_value`` and ``default_factory`` arguments. - The ``create_init`` argument of :func:`~characteristic.attributes` has been deprecated in favor of the new ``apply_with_init`` argument for the sake of consistency. Changes: ^^^^^^^^ - Switch to a year-based version scheme. - Add :func:`~characteristic.immutable` to make certain attributes of classes immutable. Also add ``apply_immutable`` argument to :func:`~characteristic.attributes`. [`14 `_] - Add explicit :class:`~characteristic.Attribute` class and use it for default factories. [`8 `_] - Add aliasing of private attributes for :func:`~characteristic.with_init`\’s initializer when used together with :class:`~characteristic.Attribute`. Allow for custom aliasing via a callable. [`6 `_, `13 `_] - Add type checks to :func:`~characteristic.with_init`\’s initializer. [`12 `_] - Add possibility to hand-pick which decorators are applied from within :func:`~characteristic.attributes`. - Add possibility to exclude single attributes from certain decorators. ---- 0.1.0 (2014-05-11) ------------------ - Initial release. characteristic-14.3.0/docs/conf.py0000644000076500000240000002260712423261361017266 0ustar hynekstaff00000000000000# -*- coding: utf-8 -*- # # characteristic documentation build configuration file, created by # sphinx-quickstart on Sun May 11 16:17:15 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import codecs import datetime import os import re try: import sphinx_rtd_theme except ImportError: sphinx_rtd_theme = None def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: return f.read() def find_version(*file_paths): """ Build a path from *file_paths* and search for a ``__version__`` string inside. """ version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'characteristic' year = datetime.date.today().year copyright = u'2014{0}, Hynek Schlawack'.format( u'-{0}'.format(year) if year != 2014 else u"" ) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. release = find_version("../characteristic.py") version = release.rsplit(u".", 1)[0] # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if sphinx_rtd_theme: html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] else: html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'characteristicdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'characteristic.tex', u'characteristic Documentation', u'Hynek Schlawack', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'characteristic', u'characteristic Documentation', [u'Hynek Schlawack'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'characteristic', u'characteristic Documentation', u'Hynek Schlawack', 'characteristic', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/2/': None} characteristic-14.3.0/docs/contributing.rst0000644000076500000240000000006412333720001021210 0ustar hynekstaff00000000000000.. _contributing: .. include:: ../CONTRIBUTING.rst characteristic-14.3.0/docs/examples.rst0000644000076500000240000001150512423261361020332 0ustar hynekstaff00000000000000.. _examples: Examples ======== :func:`@attributes ` together with the definition of the attributes using class attributes enhances your class by: - a nice ``__repr__``, - comparison methods that compare instances as if they were tuples of their attributes, - and an initializer that uses the keyword arguments to initialize the specified attributes before running the class' own initializer (you just write the validator if you need anything more than type checks!). .. doctest:: >>> from characteristic import Attribute, attributes >>> @attributes(["a", "b"]) ... class C(object): ... pass >>> obj1 = C(a=1, b="abc") >>> obj1 >>> obj2 = C(a=2, b="abc") >>> obj1 == obj2 False >>> obj1 < obj2 True >>> obj3 = C(a=1, b="bca") >>> obj3 > obj1 True To offer more power and possibilities, ``characteristic`` comes with a distinct class to define attributes: :class:`~characteristic.Attribute`. It allows for things like default values for certain attributes, making them optional when ``characteristic``\ 's generated initializer is used: .. doctest:: >>> @attributes(["a", "b", Attribute("c", default_value=42)]) ... class CWithDefaults(object): ... pass >>> obj4 = CWithDefaults(a=1, b=2) >>> obj4.characteristic_attributes [, , )>] >>> obj5 = CWithDefaults(a=1, b=2, c=42) >>> obj4 == obj5 True ``characteristic`` also offers factories for default values of complex types: .. doctest:: >>> @attributes([Attribute("a", default_factory=list), ... Attribute("b", default_factory=dict)]) ... class CWithDefaultFactory(object): ... pass >>> obj6 = CWithDefaultFactory() >>> obj6 >>> obj7 = CWithDefaultFactory() >>> obj7 >>> obj6 == obj7 True >>> obj6.a is obj7.a False >>> obj6.b is obj7.b False You can also exclude certain attributes from certain decorators: .. doctest:: >>> @attributes(["host", "user", ... Attribute("password", exclude_from_repr=True), ... Attribute("_connection", exclude_from_init=True)]) ... class DB(object): ... _connection = None ... def connect(self): ... self._connection = "not really a connection" >>> db = DB(host="localhost", user="dba", password="secret") >>> db.connect() >>> db Immutable data structures are amazing! Guess what ``characteristic`` supports? .. doctest:: >>> @attributes([Attribute("a")], apply_immutable=True) ... class ImmutableClass(object): ... pass >>> ic = ImmutableClass(a=42) >>> ic.a 42 >>> ic.a = 43 Traceback (most recent call last): ... AttributeError: Attribute 'a' of class 'ImmutableClass' is immutable. >>> @attributes([Attribute("a")], apply_immutable=True) ... class AnotherImmutableClass(object): ... def __init__(self): ... self.a *= 2 >>> ic2 = AnotherImmutableClass(a=21) >>> ic2.a 42 >>> ic.a = 43 Traceback (most recent call last): ... AttributeError: Attribute 'a' of class 'AnotherImmutableClass' is immutable. You know what else is amazing? Type checks! .. doctest:: >>> @attributes([Attribute("a", instance_of=int)]) ... class TypeCheckedClass(object): ... pass >>> TypeCheckedClass(a="totally not an int") Traceback (most recent call last): ... TypeError: Attribute 'a' must be an instance of 'int'. And if you want your classes to have certain attributes private, ``characteristic`` will keep your keyword arguments clean if not told otherwise\ [*]_: .. doctest:: >>> @attributes([Attribute("_private")]) ... class CWithPrivateAttribute(object): ... pass >>> obj8 = CWithPrivateAttribute(private=42) >>> obj8._private 42 >>> @attributes([Attribute("_private", init_aliaser=None)]) ... class CWithPrivateAttributeNoAliasing(object): ... pass >>> obj9 = CWithPrivateAttributeNoAliasing(_private=42) >>> obj9._private 42 .. [*] This works *only* for attributes defined using the :class:`~characteristic.Attribute` class. characteristic-14.3.0/docs/index.rst0000644000076500000240000000226012423261361017621 0ustar hynekstaff00000000000000characteristic: Say 'yes' to types but 'no' to typing! ====================================================== Release v\ |release| (:doc:`What's new? `). .. include:: ../README.rst :start-after: begin Teaser ------ .. doctest:: >>> from characteristic import Attribute, attributes >>> @attributes(["a", "b"]) ... class AClass(object): ... pass >>> @attributes(["a", Attribute("b", default_value="abc", instance_of=str)]) ... class AnotherClass(object): ... pass >>> obj1 = AClass(a=1, b="abc") >>> obj2 = AnotherClass(a=1, b="abc") >>> obj3 = AnotherClass(a=1) >>> AnotherClass(a=1, b=42) Traceback (most recent call last): ... TypeError: Attribute 'b' must be an instance of 'str'. >>> print obj1, obj2, obj3 >>> obj1 == obj2 False >>> obj2 == obj3 True User's Guide ------------ .. toctree:: :maxdepth: 1 why examples api Project Information ^^^^^^^^^^^^^^^^^^^ .. toctree:: :maxdepth: 1 license contributing changelog Indices and tables ================== * :ref:`genindex` * :ref:`search` characteristic-14.3.0/docs/license.rst0000644000076500000240000000052412333720031020127 0ustar hynekstaff00000000000000License and Hall of Fame ======================== ``characteristic`` is licensed under the permissive `MIT `_ license. The full license text can be also found in the `source code repository `_. .. _authors: .. include:: ../AUTHORS.rst characteristic-14.3.0/docs/Makefile0000644000076500000240000001521212333703153017421 0ustar hynekstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/characteristic.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/characteristic.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/characteristic" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/characteristic" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." characteristic-14.3.0/docs/why.rst0000644000076500000240000001225312423261361017324 0ustar hynekstaff00000000000000.. _why: Why not… ======== …tuples? -------- Readability ^^^^^^^^^^^ What makes more sense while debugging:: or:: (1, 2) ? Let's add even more ambiguity:: or:: (42, 23, "Jane", "John") ? Why would you want to write ``customer[2]`` instead of ``customer.first_name``? Don't get me started when you add nesting. If you've never ran into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter then I am. Using proper classes with names and types makes program code much more readable and comprehensible_. Especially when trying to grok a new piece of software or returning to old code after several months. .. _comprehensible: http://arxiv.org/pdf/1304.5257.pdf Extendability ^^^^^^^^^^^^^ Imagine you have a function that takes or returns a tuple. Especially if you use tuple unpacking (eg. ``x, y = get_point()``), adding additional data means that you have to change the invocation of that function *everywhere*. Adding an attribute to a class concerns only those who actually care about that attribute. …namedtuples? ------------- The difference between namedtuple_\ s and classes decorated by ``characteristic`` is that the latter are type-sensitive and less typing aside regular classes: .. doctest:: >>> from characteristic import Attribute, attributes >>> @attributes([Attribute("a", instance_of=int)]) ... class C1(object): ... def __init__(self): ... if self.a >= 5: ... raise ValueError("'a' must be smaller 5!") ... def print_a(self): ... print self.a >>> @attributes([Attribute("a", instance_of=int)]) ... class C2(object): ... pass >>> c1 = C1(a=1) >>> c2 = C2(a=1) >>> c1.a == c2.a True >>> c1 == c2 False >>> c1.print_a() 1 >>> C1(a=5) Traceback (most recent call last): ... ValueError: 'a' must be smaller 5! …while namedtuple’s purpose is *explicitly* to behave like tuples: .. doctest:: >>> from collections import namedtuple >>> NT1 = namedtuple("NT1", "a") >>> NT2 = namedtuple("NT2", "b") >>> t1 = NT1._make([1,]) >>> t2 = NT2._make([1,]) >>> t1 == t2 == (1,) True This can easily lead to surprising and unintended behaviors. Other than that, ``characteristic`` also adds nifty features like type checks or default values. .. _namedtuple: https://docs.python.org/2/library/collections.html#collections.namedtuple .. _tuple: https://docs.python.org/2/tutorial/datastructures.html#tuples-and-sequences …hand-written classes? ---------------------- While I'm a fan of all things artisanal, writing the same nine methods all over again doesn't qualify for me. I usually manage to get some typos inside and there's simply more code that can break and thus has to be tested. To bring it into perspective, the equivalent of .. doctest:: >>> @attributes(["a", "b"]) ... class SmartClass(object): ... pass >>> SmartClass(a=1, b=2) is .. doctest:: >>> class ArtisinalClass(object): ... def __init__(self, a, b): ... self.a = a ... self.b = b ... ... def __repr__(self): ... return "".format(self.a, self.b) ... ... def __eq__(self, other): ... if other.__class__ is self.__class__: ... return (self.a, self.b) == (other.a, other.b) ... else: ... return NotImplemented ... ... def __ne__(self, other): ... result = self.__eq__(other) ... if result is NotImplemented: ... return NotImplemented ... else: ... return not result ... ... def __lt__(self, other): ... if other.__class__ is self.__class__: ... return (self.a, self.b) < (other.a, other.b) ... else: ... return NotImplemented ... ... def __le__(self, other): ... if other.__class__ is self.__class__: ... return (self.a, self.b) <= (other.a, other.b) ... else: ... return NotImplemented ... ... def __gt__(self, other): ... if other.__class__ is self.__class__: ... return (self.a, self.b) > (other.a, other.b) ... else: ... return NotImplemented ... ... def __ge__(self, other): ... if other.__class__ is self.__class__: ... return (self.a, self.b) >= (other.a, other.b) ... else: ... return NotImplemented ... ... def __hash__(self): ... return hash((self.a, self.b)) >>> ArtisinalClass(a=1, b=2) which is quite a mouthful and it doesn't even use any of ``characteristic``'s more advanced features like type checks or default values Also: no tests whatsoever. And who will guarantee you, that you don't accidentally flip the ``<`` in your tenth implementation of ``__gt__``? If you don't care and like typing, I'm not gonna stop you. But if you ever get sick of the repetitiveness, ``characteristic`` will be waiting for you. :) characteristic-14.3.0/LICENSE0000644000076500000240000000207212333147702016040 0ustar hynekstaff00000000000000The MIT License (MIT) Copyright (c) 2014 Hynek Schlawack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. characteristic-14.3.0/MANIFEST.in0000644000076500000240000000025012423261361016563 0ustar hynekstaff00000000000000include *.rst *.txt LICENSE tox.ini .travis.yml docs/Makefile .coveragerc recursive-include docs *.rst recursive-include docs *.py prune benchmark.py prune docs/_build characteristic-14.3.0/PKG-INFO0000644000076500000240000000767312445026300016136 0ustar hynekstaff00000000000000Metadata-Version: 1.1 Name: characteristic Version: 14.3.0 Summary: Python attributes without boilerplate. Home-page: https://characteristic.readthedocs.org/ Author: Hynek Schlawack Author-email: hs@ox.cx License: MIT Description: characteristic: Python attributes without boilerplate. ====================================================== .. image:: https://pypip.in/version/characteristic/badge.svg :target: https://pypi.python.org/pypi/characteristic/ :alt: Latest Version .. image:: https://travis-ci.org/hynek/characteristic.svg :target: https://travis-ci.org/hynek/characteristic :alt: CI status .. image:: https://coveralls.io/repos/hynek/characteristic/badge.png?branch=master :target: https://coveralls.io/r/hynek/characteristic?branch=master :alt: Current coverage .. begin ``characteristic`` is an `MIT `_-licensed Python package with class decorators that ease the chores of implementing the most common attribute-related object protocols. You just specify the attributes to work with and ``characteristic`` gives you any or all of: - a nice human-readable ``__repr__``, - a complete set of comparison methods, - immutability for attributes, - and a kwargs-based initializer (that cooperates with your existing one and optionally even checks the types of the arguments) *without* writing dull boilerplate code again and again. This gives you the power to use actual classes with actual types in your code instead of confusing ``tuple``\ s or confusingly behaving ``namedtuple``\ s. So put down that type-less data structures and welcome some class into your life! ``characteristic``\ ’s documentation lives at `Read the Docs `_, the code on `GitHub `_. It’s rigorously tested on Python 2.6, 2.7, 3.3+, and PyPy. Authors ------- ``characteristic`` is written and maintained by `Hynek Schlawack `_. The development is kindly supported by `Variomedia AG `_. It’s inspired by Twisted’s `FancyEqMixin `_ but is implemented using class decorators because `sub-classing is bad for you `_, m’kay? The following folks helped forming ``characteristic`` into what it is now: - `Adam Dangoor `_ - `Glyph `_ - `Itamar Turner-Trauring `_ - `Jean-Paul Calderone `_ - `Julian Berman `_ - `Richard Wall `_ - `Tom Prince `_ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules characteristic-14.3.0/README.rst0000644000076500000240000000316312423261361016522 0ustar hynekstaff00000000000000characteristic: Python attributes without boilerplate. ====================================================== .. image:: https://pypip.in/version/characteristic/badge.svg :target: https://pypi.python.org/pypi/characteristic/ :alt: Latest Version .. image:: https://travis-ci.org/hynek/characteristic.svg :target: https://travis-ci.org/hynek/characteristic :alt: CI status .. image:: https://coveralls.io/repos/hynek/characteristic/badge.png?branch=master :target: https://coveralls.io/r/hynek/characteristic?branch=master :alt: Current coverage .. begin ``characteristic`` is an `MIT `_-licensed Python package with class decorators that ease the chores of implementing the most common attribute-related object protocols. You just specify the attributes to work with and ``characteristic`` gives you any or all of: - a nice human-readable ``__repr__``, - a complete set of comparison methods, - immutability for attributes, - and a kwargs-based initializer (that cooperates with your existing one and optionally even checks the types of the arguments) *without* writing dull boilerplate code again and again. This gives you the power to use actual classes with actual types in your code instead of confusing ``tuple``\ s or confusingly behaving ``namedtuple``\ s. So put down that type-less data structures and welcome some class into your life! ``characteristic``\ ’s documentation lives at `Read the Docs `_, the code on `GitHub `_. It’s rigorously tested on Python 2.6, 2.7, 3.3+, and PyPy. characteristic-14.3.0/setup.cfg0000644000076500000240000000025112445026300016643 0ustar hynekstaff00000000000000[pytest] minversion = 2.6 strict = true norecursedirs = .* build dist test_data *.egg [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 characteristic-14.3.0/setup.py0000644000076500000240000000561512426700016016547 0ustar hynekstaff00000000000000import codecs import os import re import sys from setuptools import setup from setuptools.command.test import test as TestCommand def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: return f.read() def find_version(*file_paths): """ Build a path from *file_paths* and search for a ``__version__`` string inside. """ version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = None def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.pytest_args or [] + ["test_characteristic.py"]) sys.exit(errno) if __name__ == "__main__": setup( name="characteristic", version=find_version("characteristic.py"), description="Python attributes without boilerplate.", long_description=(read("README.rst") + "\n\n" + read("AUTHORS.rst")), url="https://characteristic.readthedocs.org/", license="MIT", author="Hynek Schlawack", author_email="hs@ox.cx", py_modules=["characteristic", "test_characteristic"], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ], install_requires=[ ], tests_require=[ "pytest" ], cmdclass={ "test": PyTest, }, ) characteristic-14.3.0/test_characteristic.py0000644000076500000240000005662212445024466021453 0ustar hynekstaff00000000000000from __future__ import absolute_import, division, print_function import linecache import sys import warnings import pytest from characteristic import ( Attribute, NOTHING, PY26, _attrs_to_script, _ensure_attributes, attributes, immutable, with_cmp, with_init, with_repr, ) PY2 = sys.version_info[0] == 2 warnings.simplefilter("always") class TestAttribute(object): def test_init_simple(self): """ Instantiating with just the name initializes properly. """ a = Attribute("foo") assert "foo" == a.name assert NOTHING is a.default_value def test_init_default_factory(self): """ Instantiating with default_factory creates a proper descriptor for _default. """ a = Attribute("foo", default_factory=list) assert NOTHING is a.default_value assert list() == a.default_factory() def test_init_default_value(self): """ Instantiating with default_value initializes default properly. """ a = Attribute("foo", default_value="bar") assert "bar" == a.default_value def test_ambiguous_defaults(self): """ Instantiating with both default_value and default_factory raises ValueError. """ with pytest.raises(ValueError): Attribute( "foo", default_value="bar", default_factory=lambda: 42 ) def test_missing_attr(self): """ Accessing inexistent attributes still raises an AttributeError. """ a = Attribute("foo") with pytest.raises(AttributeError): a.bar def test_alias(self): """ If an attribute with a leading _ is defined, the initializer keyword is stripped of it. """ a = Attribute("_private") assert "private" == a._kw_name def test_non_alias(self): """ The keyword name of a non-private """ a = Attribute("public") assert "public" == a._kw_name def test_dunder(self): """ Dunder gets all _ stripped. """ a = Attribute("__very_private") assert "very_private" == a._kw_name def test_init_aliaser_none(self): """ No aliasing if init_aliaser is None. """ a = Attribute("_private", init_aliaser=None) assert a.name == a._kw_name def test_init_aliaser(self): """ Any callable works for aliasing. """ a = Attribute("a", init_aliaser=lambda _: "foo") assert "foo" == a._kw_name def test_repr(self): """ repr returns the correct string. """ a = Attribute( name="name", exclude_from_cmp=True, exclude_from_init=True, exclude_from_repr=True, exclude_from_immutable=True, default_value=42, instance_of=str, init_aliaser=None ) assert ( "," " init_aliaser=None)>" ).format("type" if PY2 else "class") == repr(a) def test_eq_different_types(self): """ Comparing Attribute with something else returns NotImplemented. """ assert NotImplemented == Attribute(name="name").__eq__(None) def test_eq_equal(self): """ Equal Attributes are detected equal. """ kw = { "name": "name", "exclude_from_cmp": True, "exclude_from_init": False, "exclude_from_repr": True, "exclude_from_immutable": False, "default_value": 42, "instance_of": int, } assert Attribute(**kw) == Attribute(**kw) def test_eq_unequal(self): """ Equal Attributes are detected equal. """ kw = { "name": "name", "exclude_from_cmp": True, "exclude_from_init": False, "exclude_from_repr": True, "exclude_from_immutable": False, "default_value": 42, "instance_of": int, } for arg in kw.keys(): kw_mutated = dict(**kw) kw_mutated[arg] = "mutated" assert Attribute(**kw) != Attribute(**kw_mutated) @with_cmp(["a", "b"]) class CmpC(object): def __init__(self, a, b): self.a = a self.b = b class TestWithCmp(object): def test_equal(self): """ Equal objects are detected as equal. """ assert CmpC(1, 2) == CmpC(1, 2) assert not (CmpC(1, 2) != CmpC(1, 2)) def test_unequal_same_class(self): """ Unequal objects of correct type are detected as unequal. """ assert CmpC(1, 2) != CmpC(2, 1) assert not (CmpC(1, 2) == CmpC(2, 1)) def test_unequal_different_class(self): """ Unequal objects of differnt type are detected even if their attributes match. """ class NotCmpC(object): a = 1 b = 2 assert CmpC(1, 2) != NotCmpC() assert not (CmpC(1, 2) == NotCmpC()) @pytest.mark.parametrize( "a,b", [ ((1, 2), (2, 1)), ((1, 2), (1, 3)), (("a", "b"), ("b", "a")), ] ) def test_lt(self, a, b): """ __lt__ compares objects as tuples of attribute values. """ assert CmpC(*a) < CmpC(*b) def test_lt_unordable(self): """ __lt__ returns NotImplemented if classes differ. """ assert NotImplemented == (CmpC(1, 2).__lt__(42)) @pytest.mark.parametrize( "a,b", [ ((1, 2), (2, 1)), ((1, 2), (1, 3)), ((1, 1), (1, 1)), (("a", "b"), ("b", "a")), (("a", "b"), ("a", "b")), ] ) def test_le(self, a, b): """ __le__ compares objects as tuples of attribute values. """ assert CmpC(*a) <= CmpC(*b) def test_le_unordable(self): """ __le__ returns NotImplemented if classes differ. """ assert NotImplemented == (CmpC(1, 2).__le__(42)) @pytest.mark.parametrize( "a,b", [ ((2, 1), (1, 2)), ((1, 3), (1, 2)), (("b", "a"), ("a", "b")), ] ) def test_gt(self, a, b): """ __gt__ compares objects as tuples of attribute values. """ assert CmpC(*a) > CmpC(*b) def test_gt_unordable(self): """ __gt__ returns NotImplemented if classes differ. """ assert NotImplemented == (CmpC(1, 2).__gt__(42)) @pytest.mark.parametrize( "a,b", [ ((2, 1), (1, 2)), ((1, 3), (1, 2)), ((1, 1), (1, 1)), (("b", "a"), ("a", "b")), (("a", "b"), ("a", "b")), ] ) def test_ge(self, a, b): """ __ge__ compares objects as tuples of attribute values. """ assert CmpC(*a) >= CmpC(*b) def test_ge_unordable(self): """ __ge__ returns NotImplemented if classes differ. """ assert NotImplemented == (CmpC(1, 2).__ge__(42)) def test_hash(self): """ __hash__ returns different hashes for different values. """ assert hash(CmpC(1, 2)) != hash(CmpC(1, 1)) def test_Attribute_exclude_from_cmp(self): """ Ignores attribute if exclude_from_cmp=True. """ @with_cmp([Attribute("a", exclude_from_cmp=True), "b"]) class C(object): def __init__(self, a, b): self.a = a self.b = b assert C(42, 1) == C(23, 1) @with_repr(["a", "b"]) class ReprC(object): def __init__(self, a, b): self.a = a self.b = b class TestReprAttrs(object): def test_repr(self): """ Test repr returns a sensible value. """ assert "" == repr(ReprC(1, 2)) def test_Attribute_exclude_from_repr(self): """ Ignores attribute if exclude_from_repr=True. """ @with_repr([Attribute("a", exclude_from_repr=True), "b"]) class C(object): def __init__(self, a, b): self.a = a self.b = b assert "" == repr(C(1, 2)) @with_init([Attribute("a"), Attribute("b")]) class InitC(object): def __init__(self): if self.a == self.b: raise ValueError class TestWithInit(object): def test_sets_attributes(self): """ The attributes are initialized using the passed keywords. """ obj = InitC(a=1, b=2) assert 1 == obj.a assert 2 == obj.b def test_custom_init(self): """ The class initializer is called too. """ with pytest.raises(ValueError): InitC(a=1, b=1) def test_passes_args(self): """ All positional parameters are passed to the original initializer. """ @with_init(["a"]) class InitWithArg(object): def __init__(self, arg): self.arg = arg obj = InitWithArg(42, a=1) assert 42 == obj.arg assert 1 == obj.a def test_passes_remaining_kw(self): """ Keyword arguments that aren't used for attributes are passed to the original initializer. """ @with_init(["a"]) class InitWithKWArg(object): def __init__(self, kw_arg=None): self.kw_arg = kw_arg obj = InitWithKWArg(a=1, kw_arg=42) assert 42 == obj.kw_arg assert 1 == obj.a def test_does_not_pass_attrs(self): """ The attributes are removed from the keyword arguments before they are passed to the original initializer. """ @with_init(["a"]) class InitWithKWArgs(object): def __init__(self, **kw): assert "a" not in kw assert "b" in kw InitWithKWArgs(a=1, b=42) def test_defaults(self): """ If defaults are passed, they are used as fallback. """ @with_init(["a", "b"], defaults={"b": 2}) class InitWithDefaults(object): pass obj = InitWithDefaults(a=1) assert 2 == obj.b def test_missing_arg(self): """ Raises `ValueError` if a value isn't passed. """ with pytest.raises(ValueError) as e: InitC(a=1) assert "Missing keyword value for 'b'." == e.value.args[0] def test_defaults_conflict(self): """ Raises `ValueError` if both defaults and an Attribute are passed. """ with pytest.raises(ValueError) as e: @with_init([Attribute("a")], defaults={"a": 42}) class C(object): pass assert ( "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." == e.value.args[0] ) def test_attribute(self): """ String attributes are converted to Attributes and thus work. """ @with_init(["a"]) class C(object): pass o = C(a=1) assert 1 == o.a def test_default_factory(self): """ The default factory is used for each instance of missing keyword argument. """ @with_init([Attribute("a", default_factory=list)]) class C(object): pass o1 = C() o2 = C() assert o1.a is not o2.a def test_underscores(self): """ with_init takes keyword aliasing into account. """ @with_init([Attribute("_a")]) class C(object): pass c = C(a=1) assert 1 == c._a def test_plain_no_alias(self): """ str-based attributes don't get aliased for backward-compatibility. """ @with_init(["_a"]) class C(object): pass c = C(_a=1) assert 1 == c._a def test_instance_of_fail(self): """ Raise `TypeError` if an Attribute with an `instance_of` is is attempted to be set to a mismatched type. """ @with_init([Attribute("a", instance_of=int)]) class C(object): pass with pytest.raises(TypeError) as e: C(a="not an int!") assert ( "Attribute 'a' must be an instance of 'int'." == e.value.args[0] ) def test_instance_of_success(self): """ Setting an attribute to a value that doesn't conflict with an `instance_of` declaration works. """ @with_init([Attribute("a", instance_of=int)]) class C(object): pass c = C(a=42) assert 42 == c.a def test_Attribute_exclude_from_init(self): """ Ignores attribute if exclude_from_init=True. """ @with_init([Attribute("a", exclude_from_init=True), "b"]) class C(object): pass C(b=1) def test_deprecation_defaults(self): """ Emits a DeprecationWarning if `defaults` is used. """ with warnings.catch_warnings(record=True) as w: @with_init(["a"], defaults={"a": 42}) class C(object): pass assert ( '`defaults` has been deprecated in 14.0, please use the ' '`Attribute` class instead.' ) == w[0].message.args[0] assert issubclass(w[0].category, DeprecationWarning) def test_linecache(self): """ The created init method is added to the linecache so PDB shows it properly. """ attrs = [Attribute("a")] @with_init(attrs) class C(object): pass assert tuple == type(linecache.cache[C.__init__.__code__.co_filename]) def test_linecache_attrs_unique(self): """ If the attributes are the same, only one linecache entry is created. Since the key within the cache is the filename, this effectively means that the filenames must be equal if the attributes are equal. """ attrs = [Attribute("a")] @with_init(attrs[:]) class C1(object): pass @with_init(attrs[:]) class C2(object): pass assert ( C1.__init__.__code__.co_filename == C2.__init__.__code__.co_filename ) def test_linecache_different_attrs(self): """ Different Attributes have different generated filenames. """ @with_init([Attribute("a")]) class C1(object): pass @with_init([Attribute("b")]) class C2(object): pass assert ( C1.__init__.__code__.co_filename != C2.__init__.__code__.co_filename ) def test_no_attributes(self): """ Specifying no attributes doesn't raise an exception. """ @with_init([]) class C(object): pass C() class TestAttributes(object): def test_leaves_init_alone(self): """ If *apply_with_init* or *create_init* is `False`, leave __init__ alone. """ @attributes(["a"], apply_with_init=False) class C(object): pass @attributes(["a"], create_init=False) class CDeprecated(object): pass obj1 = C() obj2 = CDeprecated() with pytest.raises(AttributeError): obj1.a with pytest.raises(AttributeError): obj2.a def test_wraps_init(self): """ If *create_init* is `True`, build initializer. """ @attributes(["a", "b"], apply_with_init=True) class C(object): pass obj = C(a=1, b=2) assert 1 == obj.a assert 2 == obj.b def test_immutable(self): """ If *apply_immutable* is `True`, make class immutable. """ @attributes(["a"], apply_immutable=True) class ImmuClass(object): pass obj = ImmuClass(a=42) with pytest.raises(AttributeError): obj.a = "23" def test_apply_with_cmp(self): """ Don't add cmp methods if *apply_with_cmp* is `False`. """ @attributes(["a"], apply_with_cmp=False) class C(object): pass obj = C(a=1) if PY2: assert None is getattr(obj, "__eq__", None) else: assert object.__eq__ == C.__eq__ def test_apply_with_repr(self): """ Don't add __repr__ if *apply_with_repr* is `False`. """ @attributes(["a"], apply_with_repr=False) class C(object): pass assert repr(C(a=1)).startswith("=7.0 # to avoid .egg directories pytest-cov commands = python setup.py test -a "--cov characteristic --cov-report term-missing" [testenv:flake8] basepython = python2.7 deps = flake8 commands = flake8 characteristic.py test_characteristic.py [testenv:docs] basepython = python2.7 setenv = PYTHONHASHSEED = 0 deps = sphinx commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html [testenv:manifest] deps = check-manifest commands = check-manifest