[Python-modules-commits] [python-attrs] 01/06: Import python-attrs_16.3.0.orig.tar.gz
Tristan Seligmann
mithrandi at moszumanska.debian.org
Mon Dec 12 03:22:10 UTC 2016
This is an automated email from the git hooks/post-receive script.
mithrandi pushed a commit to branch master
in repository python-attrs.
commit f9f18163dabea726ad79ab693f42417448756a26
Author: Tristan Seligmann <mithrandi at mithrandi.net>
Date: Mon Dec 12 02:49:26 2016 +0200
Import python-attrs_16.3.0.orig.tar.gz
---
CHANGELOG.rst | 24 ++++++++-
CONTRIBUTING.rst | 103 +++++++++++++++++++++++++++++++------
docs/api.rst | 8 +--
docs/examples.rst | 45 ++++++++++++++++-
docs/extending.rst | 42 ++++++++++++++-
setup.cfg | 4 ++
src/attr/__init__.py | 2 +-
src/attr/_compat.py | 57 ++++++++++++++++++++-
src/attr/_make.py | 129 ++++++++++++++++++++++++++++++-----------------
tests/test_dark_magic.py | 1 +
tests/test_dunders.py | 33 +++++++++++-
tests/test_funcs.py | 3 +-
tests/test_make.py | 108 +++++++++++++++++++++++++++++++++++++--
tests/utils.py | 54 +++++++++++++++-----
14 files changed, 522 insertions(+), 91 deletions(-)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0d6118b..364c612 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,10 +1,32 @@
Changelog
=========
-Versions are year-based with a strict backwards compatibility policy.
+Versions follow `CalVer <http://calver.org>`_ with a strict backwards compatibility policy.
The third digit is only for regressions.
+16.3.0 (2016-11-24)
+-------------------
+
+Changes:
+^^^^^^^^
+
+- Attributes now can have user-defined metadata which greatly improves ``attrs``'s extensibility.
+ `#96 <https://github.com/hynek/attrs/pull/96>`_
+- Allow for a ``__attrs_post_init__`` method that -- if defined -- will get called at the end of the ``attrs``-generated ``__init__`` method.
+ `#111 <https://github.com/hynek/attrs/pull/111>`_
+- Add ``@attr.s(str=True)`` that will optionally create a ``__str__`` method that is identical to ``__repr__``.
+ This is mainly useful with ``Exception``\ s and other classes that rely on a useful ``__str__`` implementation but overwrite the default one through a poor own one.
+ Default Python class behavior is to use ``__repr__`` as ``__str__`` anyways.
+
+ If you tried using ``attrs`` with ``Exception``\ s and were puzzled by the tracebacks: this option is for you.
+- Don't overwrite ``__name__`` with ``__qualname__`` for ``attr.s(slots=True)`` classes.
+ `#99 <https://github.com/hynek/attrs/issues/99>`_
+
+
+----
+
+
16.2.0 (2016-09-17)
-------------------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index d1cc9a5..7086b12 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,26 +1,93 @@
How To Contribute
=================
-Every open source project lives due to generous help by contributors sacrificing their time; ``attrs`` is no different.
+First off, thank you for considering contributing to ``attrs``!
+It's people like *you* who make it is such a great tool for everyone.
-Here are a few guidelines to get you started:
+Here are a few guidelines to get you started (but don't be afraid to open half-finished PRs and ask questions if something is unclear!):
-- Try to limit each pull request to one change only.
-- To run the test suite, all you need is a recent tox_.
- It will ensure the test suite runs with all dependencies against all Python versions just as it will on `Travis CI`_.
- If you lack some Python versions, you can can always limit the environments like ``tox -e py27,py35`` (in that case you may want to look into pyenv_, which makes it very easy to install many different Python versions in parallel).
-- Make sure your changes pass our CI.
+
+Workflow
+--------
+
+- No contribution is too small!
+ Please submit as many fixes for typos and grammar bloopers as you can!
+- Try to limit each pull request to *one* change only.
+- *Always* add tests and docs for your code.
+ This is a hard rule; patches with missing tests or documentation can't be accepted.
+- Make sure your changes pass our CI_.
You won't get any feedback until it's green unless you ask for it.
-- If your change is noteworthy, add an entry to the changelog_.
- Use present tense, semantic newlines, and add a link to your pull request.
-- No contribution is too small; please submit as many fixes for typos and grammar bloopers as you can!
+- Once you've addressed review feedback, make sure to bump the pull request with a short note.
+ Maintainers don’t receive notifications when you push new commits.
- Don’t break `backward compatibility`_.
-- *Always* add tests and docs for your code.
- This is a hard rule; patches with missing tests or documentation won’t be merged.
-- Write `good test docstrings`_.
+
+
+Code
+----
+
- Obey `PEP 8`_ and `PEP 257`_.
-- If you address review feedback, make sure to bump the pull request.
- Maintainers don’t receive notifications when you push new commits.
+ We use the ``"""``\ -on-separate-lines style for docstrings:
+
+ .. code-block:: python
+
+ def func(x):
+ """
+ Does something.
+
+ :param str x: A very important parameter.
+
+ :rtype: str
+ """
+- If you add or change public APIs, tag the docstring using ``.. versionadded:: 16.0.0 WHAT`` or ``.. versionchanged:: 16.2.0 WHAT``.
+- Prefer double quotes (``"``) over single quotes (``'``) unless the string contains double quotes itself.
+
+
+Tests
+-----
+
+- Write your asserts as ``expected == actual`` to line them up nicely:
+
+ .. code-block:: python
+
+ x = f()
+
+ assert 42 == x.some_attribute
+ assert "foo" == x._a_private_attribute
+
+- To run the test suite, all you need is a recent tox_.
+ It will ensure the test suite runs with all dependencies against all Python versions just as it will on Travis CI.
+ If you lack some Python versions, you can can always limit the environments like ``tox -e py27,py35`` (in that case you may want to look into pyenv_, which makes it very easy to install many different Python versions in parallel).
+- Write `good test docstrings`_.
+- To ensure new features work well with the rest of the system, they should be also added to our `Hypothesis`_ testing strategy which you find in ``tests/util.py``.
+
+
+Documentation
+-------------
+
+- Use `semantic newlines`_ in reStructuredText_ files (files ending in ``.rst``):
+
+ .. code-block:: rst
+
+ This is a sentence.
+ This is another sentence.
+
+- If you add a new feature, demonstrate its awesomeness in the `examples page`_!
+- If your change is noteworthy, add an entry to the changelog_.
+ Use present tense, `semantic newlines`_, and add a link to your pull request:
+
+ .. code-block:: rst
+
+ - Add awesome new feature.
+ The feature really *is* awesome.
+ [`#1 <https://github.com/hynek/attrs/pull/1>`_]
+ - Fix nasty bug.
+ The bug really *was* nasty.
+ [`#2 <https://github.com/hynek/attrs/pull/2>`_]
+
+****
+
+Again, this list is mainly to help you to get started by codifying tribal knowledge and expectations.
+If something is unclear, feel free to ask for help!
Please note that this project is released with a Contributor `Code of Conduct`_.
By participating in this project you agree to abide by its terms.
@@ -37,5 +104,9 @@ Thank you for considering contributing to ``attrs``!
.. _changelog: https://github.com/hynek/attrs/blob/master/CHANGELOG.rst
.. _`backward compatibility`: https://attrs.readthedocs.io/en/latest/backward-compatibility.html
.. _`tox`: https://testrun.org/tox/
-.. _`Travis CI`: https://travis-ci.org/
.. _pyenv: https://github.com/yyuu/pyenv
+.. _reStructuredText: http://sphinx-doc.org/rest.html
+.. _semantic newlines: http://rhodesmill.org/brandon/2012/one-sentence-per-line/
+.. _examples page: https://github.com/hynek/attrs/blob/master/docs/examples.rst
+.. _Hypothesis: https://hypothesis.readthedocs.org
+.. _CI: https://travis-ci.org/hynek/attrs/
diff --git a/docs/api.rst b/docs/api.rst
index 8a1edda..b3cb60a 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -18,7 +18,7 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
Core
----
-.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=True, init=True, slots=False, frozen=False)
+.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=True, init=True, slots=False, frozen=False, str=False)
.. note::
@@ -69,7 +69,7 @@ Core
... class C(object):
... x = attr.ib()
>>> C.x
- Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None)
+ Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}))
.. autofunction:: attr.make_class
@@ -125,9 +125,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields(C)
- (Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None))
+ (Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})))
>>> attr.fields(C)[1]
- Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None)
+ Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}))
>>> attr.fields(C).y is attr.fields(C)[1]
True
diff --git a/docs/examples.rst b/docs/examples.rst
index 23e0ff5..dbca8a4 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -405,6 +405,29 @@ Converters are run *before* validators, so you can use validators to check the f
ValueError: x must be be at least 0.
+.. _metadata:
+
+Metadata
+--------
+
+All ``attrs`` attributes may include arbitrary metadata in the form on a read-only dictionary.
+
+.. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib(metadata={'my_metadata': 1})
+ >>> attr.fields(C).x.metadata
+ mappingproxy({'my_metadata': 1})
+ >>> attr.fields(C).x.metadata['my_metadata']
+ 1
+
+Metadata is not used by ``attrs``, and is meant to enable rich functionality in third-party libraries.
+The metadata dictionary follows the normal dictionary rules: keys need to be hashable, and both keys and values are recommended to be immutable.
+
+If you're the author of a third-party library with ``attrs`` integration, please see :ref:`Extending Metadata <extending_metadata>`.
+
+
.. _slots:
Slots
@@ -458,7 +481,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes:
... class C(object):
... x = attr.ib()
>>> C.x
- Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None)
+ Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}))
>>> @attr.s(slots=True)
... class C(object):
... x = attr.ib()
@@ -551,6 +574,26 @@ You can still have power over the attributes if you pass a dictionary of name: `
>>> i.y
[]
+Sometimes, you want to have your class's ``__init__`` method do more than just
+the initialization, validation, etc. that gets done for you automatically when
+using ``@attr.s``.
+To do this, just define a ``__attrs_post_init__`` method in your class.
+It will get called at the end of the generated ``__init__`` method.
+
+.. doctest::
+
+ >>> @attr.s
+ ... class C(object):
+ ... x = attr.ib()
+ ... y = attr.ib()
+ ... z = attr.ib(init=False)
+ ...
+ ... def __attrs_post_init__(self):
+ ... self.z = self.x + self.y
+ >>> obj = C(x=1, y=2)
+ >>> obj
+ C(x=1, y=2, z=3)
+
Finally, you can exclude single attributes from certain methods:
.. doctest::
diff --git a/docs/extending.rst b/docs/extending.rst
index 9a40a41..604fb0f 100644
--- a/docs/extending.rst
+++ b/docs/extending.rst
@@ -17,7 +17,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
... @attr.s
... class C(object):
... a = attr.ib()
- (Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None),)
+ (Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})),)
.. warning::
@@ -36,3 +36,43 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
pass
f = a(b(original_f))
+
+.. _extending_metadata:
+
+Metadata
+--------
+
+If you're the author of a third-party library with ``attrs`` integration, you may want to take advantage of attribute metadata.
+
+Here are some tips for effective use of metadata:
+
+- Try making your metadata keys and values immutable.
+ This keeps the entire ``Attribute`` instances immutable too.
+
+- To avoid metadata key collisions, consider exposing your metadata keys from your modules.::
+
+ from mylib import MY_METADATA_KEY
+
+ @attr.s
+ class C(object):
+ x = attr.ib(metadata={MY_METADATA_KEY: 1})
+
+ Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways.
+
+- Expose ``attr.ib`` wrappers for your specific metadata.
+ This is a more graceful approach if your users don't require metadata from other libraries.
+
+ .. doctest::
+
+ >>> MY_TYPE_METADATA = '__my_type_metadata'
+ >>>
+ >>> def typed(cls, default=attr.NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None, metadata={}):
+ ... metadata = dict() if not metadata else metadata
+ ... metadata[MY_TYPE_METADATA] = cls
+ ... return attr.ib(default, validator, repr, cmp, hash, init, convert, metadata)
+ >>>
+ >>> @attr.s
+ ... class C(object):
+ ... x = typed(int, default=1, init=False)
+ >>> attr.fields(C).x.metadata[MY_TYPE_METADATA]
+ <class 'int'>
diff --git a/setup.cfg b/setup.cfg
index 0dfa294..dc14c9a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,3 +7,7 @@ testpaths = tests
[bdist_wheel]
universal = 1
+
+[metadata]
+# ensure LICENSE is included in wheel metadata
+license_file = LICENSE
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index 251289a..8527e59 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -25,7 +25,7 @@ from . import filters
from . import validators
-__version__ = "16.2.0"
+__version__ = "16.3.0"
__title__ = "attrs"
__description__ = "Attributes Without Boilerplate"
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
index 792eda8..8cbcf16 100644
--- a/src/attr/_compat.py
+++ b/src/attr/_compat.py
@@ -1,13 +1,14 @@
from __future__ import absolute_import, division, print_function
import sys
+import types
PY2 = sys.version_info[0] == 2
if PY2:
- import types
+ from UserDict import IterableUserDict
# We 'bundle' isclass instead of using inspect as importing inspect is
# fairly expensive (order of 10-15 ms for a modern machine in 2016)
@@ -22,6 +23,57 @@ if PY2:
def iterkeys(d):
return d.iterkeys()
+
+ # Python 2 is bereft of a read-only dict proxy, so we make one!
+ class ReadOnlyDict(IterableUserDict):
+ """
+ Best-effort read-only dict wrapper.
+ """
+
+ def __setitem__(self, key, val):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise TypeError("'mappingproxy' object does not support item "
+ "assignment")
+
+ def update(self, _):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError("'mappingproxy' object has no attribute "
+ "'update'")
+
+ def __delitem__(self, _):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise TypeError("'mappingproxy' object does not support item "
+ "deletion")
+
+ def clear(self):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError("'mappingproxy' object has no attribute "
+ "'clear'")
+
+ def pop(self, key, default=None):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError("'mappingproxy' object has no attribute "
+ "'pop'")
+
+ def popitem(self):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError("'mappingproxy' object has no attribute "
+ "'popitem'")
+
+ def setdefault(self, key, default=None):
+ # We gently pretend we're a Python 3 mappingproxy.
+ raise AttributeError("'mappingproxy' object has no attribute "
+ "'setdefault'")
+
+ def __repr__(self):
+ # Override to be identical to the Python 3 version.
+ return "mappingproxy(" + repr(self.data) + ")"
+
+ def metadata_proxy(d):
+ res = ReadOnlyDict()
+ res.data.update(d) # We blocked update, so we have to do it like this.
+ return res
+
else:
def isclass(klass):
return isinstance(klass, type)
@@ -33,3 +85,6 @@ else:
def iterkeys(d):
return d.keys()
+
+ def metadata_proxy(d):
+ return types.MappingProxyType(dict(d))
diff --git a/src/attr/_make.py b/src/attr/_make.py
index db133df..c745015 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -6,13 +6,14 @@ import linecache
from operator import itemgetter
from . import _config
-from ._compat import iteritems, isclass, iterkeys
+from ._compat import iteritems, isclass, iterkeys, metadata_proxy
from .exceptions import FrozenInstanceError, NotAnAttrsClassError
# This is used at least twice, so cache it here.
_obj_setattr = object.__setattr__
-_init_convert_pat = '__attr_convert_{}'
+_init_convert_pat = "__attr_convert_{}"
_tuple_property_pat = " {attr_name} = property(itemgetter({index}))"
+_empty_metadata_singleton = metadata_proxy({})
class _Nothing(object):
@@ -48,7 +49,7 @@ Sentinel to indicate the lack of a value when ``None`` is ambiguous.
def attr(default=NOTHING, validator=None,
repr=True, cmp=True, hash=True, init=True,
- convert=None):
+ convert=None, metadata={}):
"""
Create a new attribute on a class.
@@ -97,6 +98,8 @@ def attr(default=NOTHING, validator=None,
to the desired format. It is given the passed-in value, and the
returned value will be used as the new value of the attribute. The
value is converted before being passed to the validator, if any.
+ :param metadata: An arbitrary mapping, to be used by third-party
+ components.
"""
return _CountingAttr(
default=default,
@@ -106,6 +109,7 @@ def attr(default=NOTHING, validator=None,
hash=hash,
init=init,
convert=convert,
+ metadata=metadata,
)
@@ -132,8 +136,8 @@ def _make_attr_tuple_class(cls_name, attr_names):
))
else:
attr_class_template.append(" pass")
- globs = {'itemgetter': itemgetter}
- eval(compile("\n".join(attr_class_template), '', 'exec'), globs)
+ globs = {"itemgetter": itemgetter}
+ eval(compile("\n".join(attr_class_template), "", "exec"), globs)
return globs[attr_class_name]
@@ -199,8 +203,8 @@ def _frozen_setattrs(self, name, value):
def attributes(maybe_cls=None, these=None, repr_ns=None,
repr=True, cmp=True, hash=True, init=True,
- slots=False, frozen=False):
- """
+ slots=False, frozen=False, str=False):
+ r"""
A class decorator that adds `dunder
<https://wiki.python.org/moin/DunderAlias>`_\ -methods according to the
specified attributes using :func:`attr.ib` or the *these* argument.
@@ -211,7 +215,7 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
Django models) or don't want to (e.g. if you want to use
:class:`properties <property>`).
- If *these* is not `None`, the class body is *ignored*.
+ If *these* is not ``None``, the class body is *ignored*.
:type these: :class:`dict` of :class:`str` to :func:`attr.ib`
@@ -220,6 +224,9 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
namespace explicitly for a more meaningful ``repr`` output.
:param bool repr: Create a ``__repr__`` method with a human readable
represantation of ``attrs`` attributes..
+ :param bool str: Create a ``__str__`` method that is identical to
+ ``__repr__``. This is usually not necessary except for
+ :class:`Exception`\ s.
:param bool cmp: Create ``__eq__``, ``__ne__``, ``__lt__``, ``__le__``,
``__gt__``, and ``__ge__`` methods that compare the class as if it were
a tuple of its ``attrs`` attributes. But the attributes are *only*
@@ -228,7 +235,8 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
:func:`hash` of a tuple of all ``attrs`` attribute values.
:param bool init: Create a ``__init__`` method that initialiazes the
``attrs`` attributes. Leading underscores are stripped for the
- argument name.
+ argument name. If a ``__attrs_post_init__`` method exists on the
+ class, it will be called after the class is fully initialized.
:param bool slots: Create a slots_-style class that's more
memory-efficient. See :ref:`slots` for further ramifications.
:param bool frozen: Make instances immutable after initialization. If
@@ -250,10 +258,17 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
+ .. versionadded:: 16.3.0 *str*, and support for ``__attrs_post_init__``.
"""
def wrap(cls):
if getattr(cls, "__class__", None) is None:
raise TypeError("attrs only works with new-style classes.")
+
+ if repr is False and str is True:
+ raise ValueError(
+ "__str__ can only be generated if a __repr__ exists."
+ )
+
if slots:
# Only need this later if we're using slots.
if these is None:
@@ -266,6 +281,8 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
_transform_attrs(cls, these)
if repr is True:
cls = _add_repr(cls, ns=repr_ns)
+ if str is True:
+ cls.__str__ = cls.__repr__
if cmp is True:
cls = _add_cmp(cls)
if hash is True:
@@ -283,14 +300,12 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
for ca_name in ca_list:
# It might not actually be in there, e.g. if using 'these'.
cls_dict.pop(ca_name, None)
- cls_dict.pop('__dict__', None)
-
- if repr_ns is None:
- cls_name = getattr(cls, "__qualname__", cls.__name__)
- else:
- cls_name = cls.__name__
+ cls_dict.pop("__dict__", None)
- cls = type(cls_name, cls.__bases__, cls_dict)
+ qualname = getattr(cls, "__qualname__", None)
+ cls = type(cls.__name__, cls.__bases__, cls_dict)
+ if qualname is not None:
+ cls.__qualname__ = qualname
return cls
@@ -448,7 +463,11 @@ def _add_init(cls, frozen):
sha1.hexdigest()
)
- script, globs = _attrs_to_script(attrs, frozen)
+ script, globs = _attrs_to_script(
+ attrs,
+ frozen,
+ getattr(cls, "__attrs_post_init__", False),
+ )
locs = {}
bytecode = compile(script, unique_filename, "exec")
attr_dict = dict((a.name, a) for a in attrs)
@@ -542,7 +561,7 @@ def validate(inst):
a.validator(inst, a, getattr(inst, a.name))
-def _attrs_to_script(attrs, frozen):
+def _attrs_to_script(attrs, frozen, post_init):
"""
Return a script of an initializer for *attrs* and a dict of globals.
@@ -682,6 +701,8 @@ def _attrs_to_script(attrs, frozen):
a.name))
names_for_globals[val_name] = a.validator
names_for_globals[attr_name] = a
+ if post_init:
+ lines.append("self.__attrs_post_init__()")
return """\
def __init__(self, {args}):
@@ -700,42 +721,44 @@ class Attribute(object):
Plus *all* arguments of :func:`attr.ib`.
"""
- __slots__ = ('name', 'default', 'validator', 'repr', 'cmp', 'hash', 'init',
- 'convert')
-
- _optional = {"convert": None}
+ __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init",
+ "convert", "metadata")
def __init__(self, name, default, validator, repr, cmp, hash, init,
- convert=None):
+ convert=None, metadata=None):
# Cache this descriptor here to speed things up later.
__bound_setattr = _obj_setattr.__get__(self, Attribute)
- __bound_setattr('name', name)
- __bound_setattr('default', default)
- __bound_setattr('validator', validator)
- __bound_setattr('repr', repr)
- __bound_setattr('cmp', cmp)
- __bound_setattr('hash', hash)
- __bound_setattr('init', init)
- __bound_setattr('convert', convert)
+ __bound_setattr("name", name)
+ __bound_setattr("default", default)
+ __bound_setattr("validator", validator)
+ __bound_setattr("repr", repr)
+ __bound_setattr("cmp", cmp)
+ __bound_setattr("hash", hash)
+ __bound_setattr("init", init)
+ __bound_setattr("convert", convert)
+ __bound_setattr("metadata", (metadata_proxy(metadata) if metadata
+ else _empty_metadata_singleton))
def __setattr__(self, name, value):
raise FrozenInstanceError()
@classmethod
def from_counting_attr(cls, name, ca):
- return cls(name=name,
- **dict((k, getattr(ca, k))
- for k
- in Attribute.__slots__
- if k != "name"))
+ inst_dict = dict((k, getattr(ca, k))
+ for k
+ in Attribute.__slots__
+ if k != "name")
+ return cls(name=name, **inst_dict)
# Don't use _add_pickle since fields(Attribute) doesn't work
def __getstate__(self):
"""
Play nice with pickle.
"""
- return tuple(getattr(self, name) for name in self.__slots__)
+ return tuple(getattr(self, name) if name != "metadata"
+ else dict(self.metadata)
+ for name in self.__slots__)
def __setstate__(self, state):
"""
@@ -743,13 +766,20 @@ class Attribute(object):
"""
__bound_setattr = _obj_setattr.__get__(self, Attribute)
for name, value in zip(self.__slots__, state):
- __bound_setattr(name, value)
+ if name != "metadata":
+ __bound_setattr(name, value)
+ else:
+ __bound_setattr(name, metadata_proxy(value) if value else
+ _empty_metadata_singleton)
+
_a = [Attribute(name=name, default=NOTHING, validator=None,
- repr=True, cmp=True, hash=True, init=True)
+ repr=True, cmp=True, hash=(name != "metadata"), init=True)
for name in Attribute.__slots__]
+
Attribute = _add_hash(
- _add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a), attrs=_a
+ _add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a),
+ attrs=[a for a in _a if a.hash]
)
@@ -758,17 +788,23 @@ class _CountingAttr(object):
Intermediate representation of attributes that uses a counter to preserve
the order in which the attributes have been defined.
"""
- __attrs_attrs__ = [
+ __slots__ = ("counter", "default", "repr", "cmp", "hash", "init",
+ "metadata", "validator", "convert")
+ __attrs_attrs__ = tuple(
Attribute(name=name, default=NOTHING, validator=None,
repr=True, cmp=True, hash=True, init=True)
for name
in ("counter", "default", "repr", "cmp", "hash", "init",)
- ]
- counter = 0
+ ) + (
+ Attribute(name="metadata", default=None, validator=None,
+ repr=True, cmp=True, hash=False, init=True),
+ )
+ cls_counter = 0
- def __init__(self, default, validator, repr, cmp, hash, init, convert):
- _CountingAttr.counter += 1
- self.counter = _CountingAttr.counter
+ def __init__(self, default, validator, repr, cmp, hash, init, convert,
+ metadata):
+ _CountingAttr.cls_counter += 1
+ self.counter = _CountingAttr.cls_counter
self.default = default
self.validator = validator
self.repr = repr
@@ -776,6 +812,7 @@ class _CountingAttr(object):
self.hash = hash
self.init = init
self.convert = convert
+ self.metadata = metadata
_CountingAttr = _add_cmp(_add_repr(_CountingAttr))
diff --git a/tests/test_dark_magic.py b/tests/test_dark_magic.py
index 56c74f7..a36352e 100644
--- a/tests/test_dark_magic.py
+++ b/tests/test_dark_magic.py
@@ -23,6 +23,7 @@ class C1Slots(object):
x = attr.ib(validator=attr.validators.instance_of(int))
y = attr.ib()
+
foo = None
diff --git a/tests/test_dunders.py b/tests/test_dunders.py
index 4e6317b..841b622 100644
--- a/tests/test_dunders.py
+++ b/tests/test_dunders.py
@@ -18,8 +18,9 @@ from attr._make import (
_add_init,
_add_repr,
attr,
- make_class,
+ attributes,
fields,
+ make_class,
)
from attr.validators import instance_of
@@ -35,6 +36,7 @@ HashCSlots = simple_class(hash=True, slots=True)
class InitC(object):
__attrs_attrs__ = [simple_attr("a"), simple_attr("b")]
+
InitC = _add_init(InitC, False)
@@ -164,7 +166,7 @@ class TestAddRepr(object):
"""
Tests for `_add_repr`.
"""
- @given(booleans())
+ @pytest.mark.parametrize("slots", [True, False])
def test_repr(self, slots):
"""
If `repr` is False, ignore that attribute.
@@ -193,6 +195,33 @@ class TestAddRepr(object):
assert "C(_x=42)" == repr(i)
+ @pytest.mark.parametrize("add_str", [True, False])
+ def test_str(self, add_str):
+ """
+ If str is True, it returns the same as repr.
+
+ This only makes sense when subclassing a class with an poor __str__
+ (like Exceptions).
+ """
+ @attributes(str=add_str)
+ class Error(Exception):
+ x = attr()
+
+ e = Error(42)
+
+ assert (str(e) == repr(e)) is add_str
+
+ def test_str_no_repr(self):
+ """
+ Raises a ValueError if repr=False and str=True.
+ """
+ with pytest.raises(ValueError) as e:
+ simple_class(repr=False, str=True)
+
+ assert (
+ "__str__ can only be generated if a __repr__ exists."
+ ) == e.value.args[0]
+
class TestAddHash(object):
"""
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index b644a25..4738080 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -219,7 +219,8 @@ class TestAsTuple(object):
assert_proper_col_class(field_val, obj_tuple[index])
elif isinstance(field_val, (list, tuple)):
# This field holds a sequence of something.
- assert type(field_val) is type(obj_tuple[index]) # noqa: E721
+ expected_type = type(obj_tuple[index])
+ assert type(field_val) is expected_type # noqa: E721
for obj_e, obj_tuple_e in zip(field_val, obj_tuple[index]):
if has(obj_e.__class__):
assert_proper_col_class(obj_e, obj_tuple_e)
diff --git a/tests/test_make.py b/tests/test_make.py
index c5af44b..e672305 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -3,11 +3,12 @@ Tests for `attr._make`.
"""
from __future__ import absolute_import, division, print_function
+from operator import attrgetter
import pytest
from hypothesis import given
-from hypothesis.strategies import booleans, integers, sampled_from
+from hypothesis.strategies import booleans, integers, lists, sampled_from, text
from attr import _config
from attr._compat import PY2
@@ -24,9 +25,10 @@ from attr._make import (
)
from attr.exceptions import NotAnAttrsClassError
-from .utils import simple_attr, simple_attrs, simple_classes
+from .utils import (gen_attr_names, list_of_attrs, simple_attr, simple_attrs,
+ simple_attrs_without_metadata, simple_classes)
-attrs = simple_attrs.map(lambda c: Attribute.from_counting_attr('name', c))
+attrs = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c))
class TestCountingAttr(object):
@@ -103,7 +105,8 @@ class TestTransformAttrs(object):
"No mandatory attributes allowed after an attribute with a "
"default value or factory. Attribute in question: Attribute"
"(name='y', default=NOTHING, validator=None, repr=True, "
- "cmp=True, hash=True, init=True, convert=None)",
+ "cmp=True, hash=True, init=True, convert=None, "
+ "metadata=mappingproxy({}))",
) == e.value.args
def test_these(self):
@@ -287,6 +290,36 @@ class TestAttributes(object):
pass
assert "C.D()" == repr(C.D())
+ @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.")
+ @given(slots_outer=booleans(), slots_inner=booleans())
+ def test_name_not_overridden(self, slots_outer, slots_inner):
+ """
+ On Python 3, __name__ is different from __qualname__.
+ """
+ @attributes(slots=slots_outer)
+ class C(object):
+ @attributes(slots=slots_inner)
+ class D(object):
+ pass
+
+ assert C.D.__name__ == "D"
+ assert C.D.__qualname__ == C.__qualname__ + ".D"
+
+ def test_post_init(self):
+ """
+ Verify that __attrs_post_init__ gets called if defined.
+ """
+ @attributes
+ class C(object):
+ x = attr()
+ y = attr()
+
+ def __attrs_post_init__(self2):
+ self2.z = self2.x + self2.y
+
+ c = C(x=10, y=20)
+ assert 30 == getattr(c, 'z', None)
+
@attributes
class GC(object):
@@ -497,3 +530,70 @@ class TestValidate(object):
with pytest.raises(Exception) as e:
C(1)
assert (obj,) == e.value.args
+
+
+# Hypothesis seems to cache values, so the lists of attributes come out
+# unsorted.
+sorted_lists_of_attrs = list_of_attrs.map(
+ lambda l: sorted(l, key=attrgetter("counter")))
+
+
+class TestMetadata(object):
+ """
+ Tests for metadata handling.
+ """
+
+ @given(sorted_lists_of_attrs)
+ def test_metadata_present(self, list_of_attrs):
+ """
+ Assert dictionaries are copied and present.
+ """
+ C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
+
+ for hyp_attr, class_attr in zip(list_of_attrs, fields(C)):
+ if hyp_attr.metadata is None:
+ # The default is a singleton empty dict.
+ assert class_attr.metadata is not None
+ assert len(class_attr.metadata) == 0
+ else:
+ assert hyp_attr.metadata == class_attr.metadata
+
+ # Once more, just to assert getting items and iteration.
+ for k in class_attr.metadata:
+ assert hyp_attr.metadata[k] == class_attr.metadata[k]
+ assert (hyp_attr.metadata.get(k) ==
+ class_attr.metadata.get(k))
+
+ @given(simple_classes(), text())
+ def test_metadata_immutability(self, C, string):
+ """
+ The metadata dict should be best-effort immutable.
+ """
+ for a in fields(C):
+ with pytest.raises(TypeError):
+ a.metadata[string] = string
+ with pytest.raises(AttributeError):
+ a.metadata.update({string: string})
+ with pytest.raises(AttributeError):
+ a.metadata.clear()
+ with pytest.raises(AttributeError):
+ a.metadata.setdefault(string, string)
+
+ for k in a.metadata:
+ # For some reason, Python 3's MappingProxyType throws an
+ # IndexError for deletes on a large integer key.
+ with pytest.raises((TypeError, IndexError)):
+ del a.metadata[k]
+ with pytest.raises(AttributeError):
+ a.metadata.pop(k)
+ with pytest.raises(AttributeError):
+ a.metadata.popitem()
+
+ @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5))
+ def test_empty_metadata_singleton(self, list_of_attrs):
+ """
+ All empty metadata attributes share the same empty metadata dict.
+ """
+ C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
+ for a in fields(C)[1:]:
+ assert a.metadata is fields(C)[0].metadata
diff --git a/tests/utils.py b/tests/utils.py
index d91894c..c3f6cd3 100644
... 108 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-attrs.git
More information about the Python-modules-commits
mailing list