[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