characteristic-14.3.0/ 0000755 0000765 0000024 00000000000 12445026300 015024 5 ustar hynek staff 0000000 0000000 characteristic-14.3.0/.coveragerc 0000644 0000765 0000024 00000000024 12423261361 017145 0 ustar hynek staff 0000000 0000000 [run]
branch = True
characteristic-14.3.0/.travis.yml 0000644 0000765 0000024 00000000525 12370411110 017131 0 ustar hynek staff 0000000 0000000 language: 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.rst 0000644 0000765 0000024 00000001565 12445024466 016725 0 ustar hynek staff 0000000 0000000 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 `_
characteristic-14.3.0/characteristic.egg-info/ 0000755 0000765 0000024 00000000000 12445026300 021506 5 ustar hynek staff 0000000 0000000 characteristic-14.3.0/characteristic.egg-info/dependency_links.txt 0000644 0000765 0000024 00000000001 12445026277 025571 0 ustar hynek staff 0000000 0000000
characteristic-14.3.0/characteristic.egg-info/PKG-INFO 0000644 0000765 0000024 00000007673 12445026277 022635 0 ustar hynek staff 0000000 0000000 Metadata-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.txt 0000644 0000765 0000024 00000000724 12445026277 023412 0 ustar hynek staff 0000000 0000000 .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.rst characteristic-14.3.0/characteristic.egg-info/top_level.txt 0000644 0000765 0000024 00000000043 12445026277 024252 0 ustar hynek staff 0000000 0000000 characteristic
test_characteristic
characteristic-14.3.0/characteristic.py 0000644 0000765 0000024 00000052025 12445026036 020400 0 ustar hynek staff 0000000 0000000 """
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.rst 0000644 0000765 0000024 00000003773 12423261361 017503 0 ustar hynek staff 0000000 0000000 How 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.txt 0000644 0000765 0000024 00000000033 12333712550 021065 0 ustar hynek staff 0000000 0000000 -e .
pytest
pytest-cov
tox
characteristic-14.3.0/docs/ 0000755 0000765 0000024 00000000000 12445026300 015754 5 ustar hynek staff 0000000 0000000 characteristic-14.3.0/docs/api.rst 0000644 0000765 0000024 00000010507 12423261361 017266 0 ustar hynek staff 0000000 0000000 .. _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.rst 0000644 0000765 0000024 00000005764 12445026023 020453 0 ustar hynek staff 0000000 0000000 .. 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.py 0000644 0000765 0000024 00000022607 12423261361 017266 0 ustar hynek staff 0000000 0000000 # -*- 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.rst 0000644 0000765 0000024 00000000064 12333720001 021210 0 ustar hynek staff 0000000 0000000 .. _contributing:
.. include:: ../CONTRIBUTING.rst
characteristic-14.3.0/docs/examples.rst 0000644 0000765 0000024 00000011505 12423261361 020332 0 ustar hynek staff 0000000 0000000 .. _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.rst 0000644 0000765 0000024 00000002260 12423261361 017621 0 ustar hynek staff 0000000 0000000 characteristic: 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.rst 0000644 0000765 0000024 00000000524 12333720031 020127 0 ustar hynek staff 0000000 0000000 License 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/Makefile 0000644 0000765 0000024 00000015212 12333703153 017421 0 ustar hynek staff 0000000 0000000 # 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.rst 0000644 0000765 0000024 00000012253 12423261361 017324 0 ustar hynek staff 0000000 0000000 .. _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/LICENSE 0000644 0000765 0000024 00000002072 12333147702 016040 0 ustar hynek staff 0000000 0000000 The 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.in 0000644 0000765 0000024 00000000250 12423261361 016563 0 ustar hynek staff 0000000 0000000 include *.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-INFO 0000644 0000765 0000024 00000007673 12445026300 016136 0 ustar hynek staff 0000000 0000000 Metadata-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.rst 0000644 0000765 0000024 00000003163 12423261361 016522 0 ustar hynek staff 0000000 0000000 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.
characteristic-14.3.0/setup.cfg 0000644 0000765 0000024 00000000251 12445026300 016643 0 ustar hynek staff 0000000 0000000 [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.py 0000644 0000765 0000024 00000005615 12426700016 016547 0 ustar hynek staff 0000000 0000000 import 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.py 0000644 0000765 0000024 00000056622 12445024466 021453 0 ustar hynek staff 0000000 0000000 from __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("