From cb6627d5f818f8dadb84e6e0ca5231bc2b196d7e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Dec 2021 07:38:35 +0100 Subject: [PATCH 01/60] Update comparisons to dataclasses (#872) * Start on why * Mention graduality * Better commas * Add pydantic * Paragraphs * Re-order why topics * typos * Address comments from @Julian Co-authored-by: Julian Berman * link features we talk about * Split cumbersome sentence * give example Co-authored-by: Julian Berman --- docs/why.rst | 115 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 47 deletions(-) diff --git a/docs/why.rst b/docs/why.rst index 3c5e5e6d2..2c0ca4cd6 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -5,51 +5,48 @@ Why not… If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs `_! -…tuples? --------- - - -Readability -^^^^^^^^^^^ - -What makes more sense while debugging:: - - Point(x=1, y=2) - -or:: - - (1, 2) - -? - -Let's add even more ambiguity:: +…Data Classes? +-------------- - Customer(id=42, reseller=23, first_name="Jane", last_name="John") +:pep:`557` added Data Classes to `Python 3.7 `_ that resemble ``attrs`` in many ways. -or:: +They are the result of the Python community's `wish `_ to have an easier way to write classes in the standard library that doesn't carry the problems of ``namedtuple``\ s. +To that end, ``attrs`` and its developers were involved in the PEP process and while we may disagree with some minor decisions that have been made, it's a fine library and if it stops you from abusing ``namedtuple``\ s, they are a huge win. - (42, 23, "Jane", "John") +Nevertheless, there are still reasons to prefer ``attrs`` over Data Classes. +Whether they're relevant to *you* depends on your circumstances: -? +- Data Classes are *intentionally* less powerful than ``attrs``. + There is a long list of features that were sacrificed for the sake of simplicity and while the most obvious ones are validators, converters, :ref:`equality customization `, or :doc:`extensibility ` in general, it permeates throughout all APIs. -Why would you want to write ``customer[2]`` instead of ``customer.first_name``? + On the other hand, Data Classes currently do not offer any significant feature that ``attrs`` doesn't already have. +- ``attrs`` supports all mainstream Python versions, including CPython 2.7 and PyPy. +- ``attrs`` doesn't force type annotations on you if you don't like them. +- But since it **also** supports typing, it's the best way to embrace type hints *gradually*, too. +- While Data Classes are implementing features from ``attrs`` every now and then, their presence is dependent on the Python version, not the package version. + For example, support for ``__slots__`` has only been added in Python 3.10. + That is especially painful for PyPI packages that support multiple Python versions. + This includes possible implementation bugs. +- ``attrs`` can and will move faster. + We are not bound to any release schedules and we have a clear deprecation policy. -Don't get me started when you add nesting. -If you've never run into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter than yours truly. + One of the `reasons `_ to not vendor ``attrs`` in the standard library was to not impede ``attrs``'s future development. -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. +One way to think about ``attrs`` vs Data Classes is that ``attrs`` is a fully-fledged toolkit to write powerful classes while Data Classes are an easy way to get a class with some attributes. +Basically what ``attrs`` was in 2015. -.. _comprehensible: https://arxiv.org/pdf/1304.5257.pdf +…pydantic? +---------- -Extendability -^^^^^^^^^^^^^ +*pydantic* is first an foremost a *data validation library*. +As such, it is a capable complement to class building libraries like ``attrs`` (or Data Classes!) for parsing and validating untrusted data. -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*. +However, as convenient as it might be, using it for your business or data layer `is problematic in several ways `_: +Is it really necessary to re-validate all your objects while reading them from a trusted database? +In the parlance of `Form, Command, and Model Validation `_, *pydantic* is the right tool for *Commands*. -Adding an attribute to a class concerns only those who actually care about that attribute. +`Separation of concerns `_ feels tedious at times, but it's one of those things that you get to appreciate once you've shot your own foot often enough. …namedtuples? @@ -57,7 +54,7 @@ Adding an attribute to a class concerns only those who actually care about that `collections.namedtuple`\ s are tuples with names, not classes. [#history]_ Since writing classes is tiresome in Python, every now and then someone discovers all the typing they could save and gets really excited. -However that convenience comes at a price. +However, that convenience comes at a price. The most obvious difference between ``namedtuple``\ s and ``attrs``-based classes is that the latter are type-sensitive: @@ -133,26 +130,50 @@ With ``attrs`` your users won't notice a difference because it creates regular, .. _behaving like a tuple: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences -…Data Classes? --------------- +…tuples? +-------- -:pep:`557` added Data Classes to `Python 3.7 `_ that resemble ``attrs`` in many ways. +Readability +^^^^^^^^^^^ -They are the result of the Python community's `wish `_ to have an easier way to write classes in the standard library that doesn't carry the problems of ``namedtuple``\ s. -To that end, ``attrs`` and its developers were involved in the PEP process and while we may disagree with some minor decisions that have been made, it's a fine library and if it stops you from abusing ``namedtuple``\ s, they are a huge win. +What makes more sense while debugging:: -Nevertheless, there are still reasons to prefer ``attrs`` over Data Classes whose relevancy depends on your circumstances: + Point(x=1, y=2) -- ``attrs`` supports all mainstream Python versions, including CPython 2.7 and PyPy. -- Data Classes are intentionally less powerful than ``attrs``. - There is a long list of features that were sacrificed for the sake of simplicity and while the most obvious ones are validators, converters, and ``__slots__``, it permeates throughout all APIs. +or:: - On the other hand, Data Classes currently do not offer any significant feature that ``attrs`` doesn't already have. -- ``attrs`` can and will move faster. - We are not bound to any release schedules and we have a clear deprecation policy. + (1, 2) - One of the `reasons `_ to not vendor ``attrs`` in the standard library was to not impede ``attrs``'s future development. +? + +Let's add even more ambiguity:: + + Customer(id=42, reseller=23, first_name="Jane", last_name="John") + +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 run into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter than yours truly. + +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: https://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. …dicts? From e8f552c6f02cc1fc7cbc1be23e17467df82ad344 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Dec 2021 12:40:19 +0100 Subject: [PATCH 02/60] invert --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 821280357..ff65a6738 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ Day-to-Day Usage ================ - `types` help you to write *correct* and *self-documenting* code. - ``attrs`` has first class support for them and even allows you to drop the calls to `attr.ib` on modern Python versions! + ``attrs`` has first class support for them, yet keeps them optional if you’re not convinced! - Instance initialization is one of ``attrs`` key feature areas. Our goal is to relieve you from writing as much code as possible. `init` gives you an overview what ``attrs`` has to offer and explains some related philosophies we believe in. From d9ed03a751ef31fed3b6d54192fca1c400f1e1b0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Dec 2021 14:33:02 +0100 Subject: [PATCH 03/60] Stress optionality --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 08e3952b2..c4960ebcd 100644 --- a/README.rst +++ b/README.rst @@ -82,9 +82,13 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. -`Type annotations `_ will also stay entirely **optional** forever. Please check out `On The Core API Names `_ for a more in-depth explanation. From f329fb6105e8f29af1aefe011e3565266e1f54bc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Dec 2021 14:34:17 +0100 Subject: [PATCH 04/60] This ain't Markdown --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c4960ebcd..cceff4668 100644 --- a/README.rst +++ b/README.rst @@ -85,7 +85,7 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. From e4e783b18f532576b9ee8c4e85e421606358c2b0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 25 Dec 2021 14:44:14 +0100 Subject: [PATCH 05/60] Use fully-qualified name --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cceff4668..1e7420e2e 100644 --- a/README.rst +++ b/README.rst @@ -85,9 +85,9 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. +This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0. The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. Please check out `On The Core API Names `_ for a more in-depth explanation. From a23fe5f8c802e01190a2894ac33130c691b97358 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 25 Dec 2021 14:46:17 +0100 Subject: [PATCH 06/60] Better sentence flow --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1e7420e2e..de2d6abaf 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,7 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. From e7345584ffb8de9016e2ccf736e702d0289b0401 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 25 Dec 2021 15:15:10 +0100 Subject: [PATCH 07/60] Add attrs namespace (#887) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + CHANGELOG.rst | 2 +- MANIFEST.in | 2 +- README.rst | 7 +- changelog.d/887.breaking.rst | 14 + conftest.py | 6 +- docs/api.rst | 522 +++++++++++++++++-------------- docs/comparison.rst | 2 +- docs/examples.rst | 16 +- docs/extending.rst | 10 +- docs/how-does-it-work.rst | 11 +- docs/init.rst | 26 +- docs/names.rst | 19 +- docs/types.rst | 2 +- pyproject.toml | 4 + src/attr/__init__.pyi | 1 + src/attr/_config.py | 4 +- src/attr/_funcs.py | 12 +- src/attr/_make.py | 22 +- src/attr/_next_gen.py | 56 +++- src/attr/converters.py | 9 +- src/attr/filters.py | 4 +- src/attr/validators.py | 8 +- src/attrs/__init__.py | 68 ++++ src/attrs/__init__.pyi | 63 ++++ src/attrs/converters.py | 1 + src/attrs/exceptions.py | 1 + src/attrs/filters.py | 1 + src/attrs/py.typed | 0 src/attrs/setters.py | 1 + src/attrs/validators.py | 1 + tests/test_next_gen.py | 171 +++++++--- tests/typing_example.py | 121 ++++++- 33 files changed, 820 insertions(+), 368 deletions(-) create mode 100644 changelog.d/887.breaking.rst create mode 100644 src/attrs/__init__.py create mode 100644 src/attrs/__init__.pyi create mode 100644 src/attrs/converters.py create mode 100644 src/attrs/exceptions.py create mode 100644 src/attrs/filters.py create mode 100644 src/attrs/py.typed create mode 100644 src/attrs/setters.py create mode 100644 src/attrs/validators.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d25e6ccfe..88f6415e9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,6 +19,7 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele - [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py). - [ ] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``). - [ ] ...and used in the stub test file `tests/typing_example.py`. + - [ ] If they've been added to `attr/__init__.pyi`, they've *also* been re-imported in `attrs/__init__.pyi`. - [ ] Updated **documentation** for changed code. - [ ] New functions/classes have to be added to `docs/api.rst` by hand. - [ ] Changes to the signature of `@attr.s()` have to be added by hand too. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2597ce9c9..bdf3a418a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ Whenever there is a need to break compatibility, it is announced here in the cha .. warning:: - The structure of the `attr.Attribute` class is exempt from this rule. + The structure of the `attrs.Attribute` class is exempt from this rule. It *will* change in the future, but since it should be considered read-only, that shouldn't matter. However if you intend to build extensions on top of ``attrs`` you have to anticipate that. diff --git a/MANIFEST.in b/MANIFEST.in index 398252bb9..3d68bf9c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,8 +2,8 @@ include LICENSE *.rst *.toml *.yml *.yaml *.ini graft .github # Stubs -include src/attr/py.typed recursive-include src *.pyi +recursive-include src py.typed # Tests include tox.ini conftest.py diff --git a/README.rst b/README.rst index de2d6abaf..a2aa04bbc 100644 --- a/README.rst +++ b/README.rst @@ -32,13 +32,12 @@ For that, it gives you a class decorator and a way to declaratively define the a .. code-block:: pycon - >>> from typing import List - >>> from attr import asdict, define, make_class, Factory + >>> from attrs import asdict, define, make_class, Factory >>> @define ... class SomeClass: ... a_number: int = 42 - ... list_of_numbers: List[int] = Factory(list) + ... list_of_numbers: list[int] = Factory(list) ... ... def hard_math(self, another_number): ... return self.a_number + sum(self.list_of_numbers) * another_number @@ -85,7 +84,7 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. diff --git a/changelog.d/887.breaking.rst b/changelog.d/887.breaking.rst new file mode 100644 index 000000000..98b4079ff --- /dev/null +++ b/changelog.d/887.breaking.rst @@ -0,0 +1,14 @@ +``import attrs`` has finally landed! +As of this release, you can finally import ``attrs`` using its proper name. + +Not all names from the ``attr`` namespace have been transferred; most notably ``attr.s`` and ``attr.ib`` are missing. +See ``attrs.define`` and ``attrs.field`` if you haven't seen our next-generation APIs yet. +A more elaborate explanation can be found `On The Core API Names `_ + +This feature is at least for one release **provisional**. +We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. + +The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. +Please note that we have **not** moved -- or even removed -- anything from ``attr``! + +Please do report any bugs or documentation inconsistencies! diff --git a/conftest.py b/conftest.py index 14ee0c10b..f3e7556be 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,8 @@ from __future__ import absolute_import, division, print_function -import sys - from hypothesis import HealthCheck, settings -from attr._compat import PY310 +from attr._compat import PY36, PY310 def pytest_configure(config): @@ -16,7 +14,7 @@ def pytest_configure(config): collect_ignore = [] -if sys.version_info[:2] < (3, 6): +if not PY36: collect_ignore.extend( [ "tests/test_annotations.py", diff --git a/docs/api.rst b/docs/api.rst index 1dcaf978e..bb52c0697 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,27 +3,122 @@ API Reference .. currentmodule:: attr -``attrs`` works by decorating a class using `attr.define` or `attr.s` and then optionally defining attributes on the class using `attr.field`, `attr.ib`, or a type annotation. +``attrs`` works by decorating a class using `attrs.define` or `attr.s` and then optionally defining attributes on the class using `attrs.field`, `attr.ib`, or a type annotation. If you're confused by the many names, please check out `names` for clarification. What follows is the API explanation, if you'd like a more hands-on introduction, have a look at `examples`. +As of version 21.3.0, ``attrs`` consists of **two** to-level package names: + +- The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` +- The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. + Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). + Using this namespace requires Python 3.6 or later. + +The ``attrs`` namespace is built *on top of* the ``attr`` which will *never* go away. + Core ---- - .. note:: - ``attrs`` 20.1.0 added a bunch of nicer APIs (sometimes referred to as next generation -- or NG -- APIs) that were intended to become the main way of defining classes in the future. - As of 21.1.0, they are not provisional anymore and are the **recommended** way to use ``attrs``! - The next step will be adding an importable ``attrs`` namespace. - The documentation will be updated successively. + Please not that the ``attrs`` namespace has been added in version 21.3.0. + Most of the objects are simply re-imported from ``attr``. + Therefore if a class, method, or function claims that it has been added in an older version, it is only available in the ``attr`` namespace. + +.. autodata:: attrs.NOTHING + +.. autofunction:: attrs.define + +.. function:: attrs.mutable(same_as_define) - Please have a look at :ref:`next-gen`! + Alias for `attrs.define`. + + .. versionadded:: 20.1.0 + +.. function:: attrs.frozen(same_as_define) + + Behaves the same as `attrs.define` but sets *frozen=True* and *on_setattr=None*. + + .. versionadded:: 20.1.0 -.. autodata:: attr.NOTHING +.. autofunction:: attrs.field + +.. function:: define + + Old import path for `attrs.define`. + +.. function:: mutable + + Old import path for `attrs.mutable`. + +.. function:: frozen + + Old import path for `attrs.frozen`. + +.. function:: field + + Old import path for `attrs.field`. + +.. autoclass:: attrs.Attribute + :members: evolve + + For example: + + .. doctest:: + + >>> import attr + >>> @attr.s + ... class C(object): + ... x = attr.ib() + >>> attr.fields(C).x + Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) + + +.. autofunction:: attrs.make_class + + This is handy if you want to programmatically create classes. + + For example: + + .. doctest:: + + >>> C1 = attr.make_class("C1", ["x", "y"]) + >>> C1(1, 2) + C1(x=1, y=2) + >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42), + ... "y": attr.ib(default=attr.Factory(list))}) + >>> C2() + C2(x=42, y=[]) + + +.. autoclass:: attrs.Factory + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(default=attr.Factory(list)) + ... y = attr.ib(default=attr.Factory( + ... lambda self: set(self.x), + ... takes_self=True) + ... ) + >>> C() + C(x=[], y=set()) + >>> C([1, 2, 3]) + C(x=[1, 2, 3], y={1, 2, 3}) + + +Classic +~~~~~~~ + +.. data:: attr.NOTHING + + Same as `attrs.NOTHING`. .. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True) @@ -93,69 +188,32 @@ Core ... ValueError: x must be positive -.. autoclass:: attr.Attribute - :members: evolve - - For example: - - .. doctest:: - - >>> import attr - >>> @attr.s - ... class C(object): - ... x = attr.ib() - >>> attr.fields(C).x - Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - -.. autofunction:: attr.make_class - This is handy if you want to programmatically create classes. - - For example: - - .. doctest:: - - >>> C1 = attr.make_class("C1", ["x", "y"]) - >>> C1(1, 2) - C1(x=1, y=2) - >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42), - ... "y": attr.ib(default=attr.Factory(list))}) - >>> C2() - C2(x=42, y=[]) - - -.. autoclass:: attr.Factory - - For example: - - .. doctest:: +Exceptions +---------- - >>> @attr.s - ... class C(object): - ... x = attr.ib(default=attr.Factory(list)) - ... y = attr.ib(default=attr.Factory( - ... lambda self: set(self.x), - ... takes_self=True) - ... ) - >>> C() - C(x=[], y=set()) - >>> C([1, 2, 3]) - C(x=[1, 2, 3], y={1, 2, 3}) +All exceptions are available from both ``attr.exceptions`` and ``attrs.exceptions`` and are the same thing. +That means that it doesn't matter from from which namespace they've been raised and/or caught: +.. doctest:: -Exceptions ----------- + >>> import attrs, attr + >>> try: + ... raise attrs.exceptions.FrozenError() + ... except attr.exceptions.FrozenError: + ... print("this works!") + this works! -.. autoexception:: attr.exceptions.PythonTooOldError -.. autoexception:: attr.exceptions.FrozenError -.. autoexception:: attr.exceptions.FrozenInstanceError -.. autoexception:: attr.exceptions.FrozenAttributeError -.. autoexception:: attr.exceptions.AttrsAttributeNotFoundError -.. autoexception:: attr.exceptions.NotAnAttrsClassError -.. autoexception:: attr.exceptions.DefaultAlreadySetError -.. autoexception:: attr.exceptions.UnannotatedAttributeError -.. autoexception:: attr.exceptions.NotCallableError +.. autoexception:: attrs.exceptions.PythonTooOldError +.. autoexception:: attrs.exceptions.FrozenError +.. autoexception:: attrs.exceptions.FrozenInstanceError +.. autoexception:: attrs.exceptions.FrozenAttributeError +.. autoexception:: attrs.exceptions.AttrsAttributeNotFoundError +.. autoexception:: attrs.exceptions.NotAnAttrsClassError +.. autoexception:: attrs.exceptions.DefaultAlreadySetError +.. autoexception:: attrs.exceptions.UnannotatedAttributeError +.. autoexception:: attrs.exceptions.NotCallableError For example:: @@ -172,9 +230,12 @@ Helpers ``attrs`` comes with a bunch of helper methods that make working with it easier: -.. autofunction:: attr.cmp_using +.. autofunction:: attrs.cmp_using +.. function:: attr.cmp_using + + Same as `attrs.cmp_using`. -.. autofunction:: attr.fields +.. autofunction:: attrs.fields For example: @@ -184,14 +245,18 @@ Helpers ... class C(object): ... x = attr.ib() ... y = attr.ib() - >>> attr.fields(C) + >>> attrs.fields(C) (Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)) - >>> attr.fields(C)[1] + >>> attrs.fields(C)[1] Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - >>> attr.fields(C).y is attr.fields(C)[1] + >>> attrs.fields(C).y is attrs.fields(C)[1] True -.. autofunction:: attr.fields_dict +.. function:: attr.fields + + Same as `attrs.fields`. + +.. autofunction:: attrs.fields_dict For example: @@ -201,15 +266,18 @@ Helpers ... class C(object): ... x = attr.ib() ... y = attr.ib() - >>> attr.fields_dict(C) + >>> attrs.fields_dict(C) {'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)} >>> attr.fields_dict(C)['y'] Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - >>> attr.fields_dict(C)['y'] is attr.fields(C).y + >>> attrs.fields_dict(C)['y'] is attrs.fields(C).y True +.. function:: attr.fields_dict + + Same as `attrs.fields_dict`. -.. autofunction:: attr.has +.. autofunction:: attrs.has For example: @@ -223,83 +291,106 @@ Helpers >>> attr.has(object) False +.. function:: attr.has + + Same as `attrs.has`. -.. autofunction:: attr.resolve_types +.. autofunction:: attrs.resolve_types For example: .. doctest:: >>> import typing - >>> @attr.s(auto_attribs=True) + >>> @attrs.define ... class A: ... a: typing.List['A'] ... b: 'B' ... - >>> @attr.s(auto_attribs=True) + >>> @attrs.define ... class B: ... a: A ... - >>> attr.fields(A).a.type + >>> attrs.fields(A).a.type typing.List[ForwardRef('A')] - >>> attr.fields(A).b.type + >>> attrs.fields(A).b.type 'B' - >>> attr.resolve_types(A, globals(), locals()) + >>> attrs.resolve_types(A, globals(), locals()) - >>> attr.fields(A).a.type + >>> attrs.fields(A).a.type typing.List[A] - >>> attr.fields(A).b.type + >>> attrs.fields(A).b.type -.. autofunction:: attr.asdict +.. function:: attr.resolve_types + + Same as `attrs.resolve_types`. + +.. autofunction:: attrs.asdict For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - >>> attr.asdict(C(1, C(2, 3))) + >>> @attrs.define + ... class C: + ... x: int + ... y: int + >>> attrs.asdict(C(1, C(2, 3))) {'x': 1, 'y': {'x': 2, 'y': 3}} +.. autofunction:: attr.asdict -.. autofunction:: attr.astuple +.. autofunction:: attrs.astuple For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - >>> attr.astuple(C(1,2)) + >>> @attrs.define + ... class C: + ... x = attr.field() + ... y = attr.field() + >>> attrs.astuple(C(1,2)) (1, 2) -``attrs`` includes some handy helpers for filtering the attributes in `attr.asdict` and `attr.astuple`: +.. autofunction:: attr.astuple + + +``attrs`` includes some handy helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`: + +.. autofunction:: attrs.filters.include + +.. autofunction:: attrs.filters.exclude + +.. function:: attr.filters.include + + Same as `attrs.filters.include`. + +.. function:: attr.filters.exclude -.. autofunction:: attr.filters.include + Same as `attrs.filters.exclude`. -.. autofunction:: attr.filters.exclude +See :func:`attrs.asdict` for examples. -See :func:`asdict` for examples. +All objects from ``attrs.filters`` are also available from ``attr.filters``. -.. autofunction:: attr.evolve +---- + +.. autofunction:: attrs.evolve For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() + >>> @attrs.define + ... class C: + ... x: int + ... y: int >>> i1 = C(1, 2) >>> i1 C(x=1, y=2) - >>> i2 = attr.evolve(i1, y=3) + >>> i2 = attrs.evolve(i1, y=3) >>> i2 C(x=1, y=3) >>> i1 == i2 @@ -312,22 +403,30 @@ See :func:`asdict` for examples. * attributes with ``init=False`` can't be set with ``evolve``. * the usual ``__init__`` validators will validate the new values. -.. autofunction:: validate +.. function:: attr.evolve + + Same as `attrs.evolve`. + +.. autofunction:: attrs.validate For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @attrs.define(on_setattr=attrs.setters.NO_OP) + ... class C: + ... x = attrs.field(validator=attrs.validators.instance_of(int)) >>> i = C(1) >>> i.x = "1" - >>> attr.validate(i) + >>> attrs.validate(i) Traceback (most recent call last): ... TypeError: ("'x' must be (got '1' that is a ).", ...) +.. function:: attr.validate + + Same as `attrs.validate`. + Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact: @@ -341,18 +440,19 @@ Validators can be globally disabled if you want to run them only in development Validators ---------- -``attrs`` comes with some common validators in the ``attrs.validators`` module: +``attrs`` comes with some common validators in the ``attrs.validators`` module. +All objects from ``attrs.converters`` are also available from ``attr.converters``. -.. autofunction:: attr.validators.lt +.. autofunction:: attrs.validators.lt For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.lt(42)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.lt(42)) >>> C(41) C(x=41) >>> C(42) @@ -360,15 +460,15 @@ Validators ... ValueError: ("'x' must be < 42: 42") -.. autofunction:: attr.validators.le +.. autofunction:: attrs.validators.le For example: .. doctest:: - >>> @attr.s + >>> @attrs.define ... class C(object): - ... x = attr.ib(validator=attr.validators.le(42)) + ... x = attrs.field(validator=attr.validators.le(42)) >>> C(42) C(x=42) >>> C(43) @@ -376,15 +476,15 @@ Validators ... ValueError: ("'x' must be <= 42: 43") -.. autofunction:: attr.validators.ge +.. autofunction:: attrs.validators.ge For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.ge(42)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.ge(42)) >>> C(42) C(x=42) >>> C(41) @@ -392,15 +492,15 @@ Validators ... ValueError: ("'x' must be => 42: 41") -.. autofunction:: attr.validators.gt +.. autofunction:: attrs.validators.gt For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.gt(42)) + >>> @attrs.define + ... class C: + ... x = attr.field(validator=attrs.validators.gt(42)) >>> C(43) C(x=43) >>> C(42) @@ -408,15 +508,15 @@ Validators ... ValueError: ("'x' must be > 42: 42") -.. autofunction:: attr.validators.max_len +.. autofunction:: attrs.validators.max_len For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.max_len(4)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.max_len(4)) >>> C("spam") C(x='spam') >>> C("bacon") @@ -424,16 +524,15 @@ Validators ... ValueError: ("Length of 'x' must be <= 4: 5") -.. autofunction:: attr.validators.instance_of - +.. autofunction:: attrs.validators.instance_of For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -445,7 +544,7 @@ Validators ... TypeError: ("'x' must be (got None that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), , None) -.. autofunction:: attr.validators.in_ +.. autofunction:: attrs.validators.in_ For example: @@ -455,10 +554,10 @@ Validators >>> class State(enum.Enum): ... ON = "on" ... OFF = "off" - >>> @attr.s - ... class C(object): - ... state = attr.ib(validator=attr.validators.in_(State)) - ... val = attr.ib(validator=attr.validators.in_([1, 2, 3])) + >>> @attrs.define + ... class C: + ... state = attrs.field(validator=attrs.validators.in_(State)) + ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3])) >>> C(State.ON, 1) C(state=, val=1) >>> C("on", 1) @@ -470,26 +569,26 @@ Validators ... ValueError: 'val' must be in [1, 2, 3] (got 4) -.. autofunction:: attr.validators.provides +.. autofunction:: attrs.validators.provides -.. autofunction:: attr.validators.and_ +.. autofunction:: attrs.validators.and_ - For convenience, it's also possible to pass a list to `attr.ib`'s validator argument. + For convenience, it's also possible to pass a list to `attrs.field`'s validator argument. Thus the following two statements are equivalent:: - x = attr.ib(validator=attr.validators.and_(v1, v2, v3)) - x = attr.ib(validator=[v1, v2, v3]) + x = attrs.field(validator=attrs.validators.and_(v1, v2, v3)) + x = attrs.field(validator=[v1, v2, v3]) -.. autofunction:: attr.validators.optional +.. autofunction:: attrs.validators.optional For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(int))) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.optional(attr.validators.instance_of(int))) >>> C(42) C(x=42) >>> C("42") @@ -500,15 +599,15 @@ Validators C(x=None) -.. autofunction:: attr.validators.is_callable +.. autofunction:: attrs.validators.is_callable For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.is_callable()) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.is_callable()) >>> C(isinstance) C(x=) >>> C("not a callable") @@ -517,15 +616,15 @@ Validators attr.exceptions.NotCallableError: 'x' must be callable (got 'not a callable' that is a ). -.. autofunction:: attr.validators.matches_re +.. autofunction:: attrs.validators.matches_re For example: .. doctest:: - >>> @attr.s - ... class User(object): - ... email = attr.ib(validator=attr.validators.matches_re( + >>> @attrs.define + ... class User: + ... email = attrs.field(validator=attrs.validators.matches_re( ... "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")) >>> User(email="user@example.com") User(email='user@example.com') @@ -535,17 +634,17 @@ Validators ValueError: ("'email' must match regex '(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+$)' ('user@example.com@test.com' doesn't)", Attribute(name='email', default=NOTHING, validator=, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)'), 'user@example.com@test.com') -.. autofunction:: attr.validators.deep_iterable +.. autofunction:: attrs.validators.deep_iterable For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.deep_iterable( - ... member_validator=attr.validators.instance_of(int), - ... iterable_validator=attr.validators.instance_of(list) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.deep_iterable( + ... member_validator=attrs.validators.instance_of(int), + ... iterable_validator=attrs.validators.instance_of(list) ... )) >>> C(x=[1, 2, 3]) C(x=[1, 2, 3]) @@ -559,18 +658,18 @@ Validators TypeError: ("'x' must be (got '3' that is a ).", Attribute(name='x', default=NOTHING, validator=> iterables of >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), , '3') -.. autofunction:: attr.validators.deep_mapping +.. autofunction:: attrs.validators.deep_mapping For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.deep_mapping( - ... key_validator=attr.validators.instance_of(str), - ... value_validator=attr.validators.instance_of(int), - ... mapping_validator=attr.validators.instance_of(dict) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.deep_mapping( + ... key_validator=attrs.validators.instance_of(str), + ... value_validator=attrs.validators.instance_of(int), + ... mapping_validator=attrs.validators.instance_of(dict) ... )) >>> C(x={"a": 1, "b": 2}) C(x={'a': 1, 'b': 2}) @@ -589,17 +688,19 @@ Validators Validators can be both globally and locally disabled: -.. autofunction:: attr.validators.set_disabled +.. autofunction:: attrs.validators.set_disabled -.. autofunction:: attr.validators.get_disabled +.. autofunction:: attrs.validators.get_disabled -.. autofunction:: attr.validators.disabled +.. autofunction:: attrs.validators.disabled Converters ---------- -.. autofunction:: attr.converters.pipe +All objects from ``attrs.converters`` are also available from ``attr.converters``. + +.. autofunction:: attrs.converters.pipe For convenience, it's also possible to pass a list to `attr.ib`'s converter argument. @@ -608,7 +709,7 @@ Converters x = attr.ib(converter=attr.converter.pipe(c1, c2, c3)) x = attr.ib(converter=[c1, c2, c3]) -.. autofunction:: attr.converters.optional +.. autofunction:: attrs.converters.optional For example: @@ -623,7 +724,7 @@ Converters C(x=42) -.. autofunction:: attr.converters.default_if_none +.. autofunction:: attrs.converters.default_if_none For example: @@ -638,7 +739,7 @@ Converters C(x='') -.. autofunction:: attr.converters.to_bool +.. autofunction:: attrs.converters.to_bool For example: @@ -665,22 +766,23 @@ Converters Setters ------- -These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on_setattr`` arguments. +These are helpers that you can use together with `attrs.define`'s and `attrs.fields`'s ``on_setattr`` arguments. +All setters in ``attrs.setters`` are also available from ``attr.setters``. -.. autofunction:: attr.setters.frozen -.. autofunction:: attr.setters.validate -.. autofunction:: attr.setters.convert -.. autofunction:: attr.setters.pipe -.. autodata:: attr.setters.NO_OP +.. autofunction:: attrs.setters.frozen +.. autofunction:: attrs.setters.validate +.. autofunction:: attrs.setters.convert +.. autofunction:: attrs.setters.pipe +.. autodata:: attrs.setters.NO_OP For example, only ``x`` is frozen here: .. doctest:: - >>> @attr.s(on_setattr=attr.setters.frozen) - ... class C(object): - ... x = attr.ib() - ... y = attr.ib(on_setattr=attr.setters.NO_OP) + >>> @attrs.define(on_setattr=attr.setters.frozen) + ... class C: + ... x = attr.field() + ... y = attr.field(on_setattr=attr.setters.NO_OP) >>> c = C(1, 2) >>> c.y = 3 >>> c.y @@ -688,53 +790,9 @@ These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on >>> c.x = 4 Traceback (most recent call last): ... - attr.exceptions.FrozenAttributeError: () - - N.B. Please use `attr.s`'s *frozen* argument to freeze whole classes; it is more efficient. - - -.. _next-gen: - -Next Generation APIs --------------------- - -These are Python 3.6 and later-only, and keyword-only APIs that call `attr.s` with different default values. - -The most notable differences are: - -- automatically detect whether or not *auto_attribs* should be `True` -- *slots=True* (see :term:`slotted classes` for potentially surprising behaviors) -- *auto_exc=True* -- *auto_detect=True* -- *eq=True*, but *order=False* -- Converters and validators are run when you set an attribute (*on_setattr=[attr.setters.convert, attr.setters.validate*]). -- Some options that aren't relevant to Python 3 have been dropped. - -Please note that these are *defaults* and you're free to override them, just like before. - -Since the Python ecosystem has settled on the term ``field`` for defining attributes, we have also added `attr.field` as a substitute for `attr.ib`. - -.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. - -.. note:: - - `attr.s` and `attr.ib` (and their serious business cousins) aren't going anywhere. - The new APIs build on top of them. - -.. autofunction:: attr.define -.. function:: mutable(same_as_define) - - Alias for `attr.define`. - - .. versionadded:: 20.1.0 - -.. function:: frozen(same_as_define) - - Behaves the same as `attr.define` but sets *frozen=True* and *on_setattr=None*. - - .. versionadded:: 20.1.0 + attrs.exceptions.FrozenAttributeError: () -.. autofunction:: attr.field + N.B. Please use `attrs.define`'s *frozen* argument (or `attrs.frozen`) to freeze whole classes; it is more efficient. Deprecated APIs diff --git a/docs/comparison.rst b/docs/comparison.rst index 87a47d2f1..760124ca3 100644 --- a/docs/comparison.rst +++ b/docs/comparison.rst @@ -62,5 +62,5 @@ For NumPy arrays it would look like this:: .. warning:: - Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in `modern APIs `. + Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in `attrs.define` (but not in `attr.s`). You can set both at once by using the *cmp* argument that we've undeprecated just for this use-case. diff --git a/docs/examples.rst b/docs/examples.rst index 075f4e0c6..fd6feb549 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -419,7 +419,7 @@ Therefore if you use ``@default``, it is *not* enough to annotate said attribute ... TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None, kw_only=False), , '42') -Please note that if you use `attr.s` (and not `define`) to define your class, validators only run on initialization by default. +Please note that if you use `attr.s` (and not `attrs.define`) to define your class, validators only run on initialization by default. This behavior can be changed using the ``on_setattr`` argument. Check out `validators` for more details. @@ -492,7 +492,7 @@ Types >>> fields(C).x.type -If you don't mind annotating *all* attributes, you can even drop the `field` and assign default values instead: +If you don't mind annotating *all* attributes, you can even drop the `attrs.field` and assign default values instead: .. doctest:: @@ -521,8 +521,8 @@ If you don't mind annotating *all* attributes, you can even drop the `field` and The generated ``__init__`` method will have an attribute called ``__annotations__`` that contains this type information. -If your annotations contain forward references, -you can resolve these after all references have been defined by using :func:`attr.resolve_types`. +If your annotations contain strings (e.g. forward references), +you can resolve these after all references have been defined by using :func:`attrs.resolve_types`. This will replace the *type* attribute in the respective fields. .. doctest:: @@ -564,7 +564,7 @@ Slots ----- :term:`Slotted classes ` have several advantages on CPython. -Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `define` or passing ``slots=True`` to `attr.s`: +Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `attrs.define` or passing ``slots=True`` to `attr.s`: .. doctest:: @@ -624,11 +624,11 @@ Other Goodies ------------- Sometimes you may want to create a class programmatically. -``attrs`` won't let you down and gives you `attr.make_class` : +``attrs`` won't let you down and gives you `attrs.make_class` : .. doctest:: - >>> from attr import fields, make_class + >>> from attrs import fields, make_class >>> @define ... class C1: ... x = field() @@ -654,7 +654,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` >>> i.y [] -If you need to dynamically make a class with `attr.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: +If you need to dynamically make a class with `attrs.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: .. doctest:: diff --git a/docs/extending.rst b/docs/extending.rst index d229f1595..57eaee94e 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -2,7 +2,7 @@ Extending ========= Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. -It is a tuple of `attr.Attribute` carrying meta-data about each attribute. +It is a tuple of `attrs.Attribute` carrying meta-data about each attribute. So it is fairly simple to build your own decorators on top of ``attrs``: @@ -21,7 +21,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``: .. warning:: - The `define`/`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! + The `attrs.define`/`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! That means that is has to come *after* your decorator because:: @a @@ -205,13 +205,13 @@ Its main purpose is to automatically add converters to attributes based on their This hook must have the following signature: -.. function:: your_hook(cls: type, fields: list[attr.Attribute]) -> list[attr.Attribute] +.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute] :noindex: - *cls* is your class right *before* it is being converted into an attrs class. This means it does not yet have the ``__attrs_attrs__`` attribute. -- *fields* is a list of all :class:`attr.Attribute` instances that will later be set to ``__attrs_attrs__``. +- *fields* is a list of all `attrs.Attribute` instances that will later be set to ``__attrs_attrs__``. You can modify these attributes any way you want: You can add converters, change types, and even remove attributes completely or create new ones! @@ -288,7 +288,7 @@ However, the result can not always be serialized since most data types will rema To help you with this, `attr.asdict` allows you to pass a *value_serializer* hook. It has the signature -.. function:: your_hook(inst: type, field: attr.Attribute, value: typing.Any) -> typing.Any +.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any :noindex: .. doctest:: diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index 08367cbfd..f89974054 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -10,8 +10,9 @@ Boilerplate ``attrs`` certainly isn't the first library that aims to simplify class definition in Python. But its **declarative** approach combined with **no runtime overhead** lets it stand out. -Once you apply the ``@attr.s`` decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. +Once you apply the ``@attrs.define`` (or ``@attr.s``) decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. Internally they're a representation of the data passed into ``attr.ib`` along with a counter to preserve the order of the attributes. +Alternatively, it's possible to define them using :doc:`types`. In order to ensure that subclassing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all base classes. Please note that ``attrs`` does *not* call ``super()`` *ever*. @@ -41,7 +42,7 @@ No magic, no meta programming, no expensive introspection at runtime. Everything until this point happens exactly *once* when the class is defined. As soon as a class is done, it's done. And it's just a regular Python class like any other, except for a single ``__attrs_attrs__`` attribute that ``attrs`` uses internally. -Much of the information is accessible via `attr.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like `attr.asdict`). +Much of the information is accessible via `attrs.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like `attrs.asdict`). And once you start instantiating your classes, ``attrs`` is out of your way completely. @@ -53,11 +54,11 @@ This **static** approach was very much a design goal of ``attrs`` and what I str Immutability ------------ -In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attr.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. +In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attrs.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. -The same is true if you choose to freeze individual attributes using the `attr.setters.frozen` *on_setattr* hook -- except that the exception becomes `attr.exceptions.FrozenAttributeError`. +The same is true if you choose to freeze individual attributes using the `attrs.setters.frozen` *on_setattr* hook -- except that the exception becomes `attrs.exceptions.FrozenAttributeError`. -Both errors subclass `attr.exceptions.FrozenError`. +Both errors subclass `attrs.exceptions.FrozenError`. ----- diff --git a/docs/init.rst b/docs/init.rst index 4b2697898..fb276ded8 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -51,7 +51,7 @@ One thing people tend to find confusing is the treatment of private attributes t .. doctest:: - >>> import inspect, attr + >>> import inspect, attr, attrs >>> from attr import define >>> @define ... class C: @@ -162,13 +162,13 @@ If the value does not pass the validator's standards, it just raises an appropri ... ValueError: x must be smaller or equal to 42 -Again, it's important that the decorated method doesn't have the same name as the attribute and that the `field()` helper is used. +Again, it's important that the decorated method doesn't have the same name as the attribute and that the `attrs.field()` helper is used. Callables ~~~~~~~~~ -If you want to re-use your validators, you should have a look at the ``validator`` argument to `field`. +If you want to re-use your validators, you should have a look at the ``validator`` argument to `attrs.field`. It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach. @@ -181,7 +181,7 @@ Since the validators run *after* the instance is initialized, you can refer to o ... raise ValueError("'x' has to be smaller than 'y'!") >>> @define ... class C: - ... x = field(validator=[attr.validators.instance_of(int), + ... x = field(validator=[attrs.validators.instance_of(int), ... x_smaller_than_y]) ... y = field() >>> C(x=3, y=4) @@ -191,10 +191,10 @@ Since the validators run *after* the instance is initialized, you can refer to o ... ValueError: 'x' has to be smaller than 'y'! -This example also shows of some syntactic sugar for using the `attr.validators.and_` validator: if you pass a list, all validators have to pass. +This example also shows of some syntactic sugar for using the `attrs.validators.and_` validator: if you pass a list, all validators have to pass. -``attrs`` won't intercept your changes to those attributes but you can always call `attr.validate` on any instance to verify that it's still valid: -When using `define` or :func:`~attr.frozen`, ``attrs`` will run the validators even when setting the attribute. +``attrs`` won't intercept your changes to those attributes but you can always call `attrs.validate` on any instance to verify that it's still valid: +When using `attrs.define` or `attrs.frozen`, ``attrs`` will run the validators even when setting the attribute. .. doctest:: @@ -210,7 +210,7 @@ When using `define` or :func:`~attr.frozen`, ``attrs`` will run the validators e >>> @define ... class C: - ... x = field(validator=attr.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -225,7 +225,7 @@ If you define validators both ways for an attribute, they are both ran: >>> @define ... class C: - ... x = field(validator=attr.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) ... @x.validator ... def fits_byte(self, attribute, value): ... if not 0 <= value < 256: @@ -243,10 +243,10 @@ If you define validators both ways for an attribute, they are both ran: And finally you can disable validators globally: - >>> attr.validators.set_disabled(True) + >>> attrs.validators.set_disabled(True) >>> C("128") C(x='128') - >>> attr.validators.set_disabled(False) + >>> attrs.validators.set_disabled(False) >>> C("128") Traceback (most recent call last): ... @@ -254,7 +254,7 @@ And finally you can disable validators globally: You can achieve the same by using the context manager: - >>> with attr.validators.disabled(): + >>> with attrs.validators.disabled(): ... C("128") C(x='128') >>> C("128") @@ -408,7 +408,7 @@ Please note that you can't directly set attributes on frozen classes: >>> FrozenBroken(1) Traceback (most recent call last): ... - attr.exceptions.FrozenInstanceError: can't set attribute + attrs.exceptions.FrozenInstanceError: can't set attribute If you need to set attributes on a frozen class, you'll have to resort to the `same trick ` as ``attrs`` and use :meth:`object.__setattr__`: diff --git a/docs/names.rst b/docs/names.rst index abfdba480..addd4ed16 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -1,7 +1,7 @@ On The Core API Names ===================== -You may be surprised seeing ``attrs`` classes being created using `attr.define` and with type annotated fields, instead of `attr.s` and `attr.ib()`. +You may be surprised seeing ``attrs`` classes being created using `attrs.define` and with type annotated fields, instead of `attr.s` and `attr.ib()`. Or, you wonder why the web and talks are full of this weird `attr.s` and `attr.ib` -- including people having strong opinions about it and using ``attr.attrs`` and ``attr.attrib`` instead. @@ -13,14 +13,15 @@ TL;DR We recommend our modern APIs for new code: -- `define()` to define a new class, -- `mutable()` is an alias for `define()`, -- :func:`~attr.frozen` is an alias for ``define(frozen=True)`` -- and `field()` to define an attribute. +- `attrs.define()` to define a new class, +- `attrs.mutable()` is an alias for `attrs.define()`, +- `attrs.frozen()` is an alias for ``define(frozen=True)`` +- and `attrs.field()` to define an attribute. They have been added in ``attrs`` 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. They are only available in Python 3.6 and later. Sometimes they're referred to as *next-generation* or *NG* APIs. +As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. The traditional APIs `attr.s` / `attr.ib`, their serious business aliases ``attr.attrs`` / ``attr.attrib``, and the never-documented, but popular ``attr.dataclass`` easter egg will stay **forever**. @@ -48,7 +49,7 @@ But it was really just a way to say ``attrs`` and ``attrib``\ [#attr]_. Some people hated this cutey API from day one, which is why we added aliases for them that we called *serious business*: ``@attr.attrs`` and ``attr.attrib()``. Fans of them usually imported the names and didn't use the package name in the first place. -Unfortunately, the ``attr`` package name started creaking the moment we added `attr.Factory`, since it couldn’t be morphed into something meaningful in any way. +Unfortunately, the ``attr`` package name started creaking the moment we added ``attr.Factory``, since it couldn’t be morphed into something meaningful in any way. A problem that grew worse over time, as more APIs and even modules were added. But overall, ``attrs`` in this shape was a **huge** success -- especially after glyph's blog post `The One Python Library Everyone Needs `_ in August 2016 and `pytest `_ adopting it. @@ -96,7 +97,7 @@ We've spent years alone explaining that defining attributes using type annotatio Finally we've decided to take the `Go route `_: instead of fiddling with the old APIs -- whose names felt anachronistic anyway -- we'd define new ones, with better defaults. -So in July 2018, we `looked for better names `_ and came up with `define`, `field`, and friends. +So in July 2018, we `looked for better names `_ and came up with `attr.define`, `attr.field`, and friends. Then in January 2019, we `started looking for inconvenient defaults `_ that we now could fix without any repercussions. These APIs proved to be vastly popular, so we've finally changed the documentation to them in November of 2021. @@ -104,8 +105,12 @@ These APIs proved to be vastly popular, so we've finally changed the documentati All of this took way too long, of course. One reason is the COVID-19 pandemic, but also our fear to fumble this historic chance to fix our APIs. +Finally, in December 2021, we've added the ``attrs`` package namespace. + We hope you like the result:: + from attrs import define + @define class Point: x: int diff --git a/docs/types.rst b/docs/types.rst index 5a71c393e..a05d35f2a 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -78,7 +78,7 @@ pyright ``attrs`` provides support for pyright_ though the dataclass_transform_ specification. This provides static type inference for a subset of ``attrs`` equivalent to standard-library ``dataclasses``, -and requires explicit type annotations using the :ref:`next-gen` or ``@attr.s(auto_attribs=True)`` API. +and requires explicit type annotations using the `attrs.define` or ``@attr.s(auto_attribs=True)`` API. Given the following definition, ``pyright`` will generate static type signatures for ``SomeClass`` attribute access, ``__init__``, ``__eq__``, and comparison methods:: diff --git a/pyproject.toml b/pyproject.toml index 93145c9e3..b34ed515a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,10 @@ fail-under = 100 whitelist-regex = ["test_.*"] +[tool.check-wheel-contents] +toplevel = ["attr", "attrs"] + + [tool.isort] profile = "attrs" diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 2af76b7a8..c0a212650 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -447,6 +447,7 @@ def make_class( # these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! def asdict( inst: Any, recurse: bool = ..., diff --git a/src/attr/_config.py b/src/attr/_config.py index 6503f6fb0..546b43870 100644 --- a/src/attr/_config.py +++ b/src/attr/_config.py @@ -11,7 +11,7 @@ def set_run_validators(run): Set whether or not validators are run. By default, they are run. .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attr.validators.set_disabled()` + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` instead. """ if not isinstance(run, bool): @@ -25,7 +25,7 @@ def get_run_validators(): Return whether or not validators are run. .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attr.validators.get_disabled()` + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` instead. """ return _run_validators diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 6ea2de0a0..2f5fae92b 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -25,7 +25,7 @@ def asdict( ``attrs``-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the `attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable dict_factory: A callable to produce dictionaries from. For example, to produce ordered dictionaries instead of normal Python @@ -204,7 +204,7 @@ def astuple( ``attrs``-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the `attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable tuple_factory: A callable to produce tuples from. For example, to produce lists instead of tuples. @@ -314,7 +314,9 @@ def assoc(inst, **changes): class. .. deprecated:: 17.1.0 - Use `evolve` instead. + Use `attrs.evolve` instead if you can. + This function will not be removed du to the slightly different approach + compared to `attrs.evolve`. """ import warnings @@ -393,8 +395,8 @@ class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. :returns: *cls* so you can use this function also as a class decorator. - Please note that you have to apply it **after** `attr.s`. That means - the decorator has to come in the line **before** `attr.s`. + Please note that you have to apply it **after** `attrs.define`. That + means the decorator has to come in the line **before** `attrs.define`. .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* diff --git a/src/attr/_make.py b/src/attr/_make.py index 990786954..4b0d667d3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -147,11 +147,11 @@ def attrib( is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. - If the value is an instance of `Factory`, its callable will be + If the value is an instance of `attrs.Factory`, its callable will be used to construct a new value (useful for mutable data types like lists or dicts). - If a default is not set (or set manually to `attr.NOTHING`), a value + If a default is not set (or set manually to `attrs.NOTHING`), a value *must* be supplied when instantiating; otherwise a `TypeError` will be raised. @@ -164,7 +164,7 @@ def attrib( :param validator: `callable` that is called by ``attrs``-generated ``__init__`` methods after the instance has been initialized. They - receive the initialized instance, the `Attribute`, and the + receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. The return value is *not* inspected so the validator has to throw an @@ -237,10 +237,10 @@ def attrib( parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. - Set to `attr.setters.NO_OP` to run **no** `setattr` hooks for this + Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. :type on_setattr: `callable`, or a list of callables, or `None`, or - `attr.setters.NO_OP` + `attrs.setters.NO_OP` .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -1286,7 +1286,7 @@ def attrs( *cmp*, or *hash* overrides whatever *auto_detect* would determine. *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - a `PythonTooOldError`. + an `attrs.exceptions.PythonTooOldError`. :param bool repr: Create a ``__repr__`` method with a human readable representation of ``attrs`` attributes.. @@ -1373,7 +1373,7 @@ def attrs( If you assign a value to those attributes (e.g. ``x: int = 42``), that value becomes the default value like if it were passed using - ``attr.ib(default=42)``. Passing an instance of `Factory` also + ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also works as expected in most cases (see warning below). Attributes annotated as `typing.ClassVar`, and attributes that are @@ -1445,7 +1445,7 @@ def attrs( the callable. If a list of callables is passed, they're automatically wrapped in an - `attr.setters.pipe`. + `attrs.setters.pipe`. :param Optional[callable] field_transformer: A function that is called with the original class object and all @@ -2037,7 +2037,7 @@ def fields(cls): :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` class. - :rtype: tuple (with name accessors) of `attr.Attribute` + :rtype: tuple (with name accessors) of `attrs.Attribute` .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields by name. @@ -2064,7 +2064,7 @@ def fields_dict(cls): class. :rtype: an ordered dict where keys are attribute names and values are - `attr.Attribute`\\ s. This will be a `dict` if it's + `attrs.Attribute`\\ s. This will be a `dict` if it's naturally ordered like on Python 3.6+ or an :class:`~collections.OrderedDict` otherwise. @@ -2951,7 +2951,7 @@ class Factory(object): """ Stores a factory callable. - If passed as the default value to `attr.ib`, the factory is used to + If passed as the default value to `attrs.field`, the factory is used to generate a new value. :param callable factory: A callable that takes either none or exactly one diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 843447173..27adb0f52 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -3,11 +3,12 @@ `attr.ib` with different default values. """ -from functools import partial -from attr.exceptions import UnannotatedAttributeError +from functools import partial from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple from ._make import ( NOTHING, _frozen_setattrs, @@ -15,6 +16,7 @@ attrib, attrs, ) +from .exceptions import UnannotatedAttributeError def define( @@ -43,8 +45,23 @@ def define( r""" Define an ``attrs`` class. - The behavioral differences to `attr.s` are the handling of the - *auto_attribs* option: + Differences to the classic `attr.s` that it uses underneath: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - If *frozen* is `False`, run converters and validators when setting an + attribute by default. + - *slots=True* (see :term:`slotted classes` for potentially surprising + behaviors) + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *match_args=True* + - Some options that were only relevant on Python 2 or were kept around for + backwards-compatibility have been removed. + + Please note that these are all defaults and you can change them as you + wish. :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves exactly like `attr.s`. If left `None`, `attr.s` will try to guess: @@ -54,8 +71,7 @@ def define( 2. Otherwise it assumes *auto_attribs=False* and tries to collect `attr.ib`\ s. - and that mutable classes (``frozen=False``) convert and validate on - ``__setattr__``. + For now, please refer to `attr.s` for the rest of the parameters. .. versionadded:: 20.1.0 .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. @@ -168,3 +184,31 @@ def field( order=order, on_setattr=on_setattr, ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) diff --git a/src/attr/converters.py b/src/attr/converters.py index 366b8728a..1dd341e44 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -14,9 +14,10 @@ __all__ = [ - "pipe", - "optional", "default_if_none", + "optional", + "pipe", + "to_bool", ] @@ -65,14 +66,14 @@ def default_if_none(default=NOTHING, factory=None): result of *factory*. :param default: Value to be used if ``None`` is passed. Passing an instance - of `attr.Factory` is supported, however the ``takes_self`` option + of `attrs.Factory` is supported, however the ``takes_self`` option is *not*. :param callable factory: A callable that takes no parameters whose result is used if ``None`` is passed. :raises TypeError: If **neither** *default* or *factory* is passed. :raises TypeError: If **both** *default* and *factory* are passed. - :raises ValueError: If an instance of `attr.Factory` is passed with + :raises ValueError: If an instance of `attrs.Factory` is passed with ``takes_self=True``. .. versionadded:: 18.2.0 diff --git a/src/attr/filters.py b/src/attr/filters.py index ae5248568..5c88280e5 100644 --- a/src/attr/filters.py +++ b/src/attr/filters.py @@ -23,7 +23,7 @@ def include(*what): Include *what*. :param what: What to include. - :type what: `list` of `type` or `attr.Attribute`\\ s + :type what: `list` of `type` or `attrs.Attribute`\\ s :rtype: `callable` """ @@ -40,7 +40,7 @@ def exclude(*what): Exclude *what*. :param what: What to exclude. - :type what: `list` of classes or `attr.Attribute`\\ s. + :type what: `list` of classes or `attrs.Attribute`\\ s. :rtype: `callable` """ diff --git a/src/attr/validators.py b/src/attr/validators.py index 3896d8346..62fcc7e1d 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -127,7 +127,7 @@ def instance_of(type): :type type: type or tuple of types :raises TypeError: With a human readable error message, the attribute - (of type `attr.Attribute`), the expected type, and the value it + (of type `attrs.Attribute`), the expected type, and the value it got. """ return _InstanceOfValidator(type) @@ -250,7 +250,7 @@ def provides(interface): :type interface: ``zope.interface.Interface`` :raises TypeError: With a human readable error message, the attribute - (of type `attr.Attribute`), the expected interface, and the + (of type `attrs.Attribute`), the expected interface, and the value it got. """ return _ProvidesValidator(interface) @@ -323,7 +323,7 @@ def in_(options): :type options: list, tuple, `enum.Enum`, ... :raises ValueError: With a human readable error message, the attribute (of - type `attr.Attribute`), the expected options, and the value it + type `attrs.Attribute`), the expected options, and the value it got. .. versionadded:: 17.1.0 @@ -362,7 +362,7 @@ def is_callable(): .. versionadded:: 19.1.0 :raises `attr.exceptions.NotCallableError`: With a human readable error - message containing the attribute (`attr.Attribute`) name, + message containing the attribute (`attrs.Attribute`) name, and the value it got. """ return _IsCallableValidator() diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py new file mode 100644 index 000000000..7c8c11f04 --- /dev/null +++ b/src/attrs/__init__.py @@ -0,0 +1,68 @@ +from attr import ( + NOTHING, + Attribute, + Factory, + __author__, + __copyright__, + __description__, + __doc__, + __email__, + __license__, + __title__, + __url__, + __version__, + __version_info__, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._next_gen import asdict, astuple + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "Attribute", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "Factory", + "field", + "fields_dict", + "fields", + "filters", + "frozen", + "has", + "make_class", + "mutable", + "NOTHING", + "resolve_types", + "setters", + "validate", + "validators", +] diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi new file mode 100644 index 000000000..7426fa5dd --- /dev/null +++ b/src/attrs/__init__.pyi @@ -0,0 +1,63 @@ +from typing import ( + Any, + Callable, + Dict, + Mapping, + Optional, + Sequence, + Tuple, + Type, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import _FilterType +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import define as define +from attr import evolve as evolve +from attr import Factory as Factory +from attr import exceptions as exceptions +from attr import field as field +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import frozen as frozen +from attr import has as has +from attr import make_class as make_class +from attr import mutable as mutable +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators + +# TODO: see definition of attr.asdict/astuple +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: bool = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... diff --git a/src/attrs/converters.py b/src/attrs/converters.py new file mode 100644 index 000000000..c2b3cfb26 --- /dev/null +++ b/src/attrs/converters.py @@ -0,0 +1 @@ +from attr.converters import * # noqa diff --git a/src/attrs/exceptions.py b/src/attrs/exceptions.py new file mode 100644 index 000000000..2b2bc3c04 --- /dev/null +++ b/src/attrs/exceptions.py @@ -0,0 +1 @@ +from attr.exceptions import * # noqa diff --git a/src/attrs/filters.py b/src/attrs/filters.py new file mode 100644 index 000000000..cb843cac5 --- /dev/null +++ b/src/attrs/filters.py @@ -0,0 +1 @@ +from attr.filters import * # noqa diff --git a/src/attrs/py.typed b/src/attrs/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/attrs/setters.py b/src/attrs/setters.py new file mode 100644 index 000000000..348aa3b1b --- /dev/null +++ b/src/attrs/setters.py @@ -0,0 +1 @@ +from attr.setters import * # noqa diff --git a/src/attrs/validators.py b/src/attrs/validators.py new file mode 100644 index 000000000..ad46fbb03 --- /dev/null +++ b/src/attrs/validators.py @@ -0,0 +1 @@ +from attr.validators import * # noqa diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index a2ed7fe67..7f5aff75c 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -8,10 +8,11 @@ import pytest -import attr +import attr as _attr # don't use it by accident +import attrs -@attr.define +@attrs.define class C: x: str y: int @@ -29,7 +30,7 @@ def test_no_slots(self): slots can be deactivated. """ - @attr.define(slots=False) + @attrs.define(slots=False) class NoSlots: x: int @@ -42,9 +43,9 @@ def test_validates(self): Validators at __init__ and __setattr__ work. """ - @attr.define + @attrs.define class Validated: - x: int = attr.field(validator=attr.validators.instance_of(int)) + x: int = attrs.field(validator=attrs.validators.instance_of(int)) v = Validated(1) @@ -61,7 +62,7 @@ def test_no_order(self): with pytest.raises(TypeError): C("1", 2) < C("2", 3) - @attr.define(order=True) + @attrs.define(order=True) class Ordered: x: int @@ -71,23 +72,23 @@ def test_override_auto_attribs_true(self): """ Don't guess if auto_attrib is set explicitly. - Having an unannotated attr.ib/attr.field fails. + Having an unannotated attrs.ib/attrs.field fails. """ - with pytest.raises(attr.exceptions.UnannotatedAttributeError): + with pytest.raises(attrs.exceptions.UnannotatedAttributeError): - @attr.define(auto_attribs=True) + @attrs.define(auto_attribs=True) class ThisFails: - x = attr.field() + x = attrs.field() y: int def test_override_auto_attribs_false(self): """ Don't guess if auto_attrib is set explicitly. - Annotated fields that don't carry an attr.ib are ignored. + Annotated fields that don't carry an attrs.ib are ignored. """ - @attr.define(auto_attribs=False) + @attrs.define(auto_attribs=False) class NoFields: x: int y: int @@ -99,16 +100,16 @@ def test_auto_attribs_detect(self): define correctly detects if a class lacks type annotations. """ - @attr.define + @attrs.define class OldSchool: - x = attr.field() + x = attrs.field() assert OldSchool(1) == OldSchool(1) # Test with maybe_cls = None - @attr.define() + @attrs.define() class OldSchool2: - x = attr.field() + x = attrs.field() assert OldSchool2(1) == OldSchool2(1) @@ -117,10 +118,10 @@ def test_auto_attribs_detect_fields_and_annotations(self): define infers auto_attribs=True if fields have type annotations """ - @attr.define + @attrs.define class NewSchool: x: int - y: list = attr.field() + y: list = attrs.field() @y.validator def _validate_y(self, attribute, value): @@ -130,14 +131,14 @@ def _validate_y(self, attribute, value): assert NewSchool(1, 1) == NewSchool(1, 1) with pytest.raises(ValueError): NewSchool(1, -1) - assert list(attr.fields_dict(NewSchool).keys()) == ["x", "y"] + assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"] def test_auto_attribs_partially_annotated(self): """ define infers auto_attribs=True if any type annotations are found """ - @attr.define + @attrs.define class NewSchool: x: int y: list @@ -145,7 +146,7 @@ class NewSchool: # fields are defined for any annotated attributes assert NewSchool(1, []) == NewSchool(1, []) - assert list(attr.fields_dict(NewSchool).keys()) == ["x", "y"] + assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"] # while the unannotated attributes are left as class vars assert NewSchool.z == 10 @@ -156,14 +157,14 @@ def test_auto_attribs_detect_annotations(self): define correctly detects if a class has type annotations. """ - @attr.define + @attrs.define class NewSchool: x: int assert NewSchool(1) == NewSchool(1) # Test with maybe_cls = None - @attr.define() + @attrs.define() class NewSchool2: x: int @@ -174,7 +175,7 @@ def test_exception(self): Exceptions are detected and correctly handled. """ - @attr.define + @attrs.define class E(Exception): msg: str other: int @@ -190,16 +191,16 @@ class E(Exception): def test_frozen(self): """ - attr.frozen freezes classes. + attrs.frozen freezes classes. """ - @attr.frozen + @attrs.frozen class F: x: str f = F(1) - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): f.x = 2 def test_auto_detect_eq(self): @@ -209,7 +210,7 @@ def test_auto_detect_eq(self): Regression test for #670. """ - @attr.define + @attrs.define class C: def __eq__(self, o): raise ValueError() @@ -219,35 +220,35 @@ def __eq__(self, o): def test_subclass_frozen(self): """ - It's possible to subclass an `attr.frozen` class and the frozen-ness is - inherited. + It's possible to subclass an `attrs.frozen` class and the frozen-ness + is inherited. """ - @attr.frozen + @attrs.frozen class A: a: int - @attr.frozen + @attrs.frozen class B(A): b: int - @attr.define(on_setattr=attr.setters.NO_OP) + @attrs.define(on_setattr=attrs.setters.NO_OP) class C(B): c: int assert B(1, 2) == B(1, 2) assert C(1, 2, 3) == C(1, 2, 3) - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): A(1).a = 1 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): B(1, 2).a = 1 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): B(1, 2).b = 2 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): C(1, 2, 3).c = 3 def test_catches_frozen_on_setattr(self): @@ -256,7 +257,7 @@ def test_catches_frozen_on_setattr(self): immutability is inherited. """ - @attr.define(frozen=True) + @attrs.define(frozen=True) class A: pass @@ -264,7 +265,7 @@ class A: ValueError, match="Frozen classes can't use on_setattr." ): - @attr.define(frozen=True, on_setattr=attr.setters.validate) + @attrs.define(frozen=True, on_setattr=attrs.setters.validate) class B: pass @@ -276,17 +277,17 @@ class B: ), ): - @attr.define(on_setattr=attr.setters.validate) + @attrs.define(on_setattr=attrs.setters.validate) class C(A): pass @pytest.mark.parametrize( "decorator", [ - partial(attr.s, frozen=True, slots=True, auto_exc=True), - attr.frozen, - attr.define, - attr.mutable, + partial(_attr.s, frozen=True, slots=True, auto_exc=True), + attrs.frozen, + attrs.define, + attrs.mutable, ], ) def test_discard_context(self, decorator): @@ -298,7 +299,7 @@ def test_discard_context(self, decorator): @decorator class MyException(Exception): - x: str = attr.ib() + x: str = attrs.field() with pytest.raises(MyException) as ei: try: @@ -314,9 +315,9 @@ def test_converts_and_validates_by_default(self): If no on_setattr is set, assume setters.convert, setters.validate. """ - @attr.define + @attrs.define class C: - x: int = attr.field(converter=int) + x: int = attrs.field(converter=int) @x.validator def _v(self, _, value): @@ -341,7 +342,7 @@ def test_mro_ng(self): See #428 """ - @attr.define + @attrs.define class A: x: int = 10 @@ -349,21 +350,89 @@ class A: def xx(self): return 10 - @attr.define + @attrs.define class B(A): y: int = 20 - @attr.define + @attrs.define class C(A): x: int = 50 def xx(self): return 50 - @attr.define + @attrs.define class D(B, C): pass d = D() assert d.x == d.xx() + + +class TestAsTuple: + def test_smoke(self): + """ + `attrs.astuple` only changes defaults, so we just call it and compare. + """ + inst = C("foo", 42) + + assert attrs.astuple(inst) == _attr.astuple(inst) + + +class TestAsDict: + def test_smoke(self): + """ + `attrs.asdict` only changes defaults, so we just call it and compare. + """ + inst = C("foo", {(1,): 42}) + + assert attrs.asdict(inst) == _attr.asdict( + inst, retain_collection_types=True + ) + + +class TestImports: + """ + Verify our re-imports and mirroring works. + """ + + def test_converters(self): + """ + Importing from attrs.converters works. + """ + from attrs.converters import optional + + assert optional is _attr.converters.optional + + def test_exceptions(self): + """ + Importing from attrs.exceptions works. + """ + from attrs.exceptions import FrozenError + + assert FrozenError is _attr.exceptions.FrozenError + + def test_filters(self): + """ + Importing from attrs.filters works. + """ + from attrs.filters import include + + assert include is _attr.filters.include + + def test_setters(self): + """ + Importing from attrs.setters works. + """ + from attrs.setters import pipe + + assert pipe is _attr.setters.pipe + + def test_validators(self): + """ + Importing from attrs.validators works. + """ + from attrs.validators import and_ + + assert and_ is _attr.validators.and_ diff --git a/tests/typing_example.py b/tests/typing_example.py index 3fced27ee..efacda2a2 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Tuple, Union import attr +import attrs # Typing via "type" Argument --- @@ -59,6 +60,14 @@ class FF: z: Any = attr.ib() +@attrs.define +class FFF: + z: int + + +FFF(1) + + # Inheritance -- @@ -96,6 +105,19 @@ class Error(Exception): str(e) +@attrs.define +class Error2(Exception): + x: int + + +try: + raise Error2(1) +except Error as e: + e.x + e.args + str(e) + + # Converters # XXX: Currently converters can only be functions so none of this works # although the stubs should be correct. @@ -179,7 +201,7 @@ class Validated: validator=attr.validators.instance_of((int, str)) ) k: Union[int, str, C] = attr.ib( - validator=attr.validators.instance_of((int, C, str)) + validator=attrs.validators.instance_of((int, C, str)) ) @@ -188,9 +210,17 @@ class Validated2: num: int = attr.field(validator=attr.validators.ge(0)) +@attrs.define +class Validated3: + num: int = attr.field(validator=attr.validators.ge(0)) + + with attr.validators.disabled(): Validated2(num=-1) +with attrs.validators.disabled(): + Validated3(num=-1) + try: attr.validators.set_disabled(True) Validated2(num=-1) @@ -207,6 +237,14 @@ class WithCustomRepr: d: bool = attr.ib(repr=str) +@attrs.define +class WithCustomRepr2: + a: int = attrs.field(repr=True) + b: str = attrs.field(repr=False) + c: str = attrs.field(repr=lambda value: "c is for cookie") + d: bool = attrs.field(repr=str) + + # Check some of our own types @attr.s(eq=True, order=False) class OrderFlags: @@ -228,16 +266,43 @@ class ValidatedSetter: ) +@attrs.define(on_setattr=attr.setters.validate) +class ValidatedSetter2: + a: int + b: str = attrs.field(on_setattr=attrs.setters.NO_OP) + c: bool = attrs.field(on_setattr=attrs.setters.frozen) + d: int = attrs.field( + on_setattr=[attrs.setters.convert, attrs.setters.validate] + ) + e: bool = attrs.field( + on_setattr=attrs.setters.pipe( + attrs.setters.convert, attrs.setters.validate + ) + ) + + # field_transformer def ft_hook(cls: type, attribs: List[attr.Attribute]) -> List[attr.Attribute]: return attribs +# field_transformer +def ft_hook2( + cls: type, attribs: List[attrs.Attribute] +) -> List[attrs.Attribute]: + return attribs + + @attr.s(field_transformer=ft_hook) class TransformedAttrs: x: int +@attrs.define(field_transformer=ft_hook2) +class TransformedAttrs2: + x: int + + # Auto-detect @attr.s(auto_detect=True) class AutoDetect: @@ -276,6 +341,11 @@ class NGFrozen: a.evolve(repr=False) +attrs.fields(NGFrozen).x.evolve(eq=False) +a = attrs.fields(NGFrozen).x +a.evolve(repr=False) + + @attr.s(collect_by_mro=True) class MRO: pass @@ -288,6 +358,17 @@ class FactoryTest: c: List[int] = attr.ib(default=attr.Factory((lambda s: s.a), True)) +@attrs.define +class FactoryTest2: + a: List[int] = attrs.field(default=attrs.Factory(list)) + b: List[Any] = attrs.field(default=attrs.Factory(list, False)) + c: List[int] = attrs.field(default=attrs.Factory((lambda s: s.a), True)) + + +attrs.asdict(FactoryTest2()) +attr.asdict(FactoryTest(), tuple_keys=True) + + # Check match_args stub @attr.s(match_args=False) class MatchArgs: @@ -297,3 +378,41 @@ class MatchArgs: attr.asdict(FactoryTest()) attr.asdict(FactoryTest(), retain_collection_types=False) + + +# Check match_args stub +@attrs.define(match_args=False) +class MatchArgs2: + a: int + b: int + + +# NG versions of asdict/astuple +attrs.asdict(MatchArgs2(1, 2)) +attrs.astuple(MatchArgs2(1, 2)) + + +def importing_from_attr() -> None: + """ + Use a function to keep the ns clean. + """ + from attr.converters import optional + from attr.exceptions import FrozenError + from attr.filters import include + from attr.setters import frozen + from attr.validators import and_ + + assert optional and FrozenError and include and frozen and and_ + + +def importing_from_attrs() -> None: + """ + Use a function to keep the ns clean. + """ + from attrs.converters import optional + from attrs.exceptions import FrozenError + from attrs.filters import include + from attrs.setters import frozen + from attrs.validators import and_ + + assert optional and FrozenError and include and frozen and and_ From 04e7efa03773b4be04df8f8f47cebf167ef6bae3 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:06:55 +0100 Subject: [PATCH 08/60] We don't create ordering by default anymore --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a2aa04bbc..c442e4667 100644 --- a/README.rst +++ b/README.rst @@ -69,7 +69,7 @@ After *declaring* your attributes ``attrs`` gives you: - a concise and explicit overview of the class's attributes, - a nice human-readable ``__repr__``, -- a complete set of comparison methods (equality and ordering), +- a equality-checking methods, - an initializer, - and much more, From f41c6603b30f9678fe7a1c108b7e7ae7bd0667af Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:07:07 +0100 Subject: [PATCH 09/60] Cut paragraph I hope we don't need to argue against tuples in 2021 anymore. --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index c442e4667..30ce5bdeb 100644 --- a/README.rst +++ b/README.rst @@ -75,10 +75,6 @@ After *declaring* your attributes ``attrs`` gives you: *without* writing dull boilerplate code again and again and *without* runtime performance penalties. -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. -Which in turn encourages you to write *small classes* that do `one thing well `_. -Never again violate the `single responsibility principle `_ just because implementing ``__init__`` et al is a painful drag. - ---- **Hate type annotations**!? From 850df71ad27cc6c13a0f01e2791f2ae427ad70f8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:25:05 +0100 Subject: [PATCH 10/60] Shorten enumeration --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 30ce5bdeb..c25e5af2e 100644 --- a/README.rst +++ b/README.rst @@ -83,7 +83,7 @@ Types are entirely **optional** with ``attrs``. Simply assign ``attrs.field()`` to the attributes instead of annotating them with types. This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0. -The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. +The classic APIs (``@attr.s``, ``attr.ib``, and their serious business aliases) will remain indefinitely. Please check out `On The Core API Names `_ for a more in-depth explanation. From 4db5819312d7e3cc1591918617a89ebcdce741a1 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:30:46 +0100 Subject: [PATCH 11/60] Add ns explainer --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c25e5af2e..548c800fa 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,7 @@ No problem! Types are entirely **optional** with ``attrs``. Simply assign ``attrs.field()`` to the attributes instead of annotating them with types. -This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0. +This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0, and the ``attrs`` import namespace that has been added in version 21.3.0. The classic APIs (``@attr.s``, ``attr.ib``, and their serious business aliases) will remain indefinitely. Please check out `On The Core API Names `_ for a more in-depth explanation. From 9564113ebf54df09054317af79805647e3e65b80 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:33:39 +0100 Subject: [PATCH 12/60] Stress that attr isn't going anywhere --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 548c800fa..9c0381ebd 100644 --- a/README.rst +++ b/README.rst @@ -82,8 +82,8 @@ No problem! Types are entirely **optional** with ``attrs``. Simply assign ``attrs.field()`` to the attributes instead of annotating them with types. -This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0, and the ``attrs`` import namespace that has been added in version 21.3.0. -The classic APIs (``@attr.s``, ``attr.ib``, and their serious business aliases) will remain indefinitely. +This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. +The classic APIs (``@attr.s``, ``attr.ib``, plus their serious business aliases) and the ``attr`` package import name will remain **indefinitely**. Please check out `On The Core API Names `_ for a more in-depth explanation. From 0a0470148d03a2670bf6cce61aa4d437a91eb898 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 08:35:08 +0100 Subject: [PATCH 13/60] Move hr --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9c0381ebd..d81b79aa3 100644 --- a/README.rst +++ b/README.rst @@ -75,13 +75,13 @@ After *declaring* your attributes ``attrs`` gives you: *without* writing dull boilerplate code again and again and *without* runtime performance penalties. ----- - **Hate type annotations**!? No problem! Types are entirely **optional** with ``attrs``. Simply assign ``attrs.field()`` to the attributes instead of annotating them with types. +---- + This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. The classic APIs (``@attr.s``, ``attr.ib``, plus their serious business aliases) and the ``attr`` package import name will remain **indefinitely**. From b0e87f6c1011e2e67e8b94c5fb87fcf4854480d4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 09:00:11 +0100 Subject: [PATCH 14/60] Adapt types.rst to new defaults Fixes #891 --- docs/types.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/types.rst b/docs/types.rst index a05d35f2a..8a7613768 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -3,30 +3,30 @@ Type Annotations ``attrs`` comes with first class support for type annotations for both Python 3.6 (:pep:`526`) and legacy syntax. -On Python 3.6 and later, you can even drop the `attr.ib`\ s if you're willing to annotate *all* attributes. -That means that on modern Python versions, the declaration part of the example from the README can be simplified to: - +However they will forever remain *optional*, therefore the example from the README could also be written as: .. doctest:: - >>> import attr + >>> from attrs import define, field - >>> @attr.s(auto_attribs=True) + >>> @define ... class SomeClass: - ... a_number: int = 42 - ... list_of_numbers: list[int] = attr.Factory(list) + ... a_number = field(default=42) + ... list_of_numbers = field(factory=list) >>> sc = SomeClass(1, [1, 2, 3]) >>> sc SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) - >>> attr.fields(SomeClass).a_number.type - -You will still need `attr.ib` for advanced features, but not for the common cases. +You can choose freely between the approaches, but please remember that if you choose to use type annotations, you **must** annotate **all** attributes! + +---- + +Even when going all-in an type annotations, you will need `attr.field` for some advanced features though. One of those features are the decorator-based features like defaults. It's important to remember that ``attrs`` doesn't do any magic behind your back. -All the decorators are implemented using an object that is returned by the call to `attr.ib`. +All the decorators are implemented using an object that is returned by the call to `attrs.field`. Attributes that only carry a class annotation do not have that object so trying to call a method on it will inevitably fail. @@ -35,10 +35,10 @@ Attributes that only carry a class annotation do not have that object so trying Please note that types -- however added -- are *only metadata* that can be queried from the class and they aren't used for anything out of the box! Because Python does not allow references to a class object before the class is defined, -types may be defined as string literals, so-called *forward references*. -Also, starting in Python 3.10 (:pep:`526`) **all** annotations will be string literals. -When this happens, ``attrs`` will simply put these string literals into the ``type`` attributes. -If you need to resolve these to real types, you can call `attr.resolve_types` which will update the attribute in place. +types may be defined as string literals, so-called *forward references* (:pep:`526`). +You can enable this automatically for a whole module by using ``from __future__ import annotations`` (:pep:`563`) as of Python 3.7. +In this case ``attrs`` simply puts these string literals into the ``type`` attributes. +If you need to resolve these to real types, you can call `attrs.resolve_types` which will update the attribute in place. In practice though, types show their biggest usefulness in combination with tools like mypy_, pytype_, or pyright_ that have dedicated support for ``attrs`` classes. From 4278b2df3914c4fb25dd920435a24a685e9d8161 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 09:21:11 +0100 Subject: [PATCH 15/60] Final light polish on docs --- docs/api.rst | 2 +- docs/extending.rst | 6 +++--- src/attr/_next_gen.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index bb52c0697..e8b789266 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -16,7 +16,7 @@ As of version 21.3.0, ``attrs`` consists of **two** to-level package names: Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). Using this namespace requires Python 3.6 or later. -The ``attrs`` namespace is built *on top of* the ``attr`` which will *never* go away. +The ``attrs`` namespace is built *on top of* ``attr`` which will *never* go away. Core diff --git a/docs/extending.rst b/docs/extending.rst index 57eaee94e..faf71afd9 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -2,7 +2,7 @@ Extending ========= Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. -It is a tuple of `attrs.Attribute` carrying meta-data about each attribute. +It's a tuple of `attrs.Attribute` carrying metadata about each attribute. So it is fairly simple to build your own decorators on top of ``attrs``: @@ -264,14 +264,14 @@ A more realistic example would be to automatically convert data that you, e.g., Customize Value Serialization in ``asdict()`` --------------------------------------------- -``attrs`` allows you to serialize instances of ``attrs`` classes to dicts using the `attr.asdict` function. +``attrs`` allows you to serialize instances of ``attrs`` classes to dicts using the `attrs.asdict` function. However, the result can not always be serialized since most data types will remain as they are: .. doctest:: >>> import json >>> import datetime - >>> from attr import asdict + >>> from attrs import asdict >>> >>> @frozen ... class Data: diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 27adb0f52..acf630b49 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -66,10 +66,10 @@ def define( :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves exactly like `attr.s`. If left `None`, `attr.s` will try to guess: - 1. If any attributes are annotated and no unannotated `attr.ib`\ s + 1. If any attributes are annotated and no unannotated `attrs.fields`\ s are found, it assumes *auto_attribs=True*. 2. Otherwise it assumes *auto_attribs=False* and tries to collect - `attr.ib`\ s. + `attrs.fields`\ s. For now, please refer to `attr.s` for the rest of the parameters. From 046beaaaaa0e4282214568efeb5418994ab46a79 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 09:29:09 +0100 Subject: [PATCH 16/60] Apply SPDX IDs --- conftest.py | 2 ++ docs/conf.py | 2 ++ setup.py | 2 ++ src/attr/__init__.py | 2 ++ src/attr/_cmp.py | 2 ++ src/attr/_compat.py | 2 ++ src/attr/_config.py | 2 ++ src/attr/_funcs.py | 2 ++ src/attr/_make.py | 2 ++ src/attr/_next_gen.py | 2 ++ src/attr/_version_info.py | 2 ++ src/attr/converters.py | 2 ++ src/attr/exceptions.py | 2 ++ src/attr/filters.py | 2 ++ src/attr/setters.py | 2 ++ src/attr/validators.py | 2 ++ src/attrs/__init__.py | 2 ++ src/attrs/converters.py | 2 ++ src/attrs/exceptions.py | 2 ++ src/attrs/filters.py | 2 ++ src/attrs/setters.py | 2 ++ src/attrs/validators.py | 2 ++ tests/__init__.py | 1 + tests/attr_import_star.py | 2 ++ tests/dataclass_transform_example.py | 2 ++ tests/strategies.py | 2 ++ tests/test_3rd_party.py | 2 ++ tests/test_annotations.py | 2 ++ tests/test_cmp.py | 2 ++ tests/test_compat.py | 2 ++ tests/test_config.py | 2 ++ tests/test_converters.py | 2 ++ tests/test_dunders.py | 2 ++ tests/test_filters.py | 2 ++ tests/test_funcs.py | 2 ++ tests/test_functional.py | 2 ++ tests/test_hooks.py | 2 ++ tests/test_import.py | 3 +++ tests/test_init_subclass.py | 2 ++ tests/test_make.py | 2 ++ tests/test_next_gen.py | 2 ++ tests/test_pattern_matching.py | 2 ++ tests/test_pyright.py | 2 ++ tests/test_setattr.py | 2 ++ tests/test_slots.py | 2 ++ tests/test_validators.py | 2 ++ tests/test_version_info.py | 2 ++ tests/typing_example.py | 2 ++ tests/utils.py | 2 ++ 49 files changed, 98 insertions(+) diff --git a/conftest.py b/conftest.py index f3e7556be..0d539a115 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function from hypothesis import HealthCheck, settings diff --git a/docs/conf.py b/docs/conf.py index aa42845b5..56d91b13f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from importlib import metadata diff --git a/setup.py b/setup.py index 0314ba007..0bf7e50c0 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + import codecs import os import platform diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 391036bf0..81cd2da8f 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import sys diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py index b747b603f..6cffa4dba 100644 --- a/src/attr/_cmp.py +++ b/src/attr/_cmp.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import functools diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 90026fec3..dc0cb02b6 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import platform diff --git a/src/attr/_config.py b/src/attr/_config.py index 546b43870..fc9be29d0 100644 --- a/src/attr/_config.py +++ b/src/attr/_config.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 2f5fae92b..4c90085a4 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import copy diff --git a/src/attr/_make.py b/src/attr/_make.py index 4b0d667d3..19acc457d 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import copy diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index acf630b49..068253688 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ These are Python 3.6+-only and keyword-only APIs that call `attr.s` and `attr.ib` with different default values. diff --git a/src/attr/_version_info.py b/src/attr/_version_info.py index 014e78a1b..cdaeec37a 100644 --- a/src/attr/_version_info.py +++ b/src/attr/_version_info.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function from functools import total_ordering diff --git a/src/attr/converters.py b/src/attr/converters.py index 1dd341e44..1fb6c05d7 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Commonly useful converters. """ diff --git a/src/attr/exceptions.py b/src/attr/exceptions.py index f6f9861be..b2f1edc32 100644 --- a/src/attr/exceptions.py +++ b/src/attr/exceptions.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function diff --git a/src/attr/filters.py b/src/attr/filters.py index 5c88280e5..a1978a877 100644 --- a/src/attr/filters.py +++ b/src/attr/filters.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Commonly useful filters for `attr.asdict`. """ diff --git a/src/attr/setters.py b/src/attr/setters.py index 240014b3c..b1cbb5d83 100644 --- a/src/attr/setters.py +++ b/src/attr/setters.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Commonly used hooks for on_setattr. """ diff --git a/src/attr/validators.py b/src/attr/validators.py index 62fcc7e1d..0b0c8342f 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Commonly useful validators. """ diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py index 7c8c11f04..a704b8b56 100644 --- a/src/attrs/__init__.py +++ b/src/attrs/__init__.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from attr import ( NOTHING, Attribute, diff --git a/src/attrs/converters.py b/src/attrs/converters.py index c2b3cfb26..edfa8d3c1 100644 --- a/src/attrs/converters.py +++ b/src/attrs/converters.py @@ -1 +1,3 @@ +# SPDX-License-Identifier: MIT + from attr.converters import * # noqa diff --git a/src/attrs/exceptions.py b/src/attrs/exceptions.py index 2b2bc3c04..bd9efed20 100644 --- a/src/attrs/exceptions.py +++ b/src/attrs/exceptions.py @@ -1 +1,3 @@ +# SPDX-License-Identifier: MIT + from attr.exceptions import * # noqa diff --git a/src/attrs/filters.py b/src/attrs/filters.py index cb843cac5..52959005b 100644 --- a/src/attrs/filters.py +++ b/src/attrs/filters.py @@ -1 +1,3 @@ +# SPDX-License-Identifier: MIT + from attr.filters import * # noqa diff --git a/src/attrs/setters.py b/src/attrs/setters.py index 348aa3b1b..9b5077080 100644 --- a/src/attrs/setters.py +++ b/src/attrs/setters.py @@ -1 +1,3 @@ +# SPDX-License-Identifier: MIT + from attr.setters import * # noqa diff --git a/src/attrs/validators.py b/src/attrs/validators.py index ad46fbb03..ab2c9b302 100644 --- a/src/attrs/validators.py +++ b/src/attrs/validators.py @@ -1 +1,3 @@ +# SPDX-License-Identifier: MIT + from attr.validators import * # noqa diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..548d2d447 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: MIT diff --git a/tests/attr_import_star.py b/tests/attr_import_star.py index 810f6c07c..eaec321ba 100644 --- a/tests/attr_import_star.py +++ b/tests/attr_import_star.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import from attr import * # noqa: F401,F403 diff --git a/tests/dataclass_transform_example.py b/tests/dataclass_transform_example.py index f2e949d92..49e09061a 100644 --- a/tests/dataclass_transform_example.py +++ b/tests/dataclass_transform_example.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + import attr diff --git a/tests/strategies.py b/tests/strategies.py index 70d424af4..99f9f4853 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Testing strategies for Hypothesis-based tests. """ diff --git a/tests/test_3rd_party.py b/tests/test_3rd_party.py index 0dab852ec..1de6b335f 100644 --- a/tests/test_3rd_party.py +++ b/tests/test_3rd_party.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for compatibility against other Python modules. """ diff --git a/tests/test_annotations.py b/tests/test_annotations.py index dd815228d..a201ebf7f 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for PEP-526 type annotations. diff --git a/tests/test_cmp.py b/tests/test_cmp.py index fa6b31821..ec2c68748 100644 --- a/tests/test_cmp.py +++ b/tests/test_cmp.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for methods from `attrib._cmp`. """ diff --git a/tests/test_compat.py b/tests/test_compat.py index 43ba374bf..464b492f0 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + import pytest from attr._compat import metadata_proxy diff --git a/tests/test_config.py b/tests/test_config.py index 287be03a5..bbf675640 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr._config`. """ diff --git a/tests/test_converters.py b/tests/test_converters.py index 82c62005a..d0fc723eb 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr.converters`. """ diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 57d33bef1..186762eb0 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for dunder methods from `attrib._make`. """ diff --git a/tests/test_filters.py b/tests/test_filters.py index c47cca47a..d1ec24dc6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr.filters`. """ diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 79dfdffe2..4490ed815 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr._funcs`. """ diff --git a/tests/test_functional.py b/tests/test_functional.py index c616f8c1f..9b6a27e2f 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ End-to-end tests. """ diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 7e5ac3d9e..92fc2dcaa 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from datetime import datetime from typing import Dict, List diff --git a/tests/test_import.py b/tests/test_import.py index bd2cb4aec..423124319 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: MIT + + class TestImportStar(object): def test_from_attr_import_star(self): """ diff --git a/tests/test_init_subclass.py b/tests/test_init_subclass.py index 2748655a0..863e79437 100644 --- a/tests/test_init_subclass.py +++ b/tests/test_init_subclass.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `__init_subclass__` related tests. diff --git a/tests/test_make.py b/tests/test_make.py index 6f4888ace..729d3a71f 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr._make`. """ diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 7f5aff75c..8395f9c02 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Python 3-only integration tests for provisional next generation APIs. """ diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py index 7c320a75d..470446c32 100644 --- a/tests/test_pattern_matching.py +++ b/tests/test_pattern_matching.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + # flake8: noqa # Python 3.10 issue in flake8: https://github.com/PyCQA/pyflakes/issues/634 # Keep this file SHORT, until Black and flake8 can handle it. diff --git a/tests/test_pyright.py b/tests/test_pyright.py index 60aabe780..c30dcc5cb 100644 --- a/tests/test_pyright.py +++ b/tests/test_pyright.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + import json import os.path import shutil diff --git a/tests/test_setattr.py b/tests/test_setattr.py index 8e55da2d1..aaedde574 100644 --- a/tests/test_setattr.py +++ b/tests/test_setattr.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import pickle diff --git a/tests/test_slots.py b/tests/test_slots.py index 47abee238..baf9a40dd 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Unit tests for slots-related functionality. """ diff --git a/tests/test_validators.py b/tests/test_validators.py index fb4382a9f..d7c6de8ba 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Tests for `attr.validators`. """ diff --git a/tests/test_version_info.py b/tests/test_version_info.py index db4053f94..41f75f47a 100644 --- a/tests/test_version_info.py +++ b/tests/test_version_info.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + from __future__ import absolute_import, division, print_function import pytest diff --git a/tests/typing_example.py b/tests/typing_example.py index efacda2a2..a85c768c1 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + import re from typing import Any, Dict, List, Tuple, Union diff --git a/tests/utils.py b/tests/utils.py index ad3fb578a..a2fefbd60 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + """ Common helper functions for tests. """ From 9f745505190973f4e3ebc8464aee0d1eedbb11cb Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 09:32:19 +0100 Subject: [PATCH 17/60] flake8 can handle pattern matching now --- tests/test_pattern_matching.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py index 470446c32..590804a8a 100644 --- a/tests/test_pattern_matching.py +++ b/tests/test_pattern_matching.py @@ -1,14 +1,10 @@ # SPDX-License-Identifier: MIT -# flake8: noqa -# Python 3.10 issue in flake8: https://github.com/PyCQA/pyflakes/issues/634 -# Keep this file SHORT, until Black and flake8 can handle it. +# Keep this file SHORT, until Black can handle it. import pytest import attr -from attr import make_class - class TestPatternMatching: """ @@ -35,6 +31,7 @@ class C(object): matched = True assert matched + assert 1 == a def test_explicit_match_args(self): """ @@ -53,7 +50,7 @@ class C: msg = r"C\(\) accepts 0 positional sub-patterns \(1 given\)" with pytest.raises(TypeError, match=msg): match c: - case C(a): + case C(_): pass def test_match_args_kw_only(self): @@ -101,3 +98,4 @@ class C: found = True assert found + assert (1, 1) == (a, b) From bab5d131f826943be6ef68a4e1c210592fab0396 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 09:33:13 +0100 Subject: [PATCH 18/60] Check YAML, too --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a29c55487..a913b068f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,10 +33,11 @@ repos: language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements language_version: python3.10 - id: check-toml + - id: check-yaml From d4e32209dc5855796e57c2b08bdc1c1702d051ab Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 10:07:18 +0100 Subject: [PATCH 19/60] Use attrs namespace throughout examples.rst --- docs/examples.rst | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index fd6feb549..1ef1a4ba4 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -9,7 +9,7 @@ The simplest possible usage is: .. doctest:: - >>> from attr import define + >>> from attrs import define >>> @define ... class Empty: ... pass @@ -189,18 +189,16 @@ When you have a class with data, it often is very convenient to transform that c .. doctest:: - >>> from attr import asdict + >>> from attrs import asdict >>> asdict(Coordinates(x=1, y=2)) {'x': 1, 'y': 2} Some fields cannot or should not be transformed. -For that, `attr.asdict` offers a callback that decides whether an attribute should be included: +For that, `attrs.asdict` offers a callback that decides whether an attribute should be included: .. doctest:: - >>> from attr import asdict - >>> @define ... class User(object): ... email: str @@ -219,7 +217,7 @@ For the common case where you want to `include ` or `exclu .. doctest:: - >>> from attr import asdict, filters, fields + >>> from attrs import asdict, filters, fields >>> @define ... class User: @@ -247,7 +245,7 @@ Other times, all you want is a tuple and ``attrs`` won't let you down: .. doctest:: >>> import sqlite3 - >>> from attr import astuple + >>> from attrs import astuple >>> @define ... class Foo: @@ -363,7 +361,7 @@ You can use a decorator: .. doctest:: - >>> from attr import validators + >>> from attrs import validators >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: @@ -454,7 +452,7 @@ All ``attrs`` attributes may include arbitrary metadata in the form of a read-on .. doctest:: - >>> from attr import fields + >>> from attrs import fields >>> @define ... class C: @@ -478,7 +476,7 @@ Types .. doctest:: - >>> from attr import attrib, fields + >>> from attrs import attrib, fields >>> @define ... class C: @@ -497,7 +495,7 @@ If you don't mind annotating *all* attributes, you can even drop the `attrs.fiel .. doctest:: >>> import typing - >>> from attr import fields + >>> from attrs import fields >>> @define ... class AutoC: @@ -527,7 +525,7 @@ This will replace the *type* attribute in the respective fields. .. doctest:: - >>> from attr import fields, resolve_types + >>> from attrs import fields, resolve_types >>> @define ... class A: @@ -604,7 +602,7 @@ In Clojure that function is called `assoc >> from attr import evolve + >>> from attrs import evolve >>> @frozen ... class C: @@ -641,7 +639,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` .. doctest:: - >>> from attr import make_class + >>> from attrs import make_class >>> C = make_class("C", {"x": field(default=42), ... "y": field(default=Factory(list))}, @@ -658,7 +656,7 @@ If you need to dynamically make a class with `attrs.make_class` and it needs to .. doctest:: - >>> from attr import make_class + >>> from attrs import make_class >>> class D: ... def __eq__(self, other): From b4dc9b07c70c16848960da077fc7ac18fe5e9bc8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 10:15:13 +0100 Subject: [PATCH 20/60] Better 2.7 example --- docs/examples.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 1ef1a4ba4..ba5343d4a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -476,7 +476,7 @@ Types .. doctest:: - >>> from attrs import attrib, fields + >>> from attrs import fields >>> @define ... class C: @@ -484,9 +484,10 @@ Types >>> fields(C).x.type - >>> @define - ... class C: - ... x = attrib(type=int) + >>> import attr + >>> @attr.s + ... class C(object): + ... x = attr.ib(type=int) >>> fields(C).x.type From 2d77d83d4e3ceadbf4414da5963623a20564c415 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 16:38:49 +0100 Subject: [PATCH 21/60] Fix dataclass_transform links --- docs/types.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types.rst b/docs/types.rst index 8a7613768..4c6a47c88 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -96,7 +96,7 @@ Given the following definition, ``pyright`` will generate static type signatures - The ``attr.frozen`` decorator is not typed with frozen attributes, which are properly typed via ``attr.define(frozen=True)``. - A `full list `_ of limitations and incompatibilities can be found in pyright's repository. + A `full list `_ of limitations and incompatibilities can be found in pyright's repository. Your constructive feedback is welcome in both `attrs#795 `_ and `pyright#1782 `_. Generally speaking, the decision on improving ``attrs`` support in pyright is entirely Microsoft's prerogative though. @@ -105,4 +105,4 @@ Given the following definition, ``pyright`` will generate static type signatures .. _mypy: http://mypy-lang.org .. _pytype: https://google.github.io/pytype/ .. _pyright: https://github.com/microsoft/pyright -.. _dataclass_transform: https://github.com/microsoft/pyright/blob/master/specs/dataclass_transforms.md +.. _dataclass_transform: https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md From 3333e749781a107c829717f7bc0382d33b538b6e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 16:39:25 +0100 Subject: [PATCH 22/60] Remove dead achor --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bdf3a418a..d6b2ba433 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -234,7 +234,7 @@ Deprecations Please check out the linked issue for more details. These new APIs have been added *provisionally* as part of #666 so you can try them out today and provide feedback. - Learn more in the `API docs `_. + Learn more in the `API docs `_. `#408 `_ From 26c0cef8e48bd131d062d45bdaa0c949d4a2d035 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 27 Dec 2021 17:47:14 +0100 Subject: [PATCH 23/60] Streamline workflow --- .github/workflows/main.yml | 57 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5aa1ee95..64296294b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,14 +10,14 @@ on: env: FORCE_COLOR: "1" # Make tools pretty. - TOX_TESTENV_PASSENV: "FORCE_COLOR" + TOX_TESTENV_PASSENV: FORCE_COLOR PYTHON_LATEST: "3.10" jobs: tests: - name: "tox on ${{ matrix.python-version }}" - runs-on: "ubuntu-latest" + name: tox on ${{ matrix.python-version }} + runs-on: ubuntu-latest strategy: fail-fast: false @@ -25,10 +25,10 @@ jobs: python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-2.7", "pypy-3.7"] steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: - python-version: "${{ matrix.python-version }}" + python-version: ${{ matrix.python-version }} - name: "Install dependencies" run: | @@ -37,8 +37,7 @@ jobs: python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade virtualenv tox tox-gh-actions - - name: "Run tox targets for ${{ matrix.python-version }}" - run: "python -m tox" + - run: "python -m tox" - name: Upload coverage data uses: "actions/upload-artifact@v2" @@ -49,7 +48,7 @@ jobs: coverage: - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest needs: tests steps: @@ -59,21 +58,20 @@ jobs: # Use latest Python, so it understands all syntax. python-version: ${{env.PYTHON_LATEST}} - - name: Install Coverage.py - run: python -m pip install --upgrade coverage[toml] + - run: python -m pip install --upgrade coverage[toml] - name: Download coverage data uses: actions/download-artifact@v2 with: name: coverage-data - - name: Combine coverage and fail if it's <100% + - name: Combine coverage and fail if it's <100%. run: | python -m coverage combine python -m coverage html --skip-covered --skip-empty python -m coverage report --fail-under=100 - - name: Upload HTML report for failed check + - name: Upload HTML report if check failed. uses: actions/upload-artifact@v2 with: name: html-report @@ -82,35 +80,34 @@ jobs: package: - name: "Build & verify package" - runs-on: "ubuntu-latest" + name: Build & verify package + runs-on: ubuntu-latest steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: python-version: ${{env.PYTHON_LATEST}} - - run: "python -m pip install build twine check-wheel-contents" - - run: "python -m build --sdist --wheel ." - - run: "ls -l dist" - - run: "check-wheel-contents dist/*.whl" - - name: "Check long_description" - run: "python -m twine check dist/*" + - run: python -m pip install build twine check-wheel-contents + - run: python -m build --sdist --wheel . + - run: ls -l dist + - run: check-wheel-contents dist/*.whl + - name: Check long_description + run: python -m twine check dist/* install-dev: - name: "Verify dev env" - runs-on: "${{ matrix.os }}" + name: Verify dev env + runs-on: ${{ matrix.os }} strategy: matrix: os: ["ubuntu-latest", "windows-latest"] steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: python-version: ${{env.PYTHON_LATEST}} - - run: "python -m pip install -e .[dev]" - - name: "Import package" - run: "python -c 'import attr; print(attr.__version__)'" + - run: python -m pip install -e .[dev] + - run: python -c 'import attr; print(attr.__version__)' From e09873485e14e9b11d5d590a55280894df367d92 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 06:34:04 +0100 Subject: [PATCH 24/60] Add logo to PyPI description --- setup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0bf7e50c0..ce0419f11 100644 --- a/setup.py +++ b/setup.py @@ -99,12 +99,16 @@ def find_meta(meta): raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) +LOGO = """ +.. image:: https://www.attrs.org/en/stable/_static/attrs_logo.png + :alt: attrs logo + :align: center +""" # noqa + VERSION = find_meta("version") URL = find_meta("url") LONG = ( - "======================================\n" - "``attrs``: Classes Without Boilerplate\n" - "======================================\n" + LOGO + read("README.rst").split(".. teaser-begin")[1] + "\n\n" + "Release Information\n" From fcfb5a692cc8c9f8fde8e39bbd2c5733a47fb1e7 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 06:39:24 +0100 Subject: [PATCH 25/60] Last pass over changelogs --- changelog.d/817.change.rst | 2 +- changelog.d/819.change.rst | 2 +- changelog.d/830.change.rst | 2 +- changelog.d/835.breaking.rst | 4 ++-- changelog.d/843.change.rst | 2 +- changelog.d/857.change.rst | 2 +- changelog.d/859.change.rst | 4 ++-- changelog.d/877.change.rst | 2 +- changelog.d/886.breaking.rst | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/changelog.d/817.change.rst b/changelog.d/817.change.rst index 3a53efc74..9c5715279 100644 --- a/changelog.d/817.change.rst +++ b/changelog.d/817.change.rst @@ -1 +1 @@ -If the class-level *on_setattr* is set to ``attr.setters.validate`` (default in ``@attr.define`` and ``@attr.mutable``) but no field defines a validator, pretend that it's not set. +If the class-level *on_setattr* is set to ``attrs.setters.validate`` (default in ``@define`` and ``@mutable``) but no field defines a validator, pretend that it's not set. diff --git a/changelog.d/819.change.rst b/changelog.d/819.change.rst index eb45d6168..51fc54b2f 100644 --- a/changelog.d/819.change.rst +++ b/changelog.d/819.change.rst @@ -1 +1 @@ -The generated ``__repr__`` is significantly faster on Pythons with F-strings. +The generated ``__repr__`` is significantly faster on Pythons with f-strings. diff --git a/changelog.d/830.change.rst b/changelog.d/830.change.rst index 06d454498..ce1381391 100644 --- a/changelog.d/830.change.rst +++ b/changelog.d/830.change.rst @@ -1 +1 @@ -Added ``attr.converters.to_bool()``. +Added ``attrs.converters.to_bool()``. diff --git a/changelog.d/835.breaking.rst b/changelog.d/835.breaking.rst index 8cdf0412d..369a22f33 100644 --- a/changelog.d/835.breaking.rst +++ b/changelog.d/835.breaking.rst @@ -1,4 +1,4 @@ -When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. -I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``. +When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. +I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. diff --git a/changelog.d/843.change.rst b/changelog.d/843.change.rst index 746950180..ed48f9821 100644 --- a/changelog.d/843.change.rst +++ b/changelog.d/843.change.rst @@ -1,2 +1,2 @@ -``attr.resolve_types()`` now resolves types of subclasses after the parents are resolved. +``attrs.resolve_types()`` now resolves types of subclasses after the parents are resolved. `#842 `_ diff --git a/changelog.d/857.change.rst b/changelog.d/857.change.rst index d8c4e9bd2..66fd13fa4 100644 --- a/changelog.d/857.change.rst +++ b/changelog.d/857.change.rst @@ -1 +1 @@ -``attrs`` classes are now fully compatible with `cloudpickle `_ (no need to disabled ``repr`` anymore). +``attrs`` classes are now fully compatible with `cloudpickle `_ (no need to disable ``repr`` anymore). diff --git a/changelog.d/859.change.rst b/changelog.d/859.change.rst index a79bd984f..12a965eea 100644 --- a/changelog.d/859.change.rst +++ b/changelog.d/859.change.rst @@ -1,4 +1,4 @@ -Added new context manager ``attr.validators.disabled()`` and functions ``attr.validators.(set|get)_disabled()``. -They deprecate ``attr.(set|get)_run_validators()``. +Added new context manager ``attrs.validators.disabled()`` and functions ``attrs.validators.(set|get)_disabled()``. +They deprecate ``attrs.(set|get)_run_validators()``. All functions are interoperable and modify the same internal state. They are not – and never were – thread-safe, though. diff --git a/changelog.d/877.change.rst b/changelog.d/877.change.rst index b90209025..2ad5fcebd 100644 --- a/changelog.d/877.change.rst +++ b/changelog.d/877.change.rst @@ -1 +1 @@ -``attr.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. +``attrs.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. diff --git a/changelog.d/886.breaking.rst b/changelog.d/886.breaking.rst index 8cdf0412d..369a22f33 100644 --- a/changelog.d/886.breaking.rst +++ b/changelog.d/886.breaking.rst @@ -1,4 +1,4 @@ -When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. -I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``. +When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. +I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. From d528dd425980eff3f43b0e29b0ce4dc81ecd8d84 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 06:47:51 +0100 Subject: [PATCH 26/60] Fix more links --- CHANGELOG.rst | 2 +- docs/conf.py | 4 ++++ docs/types.rst | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6b2ba433..7c532bc46 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -115,7 +115,7 @@ Changes See the `new docs on comparison `_ for more details. `#787 `_ -- Added **provisional** support for static typing in ``pyright`` via the `dataclass_transforms specification `_. +- Added **provisional** support for static typing in ``pyright`` via the `dataclass_transforms specification `_. Both the ``pyright`` specification and ``attrs`` implementation may change in future versions of both projects. Your constructive feedback is welcome in both `attrs#795 `_ and `pyright#1782 `_. diff --git a/docs/conf.py b/docs/conf.py index 56d91b13f..0cc80be6a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,11 @@ """ linkcheck_ignore = [ + # We run into GitHub's rate limits. r"https://github.com/.*/(issues|pull)/\d+", + # It never finds the anchor even though it's there. + "https://github.com/microsoft/pyright/blob/main/specs/" + "dataclass_transforms.md#attrs", ] # In nitpick mode (-n), still ignore any of the following "broken" references diff --git a/docs/types.rst b/docs/types.rst index 4c6a47c88..fbb90a7e9 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -96,7 +96,7 @@ Given the following definition, ``pyright`` will generate static type signatures - The ``attr.frozen`` decorator is not typed with frozen attributes, which are properly typed via ``attr.define(frozen=True)``. - A `full list `_ of limitations and incompatibilities can be found in pyright's repository. + A `full list `_ of limitations and incompatibilities can be found in pyright's repository. Your constructive feedback is welcome in both `attrs#795 `_ and `pyright#1782 `_. Generally speaking, the decision on improving ``attrs`` support in pyright is entirely Microsoft's prerogative though. From 20bf4b6e54a75201b378cff8e6dd9521d2da28f1 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 06:57:34 +0100 Subject: [PATCH 27/60] Go over CONTRIBUTING.md --- .github/CONTRIBUTING.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4229bc30a..bbdc20f19 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,6 +30,9 @@ The official tag is `python-attrs` and helping out in support frees us up to imp This is a hard rule; patches with missing tests or documentation can't be merged. - Make sure your changes pass our [CI]. You won't get any feedback until it's green unless you ask for it. +- For the CI to pass, the coverage must be 100%. + If you have problems to test something, open anyway and ask for advice. + In some situations, we may agree to add an `# pragma: no cover`. - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. - Don’t break backwards compatibility. @@ -120,27 +123,27 @@ You don't need to install *towncrier* yourself, you just have to abide by a few - Wrap arguments into asterisks like in docstrings: `Added new argument *an_argument*.` - If you mention functions or other callables, add parentheses at the end of their names: - `attr.func()` or `attr.Class.method()`. + `attrs.func()` or `attrs.Class.method()`. This makes the changelog a lot more readable. - Prefer simple past tense or constructions with "now". For example: - + Added `attr.validators.func()`. - + `attr.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. + + Added `attrs.validators.func()`. + + `attrs.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. - If you want to reference multiple issues, copy the news fragment to another filename. *towncrier* will merge all news fragments with identical contents into one entry with multiple links to the respective pull requests. Example entries: ```rst - Added ``attr.validators.func()``. + Added ``attrs.validators.func()``. The feature really *is* awesome. ``` or: ```rst - ``attr.func()`` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. + ``attrs.func()`` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. The bug really *was* nasty. ``` From dd26edd68e12879f716c6554f25d957af299b801 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 06:59:45 +0100 Subject: [PATCH 28/60] Prepare 21.3.0 --- CHANGELOG.rst | 72 +++++++++++++++++++++++++++++++++--- changelog.d/646.change.rst | 1 - changelog.d/815.change.rst | 4 -- changelog.d/817.change.rst | 1 - changelog.d/819.change.rst | 1 - changelog.d/824.change.rst | 1 - changelog.d/828.change.rst | 1 - changelog.d/830.change.rst | 1 - changelog.d/835.breaking.rst | 4 -- changelog.d/843.change.rst | 2 - changelog.d/845.change.rst | 1 - changelog.d/857.change.rst | 1 - changelog.d/859.change.rst | 4 -- changelog.d/877.change.rst | 1 - changelog.d/886.breaking.rst | 4 -- changelog.d/887.breaking.rst | 14 ------- changelog.d/888.change.rst | 1 - src/attr/__init__.py | 2 +- 18 files changed, 67 insertions(+), 49 deletions(-) delete mode 100644 changelog.d/646.change.rst delete mode 100644 changelog.d/815.change.rst delete mode 100644 changelog.d/817.change.rst delete mode 100644 changelog.d/819.change.rst delete mode 100644 changelog.d/824.change.rst delete mode 100644 changelog.d/828.change.rst delete mode 100644 changelog.d/830.change.rst delete mode 100644 changelog.d/835.breaking.rst delete mode 100644 changelog.d/843.change.rst delete mode 100644 changelog.d/845.change.rst delete mode 100644 changelog.d/857.change.rst delete mode 100644 changelog.d/859.change.rst delete mode 100644 changelog.d/877.change.rst delete mode 100644 changelog.d/886.breaking.rst delete mode 100644 changelog.d/887.breaking.rst delete mode 100644 changelog.d/888.change.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c532bc46..c4298c414 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,16 +17,76 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. -Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. +.. towncrier release notes start -.. - Do *NOT* add changelog entries here! +21.3.0 (2021-12-28) +------------------- - This changelog is managed by towncrier and is compiled at release time. +Backward-incompatible Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. +- When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. + I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. + + This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. + `#835 `_, + `#886 `_ +- ``import attrs`` has finally landed! + As of this release, you can finally import ``attrs`` using its proper name. + + Not all names from the ``attr`` namespace have been transferred; most notably ``attr.s`` and ``attr.ib`` are missing. + See ``attrs.define`` and ``attrs.field`` if you haven't seen our next-generation APIs yet. + A more elaborate explanation can be found `On The Core API Names `_ + + This feature is at least for one release **provisional**. + We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. + + The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. + Please note that we have **not** moved -- or even removed -- anything from ``attr``! + + Please do report any bugs or documentation inconsistencies! + `#887 `_ + + +Changes +^^^^^^^ + +- ``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. + `#646 `_, + `#888 `_ +- ``__match_args__`` are now generated to support Python 3.10's + `Structural Pattern Matching `_. + This can be controlled by the ``match_args`` argument to the class decorators on Python 3.10 and later. + On older versions, it is never added and the argument is ignored. + `#815 `_ +- If the class-level *on_setattr* is set to ``attrs.setters.validate`` (default in ``@define`` and ``@mutable``) but no field defines a validator, pretend that it's not set. + `#817 `_ +- The generated ``__repr__`` is significantly faster on Pythons with f-strings. + `#819 `_ +- Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again. + `#824 `_ +- Generated source code is now cached more efficiently for identical classes. + `#828 `_ +- Added ``attrs.converters.to_bool()``. + `#830 `_ +- ``attrs.resolve_types()`` now resolves types of subclasses after the parents are resolved. + `#842 `_ + `#843 `_ +- Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and ``maxlen(n)``. + `#845 `_ +- ``attrs`` classes are now fully compatible with `cloudpickle `_ (no need to disable ``repr`` anymore). + `#857 `_ +- Added new context manager ``attrs.validators.disabled()`` and functions ``attrs.validators.(set|get)_disabled()``. + They deprecate ``attrs.(set|get)_run_validators()``. + All functions are interoperable and modify the same internal state. + They are not – and never were – thread-safe, though. + `#859 `_ +- ``attrs.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. + `#877 `_ + + +---- -.. towncrier release notes start 21.2.0 (2021-05-07) ------------------- diff --git a/changelog.d/646.change.rst b/changelog.d/646.change.rst deleted file mode 100644 index aa3e3893d..000000000 --- a/changelog.d/646.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. diff --git a/changelog.d/815.change.rst b/changelog.d/815.change.rst deleted file mode 100644 index e6c368453..000000000 --- a/changelog.d/815.change.rst +++ /dev/null @@ -1,4 +0,0 @@ -``__match_args__`` are now generated to support Python 3.10's -`Structural Pattern Matching `_. -This can be controlled by the ``match_args`` argument to the class decorators on Python 3.10 and later. -On older versions, it is never added and the argument is ignored. diff --git a/changelog.d/817.change.rst b/changelog.d/817.change.rst deleted file mode 100644 index 9c5715279..000000000 --- a/changelog.d/817.change.rst +++ /dev/null @@ -1 +0,0 @@ -If the class-level *on_setattr* is set to ``attrs.setters.validate`` (default in ``@define`` and ``@mutable``) but no field defines a validator, pretend that it's not set. diff --git a/changelog.d/819.change.rst b/changelog.d/819.change.rst deleted file mode 100644 index 51fc54b2f..000000000 --- a/changelog.d/819.change.rst +++ /dev/null @@ -1 +0,0 @@ -The generated ``__repr__`` is significantly faster on Pythons with f-strings. diff --git a/changelog.d/824.change.rst b/changelog.d/824.change.rst deleted file mode 100644 index 4d3e6acda..000000000 --- a/changelog.d/824.change.rst +++ /dev/null @@ -1 +0,0 @@ -Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again. diff --git a/changelog.d/828.change.rst b/changelog.d/828.change.rst deleted file mode 100644 index b4a5454c8..000000000 --- a/changelog.d/828.change.rst +++ /dev/null @@ -1 +0,0 @@ -Generated source code is now cached more efficiently for identical classes. diff --git a/changelog.d/830.change.rst b/changelog.d/830.change.rst deleted file mode 100644 index ce1381391..000000000 --- a/changelog.d/830.change.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``attrs.converters.to_bool()``. diff --git a/changelog.d/835.breaking.rst b/changelog.d/835.breaking.rst deleted file mode 100644 index 369a22f33..000000000 --- a/changelog.d/835.breaking.rst +++ /dev/null @@ -1,4 +0,0 @@ -When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. -I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. - -This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. diff --git a/changelog.d/843.change.rst b/changelog.d/843.change.rst deleted file mode 100644 index ed48f9821..000000000 --- a/changelog.d/843.change.rst +++ /dev/null @@ -1,2 +0,0 @@ -``attrs.resolve_types()`` now resolves types of subclasses after the parents are resolved. -`#842 `_ diff --git a/changelog.d/845.change.rst b/changelog.d/845.change.rst deleted file mode 100644 index 80f3f7da9..000000000 --- a/changelog.d/845.change.rst +++ /dev/null @@ -1 +0,0 @@ -Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and ``maxlen(n)``. diff --git a/changelog.d/857.change.rst b/changelog.d/857.change.rst deleted file mode 100644 index 66fd13fa4..000000000 --- a/changelog.d/857.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attrs`` classes are now fully compatible with `cloudpickle `_ (no need to disable ``repr`` anymore). diff --git a/changelog.d/859.change.rst b/changelog.d/859.change.rst deleted file mode 100644 index 12a965eea..000000000 --- a/changelog.d/859.change.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added new context manager ``attrs.validators.disabled()`` and functions ``attrs.validators.(set|get)_disabled()``. -They deprecate ``attrs.(set|get)_run_validators()``. -All functions are interoperable and modify the same internal state. -They are not – and never were – thread-safe, though. diff --git a/changelog.d/877.change.rst b/changelog.d/877.change.rst deleted file mode 100644 index 2ad5fcebd..000000000 --- a/changelog.d/877.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attrs.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. diff --git a/changelog.d/886.breaking.rst b/changelog.d/886.breaking.rst deleted file mode 100644 index 369a22f33..000000000 --- a/changelog.d/886.breaking.rst +++ /dev/null @@ -1,4 +0,0 @@ -When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. -I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. - -This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. diff --git a/changelog.d/887.breaking.rst b/changelog.d/887.breaking.rst deleted file mode 100644 index 98b4079ff..000000000 --- a/changelog.d/887.breaking.rst +++ /dev/null @@ -1,14 +0,0 @@ -``import attrs`` has finally landed! -As of this release, you can finally import ``attrs`` using its proper name. - -Not all names from the ``attr`` namespace have been transferred; most notably ``attr.s`` and ``attr.ib`` are missing. -See ``attrs.define`` and ``attrs.field`` if you haven't seen our next-generation APIs yet. -A more elaborate explanation can be found `On The Core API Names `_ - -This feature is at least for one release **provisional**. -We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. - -The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. -Please note that we have **not** moved -- or even removed -- anything from ``attr``! - -Please do report any bugs or documentation inconsistencies! diff --git a/changelog.d/888.change.rst b/changelog.d/888.change.rst deleted file mode 100644 index aa3e3893d..000000000 --- a/changelog.d/888.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 81cd2da8f..dc7d68833 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -24,7 +24,7 @@ from ._version_info import VersionInfo -__version__ = "21.3.0.dev0" +__version__ = "21.3.0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From 421a9d37fc7f71bf1926d1e20c4d2b1e18792eab Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 28 Dec 2021 07:07:20 +0100 Subject: [PATCH 29/60] Start new development cycle --- CHANGELOG.rst | 9 +++++++++ src/attr/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c4298c414..a1b7fe94d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,15 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. +Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. + +.. + Do *NOT* add changelog entries here! + + This changelog is managed by towncrier and is compiled at release time. + + See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. + .. towncrier release notes start 21.3.0 (2021-12-28) diff --git a/src/attr/__init__.py b/src/attr/__init__.py index dc7d68833..66bbb4bf2 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -24,7 +24,7 @@ from ._version_info import VersionInfo -__version__ = "21.3.0" +__version__ = "21.4.0.dev0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From ada66bef24700d159acff4b72b7cb34a263ccccf Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Tue, 28 Dec 2021 03:24:51 -0800 Subject: [PATCH 30/60] docs: remove typo in README.rst (#893) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d81b79aa3..e2b07430d 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,7 @@ Data Classes ============ On the tin, ``attrs`` might remind you of ``dataclasses`` (and indeed, ``dataclasses`` are a descendant of ``attrs``). -In practice it does a lot more more and is more flexible. +In practice it does a lot more and is more flexible. For instance it allows you to define `special handling of NumPy arrays for equality checks `_, or allows more ways to `plug into the initialization process `_. For more details, please refer to our `comparison page `_. From 7695908aa7b4a96fbfefb7432145f843ae9c0c98 Mon Sep 17 00:00:00 2001 From: hkclark Date: Wed, 29 Dec 2021 01:42:51 -0500 Subject: [PATCH 31/60] docs: fix very minor typo (#894) Co-authored-by: kclark --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index e8b789266..02aed52ad 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,7 +24,7 @@ Core .. note:: - Please not that the ``attrs`` namespace has been added in version 21.3.0. + Please note that the ``attrs`` namespace has been added in version 21.3.0. Most of the objects are simply re-imported from ``attr``. Therefore if a class, method, or function claims that it has been added in an older version, it is only available in the ``attr`` namespace. From c86fbc864862983111184eb9c3092afe2f2d477e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 08:24:46 +0100 Subject: [PATCH 32/60] Use better word ref #894 --- docs/names.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/names.rst b/docs/names.rst index addd4ed16..0fe953e6a 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -100,7 +100,7 @@ instead of fiddling with the old APIs -- whose names felt anachronistic anyway - So in July 2018, we `looked for better names `_ and came up with `attr.define`, `attr.field`, and friends. Then in January 2019, we `started looking for inconvenient defaults `_ that we now could fix without any repercussions. -These APIs proved to be vastly popular, so we've finally changed the documentation to them in November of 2021. +These APIs proved to be very popular, so we've finally changed the documentation to them in November of 2021. All of this took way too long, of course. One reason is the COVID-19 pandemic, but also our fear to fumble this historic chance to fix our APIs. From 9424a930f966ade789b1ea4b8043efe916c50769 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 09:15:47 +0100 Subject: [PATCH 33/60] Tell coverage about import attrs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b34ed515a..52c0e49ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [tool.coverage.run] parallel = true branch = true -source = ["attr"] +source = ["attr", "attrs"] [tool.coverage.paths] source = ["src", ".tox/*/site-packages"] From ce50f40b52567564000af685959dc6df97c5f384 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 09:16:48 +0100 Subject: [PATCH 34/60] Use correct words ref https://github.com/python-attrs/attrs/commit/430b12ef0c539f28392b7d818af8dd8351e6c72b#r62580685 --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a1b7fe94d..0b6c6d014 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,9 +3,9 @@ Changelog Versions follow `CalVer `_ with a strict backwards-compatibility policy. -The **first digit** of the version is the year. -The **second digit** is incremented with each release, starting at 1 for each year. -The **third digit** is when we need to start branches for older releases (only for emergencies). +The **first number** of the version is the year. +The **second number** is incremented with each release, starting at 1 for each year. +The **third number** is when we need to start branches for older releases (only for emergencies). Put simply, you shouldn't ever be afraid to upgrade ``attrs`` if you're only using its public APIs. Whenever there is a need to break compatibility, it is announced here in the changelog, and raises a ``DeprecationWarning`` for a year (if possible) before it's finally really broken. From 03dd7136cf1ccc58c9612531ba9711892830c1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 29 Dec 2021 09:43:19 +0100 Subject: [PATCH 35/60] Do not require cloudpickle for PyPy (#892) * Do not require cloudpickle for PyPy The cloudpickle package relies on CPython implementation details, and does not even import on PyPy: ``` ImportError while importing test module '/tmp/attrs/tests/test_3rd_party.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: /usr/lib/pypy3.8/importlib/__init__.py:127: in import_module return _bootstrap._gcd_import(name[level:], package, level) tests/test_3rd_party.py:7: in import cloudpickle .tox/pypy3/lib/pypy3.8/site-packages/cloudpickle/__init__.py:4: in from cloudpickle.cloudpickle import * # noqa .tox/pypy3/lib/pypy3.8/site-packages/cloudpickle/cloudpickle.py:57: in from .compat import pickle .tox/pypy3/lib/pypy3.8/site-packages/cloudpickle/compat.py:13: in from _pickle import Pickler # noqa: F401 E ModuleNotFoundError: No module named '_pickle' ``` Disable the dependency for PyPy and make the test handle missing cloudpickle gracefully. * Enable testing on pypy-3.8 * add a news entry Co-authored-by: Hynek Schlawack --- .github/workflows/main.yml | 2 +- changelog.d/892.change.rst | 1 + setup.py | 2 +- tests/test_3rd_party.py | 5 ++++- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog.d/892.change.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 64296294b..f38fd9150 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-2.7", "pypy-3.7"] + python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-2.7", "pypy-3.7", "pypy-3.8"] steps: - uses: actions/checkout@v2 diff --git a/changelog.d/892.change.rst b/changelog.d/892.change.rst new file mode 100644 index 000000000..aa2ebcbc9 --- /dev/null +++ b/changelog.d/892.change.rst @@ -0,0 +1 @@ +Fixed the test suite on PyPy3.8 where cloudpickle does not work. diff --git a/setup.py b/setup.py index ce0419f11..00e7b012a 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "docs": ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"], "tests_no_zope": [ # For regression test to ensure cloudpickle compat doesn't break. - "cloudpickle", + 'cloudpickle; python_implementation == "CPython"', # 5.0 introduced toml; parallel was broken until 5.0.2 "coverage[toml]>=5.0.2", "hypothesis", diff --git a/tests/test_3rd_party.py b/tests/test_3rd_party.py index 1de6b335f..8866d7f6e 100644 --- a/tests/test_3rd_party.py +++ b/tests/test_3rd_party.py @@ -4,13 +4,16 @@ Tests for compatibility against other Python modules. """ -import cloudpickle +import pytest from hypothesis import given from .strategies import simple_classes +cloudpickle = pytest.importorskip("cloudpickle") + + class TestCloudpickleCompat(object): """ Tests for compatibility with ``cloudpickle``. From 0575d51ffddc0de465e7229571d11f9c6f6b8575 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 14:02:59 +0100 Subject: [PATCH 36/60] Make virtual repr file names unique (#896) * Make virtual repr file names unique * Add newsfragments --- changelog.d/895.change.rst | 1 + changelog.d/896.change.rst | 1 + src/attr/_make.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/895.change.rst create mode 100644 changelog.d/896.change.rst diff --git a/changelog.d/895.change.rst b/changelog.d/895.change.rst new file mode 100644 index 000000000..d6c2aff7e --- /dev/null +++ b/changelog.d/895.change.rst @@ -0,0 +1 @@ +Fix ``coverage report`` for projects who use ``attrs`` and don't set a ``--source``. diff --git a/changelog.d/896.change.rst b/changelog.d/896.change.rst new file mode 100644 index 000000000..d6c2aff7e --- /dev/null +++ b/changelog.d/896.change.rst @@ -0,0 +1 @@ +Fix ``coverage report`` for projects who use ``attrs`` and don't set a ``--source``. diff --git a/src/attr/_make.py b/src/attr/_make.py index 19acc457d..d46f8a3e7 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1888,7 +1888,7 @@ def _add_eq(cls, attrs=None): if HAS_F_STRINGS: def _make_repr(attrs, ns, cls): - unique_filename = "repr" + unique_filename = _generate_unique_filename(cls, "repr") # Figure out which attributes to include, and which function to use to # format them. The a.repr value can be either bool or a custom # callable. From 1ff3f1ee919178946bb48442f1e183ea99dae373 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 14:04:39 +0100 Subject: [PATCH 37/60] Minor changelog polish --- changelog.d/892.change.rst | 2 +- changelog.d/895.change.rst | 2 +- changelog.d/896.change.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.d/892.change.rst b/changelog.d/892.change.rst index aa2ebcbc9..78ac31c51 100644 --- a/changelog.d/892.change.rst +++ b/changelog.d/892.change.rst @@ -1 +1 @@ -Fixed the test suite on PyPy3.8 where cloudpickle does not work. +Fixed the test suite on PyPy3.8 where ``cloudpickle`` does not work. diff --git a/changelog.d/895.change.rst b/changelog.d/895.change.rst index d6c2aff7e..ed8d60d7e 100644 --- a/changelog.d/895.change.rst +++ b/changelog.d/895.change.rst @@ -1 +1 @@ -Fix ``coverage report`` for projects who use ``attrs`` and don't set a ``--source``. +Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. diff --git a/changelog.d/896.change.rst b/changelog.d/896.change.rst index d6c2aff7e..ed8d60d7e 100644 --- a/changelog.d/896.change.rst +++ b/changelog.d/896.change.rst @@ -1 +1 @@ -Fix ``coverage report`` for projects who use ``attrs`` and don't set a ``--source``. +Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. From 02ba249b81a8d01ef6e7b04a7412556234aaa3bb Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 14:08:18 +0100 Subject: [PATCH 38/60] Remove dead anchors --- CHANGELOG.rst | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b6c6d014..9a9f606af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -145,7 +145,7 @@ Changes - It's now possible to customize the behavior of ``eq`` and ``order`` by passing in a callable. `#435 `_, `#627 `_ -- The instant favorite `next-generation APIs `_ are not provisional anymore! +- The instant favorite next-generation APIs are not provisional anymore! They are also officially supported by Mypy as of their `0.800 release `_. diff --git a/README.rst b/README.rst index e2b07430d..709bba83d 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,7 @@ Simply assign ``attrs.field()`` to the attributes instead of annotating them wit ---- -This example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. +This example uses ``attrs``'s modern APIs that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. The classic APIs (``@attr.s``, ``attr.ib``, plus their serious business aliases) and the ``attr`` package import name will remain **indefinitely**. Please check out `On The Core API Names `_ for a more in-depth explanation. From 2de90143100e713d8ae6b5d1adb5e1e879af01fb Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 14:09:28 +0100 Subject: [PATCH 39/60] Prepare 21.4.0 --- CHANGELOG.rst | 19 +++++++++++++------ changelog.d/892.change.rst | 1 - changelog.d/895.change.rst | 1 - changelog.d/896.change.rst | 1 - src/attr/__init__.py | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) delete mode 100644 changelog.d/892.change.rst delete mode 100644 changelog.d/895.change.rst delete mode 100644 changelog.d/896.change.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a9f606af..1d194add2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,16 +17,23 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. -Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. +.. towncrier release notes start + +21.4.0 (2021-12-29) +------------------- + +Changes +^^^^^^^ -.. - Do *NOT* add changelog entries here! +- Fixed the test suite on PyPy3.8 where ``cloudpickle`` does not work. + `#892 `_ +- Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. + `#895 `_, + `#896 `_ - This changelog is managed by towncrier and is compiled at release time. - See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. +---- -.. towncrier release notes start 21.3.0 (2021-12-28) ------------------- diff --git a/changelog.d/892.change.rst b/changelog.d/892.change.rst deleted file mode 100644 index 78ac31c51..000000000 --- a/changelog.d/892.change.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed the test suite on PyPy3.8 where ``cloudpickle`` does not work. diff --git a/changelog.d/895.change.rst b/changelog.d/895.change.rst deleted file mode 100644 index ed8d60d7e..000000000 --- a/changelog.d/895.change.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. diff --git a/changelog.d/896.change.rst b/changelog.d/896.change.rst deleted file mode 100644 index ed8d60d7e..000000000 --- a/changelog.d/896.change.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 66bbb4bf2..f95c96dd5 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -24,7 +24,7 @@ from ._version_info import VersionInfo -__version__ = "21.4.0.dev0" +__version__ = "21.4.0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From 0c462938e1c74e0e0267daf8ebdcc0fe3053134d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 14:15:10 +0100 Subject: [PATCH 40/60] Start new development cycle --- CHANGELOG.rst | 9 +++++++++ src/attr/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d194add2..2ea83e4da 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,15 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. +Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. + +.. + Do *NOT* add changelog entries here! + + This changelog is managed by towncrier and is compiled at release time. + + See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. + .. towncrier release notes start 21.4.0 (2021-12-29) diff --git a/src/attr/__init__.py b/src/attr/__init__.py index f95c96dd5..1981d24d4 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -24,7 +24,7 @@ from ._version_info import VersionInfo -__version__ = "21.4.0" +__version__ = "21.5.0.dev0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From 19dd02314e85bc707e47e0a71777514d69e00104 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 29 Dec 2021 20:04:27 +0100 Subject: [PATCH 41/60] Remove stray a --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 709bba83d..bd5f3e259 100644 --- a/README.rst +++ b/README.rst @@ -69,7 +69,7 @@ After *declaring* your attributes ``attrs`` gives you: - a concise and explicit overview of the class's attributes, - a nice human-readable ``__repr__``, -- a equality-checking methods, +- equality-checking methods, - an initializer, - and much more, From 7cfa3b68c581c6a0f924933eacf7781d61bbf3b2 Mon Sep 17 00:00:00 2001 From: "Kian Meng, Ang" Date: Fri, 31 Dec 2021 16:38:37 +0800 Subject: [PATCH 42/60] Fix typos (#899) --- CHANGELOG.rst | 4 ++-- docs/hashing.rst | 2 +- docs/init.rst | 2 +- src/attr/_make.py | 2 +- tests/test_make.py | 2 +- tests/test_slots.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ea83e4da..8ed9bf7e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -596,7 +596,7 @@ Changes `#349 `_ - The order of attributes that are passed into ``attr.make_class()`` or the *these* argument of ``@attr.s()`` is now retained if the dictionary is ordered (i.e. ``dict`` on Python 3.6 and later, ``collections.OrderedDict`` otherwise). - Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programatically. + Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programmatically. `#300 `_, `#339 `_, @@ -608,7 +608,7 @@ Changes - Setting the cell type is now completely best effort. This fixes ``attrs`` on Jython. - We cannot make any guarantees regarding Jython though, because our test suite cannot run due to dependency incompatabilities. + We cannot make any guarantees regarding Jython though, because our test suite cannot run due to dependency incompatibilities. `#321 `_, `#334 `_ diff --git a/docs/hashing.rst b/docs/hashing.rst index 30888f97b..3e90f8cba 100644 --- a/docs/hashing.rst +++ b/docs/hashing.rst @@ -54,7 +54,7 @@ Because according to the definition_ from the official Python docs, the returned #. The hash of an object **must not** change. - If you create a class with ``@attr.s(frozen=True)`` this is fullfilled by definition, therefore ``attrs`` will write a ``__hash__`` function for you automatically. + If you create a class with ``@attr.s(frozen=True)`` this is fulfilled by definition, therefore ``attrs`` will write a ``__hash__`` function for you automatically. You can also force it to write one with ``hash=True`` but then it's *your* responsibility to make sure that the object is not mutated. This point is the reason why mutable structures like lists, dictionaries, or sets aren't hashable while immutable ones like tuples or frozensets are: diff --git a/docs/init.rst b/docs/init.rst index fb276ded8..0c3f4cd79 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -345,7 +345,7 @@ And for that ``attrs`` offers three means: Pre Init ~~~~~~~~ -The sole reason for the existance of ``__attrs_pre_init__`` is to give users the chance to call ``super().__init__()``, because some subclassing-based APIs require that. +The sole reason for the existence of ``__attrs_pre_init__`` is to give users the chance to call ``super().__init__()``, because some subclassing-based APIs require that. .. doctest:: diff --git a/src/attr/_make.py b/src/attr/_make.py index d46f8a3e7..072d5ff81 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -868,7 +868,7 @@ def _create_slots_class(self): slot_names = [name for name in names if name not in base_names] # There are slots for attributes from current class # that are defined in parent classes. - # As their descriptors may be overriden by a child class, + # As their descriptors may be overridden by a child class, # we collect them here and update the class dict reused_slots = { slot: slot_descriptor diff --git a/tests/test_make.py b/tests/test_make.py index 729d3a71f..498c98aa4 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -235,7 +235,7 @@ def test_kw_only(self): """ Converts all attributes, including base class' attributes, if `kw_only` is provided. Therefore, `kw_only` allows attributes with defaults to - preceed mandatory attributes. + precede mandatory attributes. Updates in the subclass *don't* affect the base class attributes. """ diff --git a/tests/test_slots.py b/tests/test_slots.py index baf9a40dd..b560f2332 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -490,7 +490,7 @@ def statmethod(): def test_code_hack_failure(self, monkeypatch): """ Keeps working if function/code object introspection doesn't work - on this (nonstandard) interpeter. + on this (nonstandard) interpreter. A warning is emitted that points to the actual code. """ From c162c783dceffa2aec15acb945d23b7e0b730a0b Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 1 Jan 2022 15:26:50 +0100 Subject: [PATCH 43/60] Qualify claim w/ link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bd5f3e259..c2e5cf309 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ Please check out `On The Core API Names `_ of ``attrs``). In practice it does a lot more and is more flexible. For instance it allows you to define `special handling of NumPy arrays for equality checks `_, or allows more ways to `plug into the initialization process `_. From 3df7bbe089658ecf1dcc21308926bfd9133b12e1 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 3 Jan 2022 14:39:41 +0100 Subject: [PATCH 44/60] Speedup uses of `object.__setattr__` (#898) * Speedup uses of `object.__setattr__` * Add changelog fragment --- changelog.d/898.change.rst | 1 + docs/how-does-it-work.rst | 10 +++++----- src/attr/_make.py | 24 +++++++----------------- tests/test_functional.py | 5 ++--- 4 files changed, 15 insertions(+), 25 deletions(-) create mode 100644 changelog.d/898.change.rst diff --git a/changelog.d/898.change.rst b/changelog.d/898.change.rst new file mode 100644 index 000000000..bbd2d8bee --- /dev/null +++ b/changelog.d/898.change.rst @@ -0,0 +1 @@ +Speedup instantiation of frozen slotted classes. diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index f89974054..c7b408341 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -87,16 +87,16 @@ This is (still) slower than a plain assignment: $ pyperf timeit --rigorous \ -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \ "C(1, 2, 3)" - ........................................ - Median +- std dev: 378 ns +- 12 ns + ......................................... + Mean +- std dev: 228 ns +- 18 ns $ pyperf timeit --rigorous \ -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \ "C(1, 2, 3)" - ........................................ - Median +- std dev: 676 ns +- 16 ns + ......................................... + Mean +- std dev: 450 ns +- 26 ns -So on a laptop computer the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds). +So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds). It's certainly something you'll feel in a hot loop but shouldn't matter in normal code. Pick what's more important to you. diff --git a/src/attr/_make.py b/src/attr/_make.py index 072d5ff81..915c5e663 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -807,7 +807,7 @@ def _patch_original_class(self): cls.__attrs_own_setattr__ = False if not self._has_custom_setattr: - cls.__setattr__ = object.__setattr__ + cls.__setattr__ = _obj_setattr return cls @@ -835,7 +835,7 @@ def _create_slots_class(self): if not self._has_custom_setattr: for base_cls in self._cls.__bases__: if base_cls.__dict__.get("__attrs_own_setattr__", False): - cd["__setattr__"] = object.__setattr__ + cd["__setattr__"] = _obj_setattr break # Traverse the MRO to collect existing slots @@ -2159,7 +2159,6 @@ def _make_init( cache_hash, base_attr_map, is_exc, - needs_cached_setattr, has_cls_on_setattr, attrs_init, ) @@ -2172,7 +2171,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_cached_setattr"] = _obj_setattr + globs["_setattr"] = _obj_setattr init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2189,7 +2188,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr('%s', %s)" % (attr_name, value_var) + return "_setattr(self, '%s', %s)" % (attr_name, value_var) def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2197,7 +2196,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr): Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """ - return "_setattr('%s', %s(%s))" % ( + return "_setattr(self, '%s', %s(%s))" % ( attr_name, _init_converter_pat % (attr_name,), value_var, @@ -2296,7 +2295,6 @@ def _attrs_to_init_script( cache_hash, base_attr_map, is_exc, - needs_cached_setattr, has_cls_on_setattr, attrs_init, ): @@ -2312,14 +2310,6 @@ def _attrs_to_init_script( if pre_init: lines.append("self.__attrs_pre_init__()") - if needs_cached_setattr: - lines.append( - # Circumvent the __setattr__ descriptor to save one lookup per - # assignment. - # Note _setattr will be used again below if cache_hash is True - "_setattr = _cached_setattr.__get__(self, self.__class__)" - ) - if frozen is True: if slots is True: fmt_setter = _setattr @@ -2535,7 +2525,7 @@ def fmt_setter_with_converter( if post_init: lines.append("self.__attrs_post_init__()") - # because this is set only after __attrs_post_init is called, a crash + # because this is set only after __attrs_post_init__ is called, a crash # will result if post-init tries to access the hash code. This seemed # preferable to setting this beforehand, in which case alteration to # field values during post-init combined with post-init accessing the @@ -2544,7 +2534,7 @@ def fmt_setter_with_converter( if frozen: if slots: # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr('%s', %s)" + init_hash_cache = "_setattr(self, '%s', %s)" else: # if frozen and not slots, then _inst_dict defined above init_hash_cache = "_inst_dict['%s'] = %s" diff --git a/tests/test_functional.py b/tests/test_functional.py index 9b6a27e2f..6bb989f66 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -784,7 +784,6 @@ class D(C): src = inspect.getsource(D.__init__) - assert "_setattr = _cached_setattr" in src - assert "_setattr('x', x)" in src - assert "_setattr('y', y)" in src + assert "_setattr(self, 'x', x)" in src + assert "_setattr(self, 'y', y)" in src assert object.__setattr__ != D.__setattr__ From f3dfe96b48a3d7718f3c1ac04cbac090f290ed16 Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Tue, 4 Jan 2022 04:46:36 -0800 Subject: [PATCH 45/60] docs: fix typos and some grammar (#900) Co-authored-by: Hynek Schlawack --- docs/examples.rst | 2 +- docs/hashing.rst | 4 ++-- docs/init.rst | 2 +- docs/names.rst | 6 +++--- docs/overview.rst | 2 +- docs/python-2.rst | 2 +- docs/types.rst | 4 ++-- docs/why.rst | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index ba5343d4a..08bc52838 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -166,7 +166,7 @@ Keyword-only attributes allow subclasses to add attributes without default value ... TypeError: B() missing 1 required keyword-only argument: 'b' -If you don't set ``kw_only=True``, then there's is no valid attribute ordering and you'll get an error: +If you don't set ``kw_only=True``, then there is no valid attribute ordering, and you'll get an error: .. doctest:: diff --git a/docs/hashing.rst b/docs/hashing.rst index 3e90f8cba..1fe943fc7 100644 --- a/docs/hashing.rst +++ b/docs/hashing.rst @@ -47,7 +47,7 @@ Because according to the definition_ from the official Python docs, the returned The easiest way to reset ``__hash__`` on a class is adding ``__hash__ = object.__hash__`` in the class body. -#. If two object are not equal, their hash **should** be different. +#. If two objects are not equal, their hash **should** be different. While this isn't a requirement from a standpoint of correctness, sets and dicts become less effective if there are a lot of identical hashes. The worst case is when all objects have the same hash which turns a set into a list. @@ -80,7 +80,7 @@ If such objects are to be stored in hash-based collections, it can be useful to To enable caching of hash codes, pass ``cache_hash=True`` to ``@attrs``. This may only be done if ``attrs`` is already generating a hash function for the object. -.. [#fn1] The hash is computed by hashing a tuple that consists of an unique id for the class plus all attribute values. +.. [#fn1] The hash is computed by hashing a tuple that consists of a unique id for the class plus all attribute values. .. _definition: https://docs.python.org/3/glossary.html#term-hashable .. _`Python Hashes and Equality`: https://hynek.me/articles/hashes-and-equality/ diff --git a/docs/init.rst b/docs/init.rst index 0c3f4cd79..231577faf 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -448,7 +448,7 @@ Derived Attributes One of the most common ``attrs`` questions on *Stack Overflow* is how to have attributes that depend on other attributes. For example if you have an API token and want to instantiate a web client that uses it for authentication. -Based on the previous sections, there's two approaches. +Based on the previous sections, there are two approaches. The simpler one is using ``__attrs_post_init__``:: diff --git a/docs/names.rst b/docs/names.rst index 0fe953e6a..1db82fb42 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -43,7 +43,7 @@ In the wake of all of that, `glyph `_ and `Hynek `_ in Python 3.6 and Guido felt like it would be a good mechanic to introduce something similar to ``attrs`` to the Python standard library. The result, of course, was `PEP 557 `_\ [#stdlib]_ which eventually became the `dataclasses` module in Python 3.7. -``attrs`` at this point was lucky to have several people on board who were also very excited about type annotations and helped implementing it; including a `Mypy plugin `_. +``attrs`` at this point was lucky to have several people on board who were also very excited about type annotations and helped implement it; including a `Mypy plugin `_. And so it happened that ``attrs`` `shipped `_ the new method of defining classes more than half a year before Python 3.7 -- and thus `dataclasses` -- were released. ----- @@ -90,7 +90,7 @@ We're determined to serve both. ^^^^^^^^^^^^^ Over its existence, ``attrs`` never stood still. -But since we also greatly care about backward compatibility and not breaking our users's code, many features and niceties have to be manually activated. +But since we also greatly care about backward compatibility and not breaking our users' code, many features and niceties have to be manually activated. That is not only annoying, it also leads to the problem that many of ``attrs``'s users don't even know what it can do for them. We've spent years alone explaining that defining attributes using type annotations is in no way unique to `dataclasses`. diff --git a/docs/overview.rst b/docs/overview.rst index b35f66f2d..2d7302c74 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -49,7 +49,7 @@ What ``attrs`` Is Not All ``attrs`` does is: -1. take your declaration, +1. Take your declaration, 2. write :term:`dunder methods` based on that information, 3. and attach them to your class. diff --git a/docs/python-2.rst b/docs/python-2.rst index 7ec9e5112..863c6b034 100644 --- a/docs/python-2.rst +++ b/docs/python-2.rst @@ -12,7 +12,7 @@ Feasibility in this case means: 1. Possibility to run the tests on our development computers, 2. and **free** CI options. -This can mean that we will have to run our tests on PyPy, whose maintainters have unequivocally declared that they do not intend to stop the development and maintenance of their Python 2-compatible line at all. +This can mean that we will have to run our tests on PyPy, whose maintainers have unequivocally declared that they do not intend to stop the development and maintenance of their Python 2-compatible line at all. And this can mean that at some point, a sponsor will have to step up and pay for bespoke CI setups. **However**: there is no promise of new features coming to ``attrs`` running under Python 2. diff --git a/docs/types.rst b/docs/types.rst index fbb90a7e9..20f9f5fd8 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -22,7 +22,7 @@ You can choose freely between the approaches, but please remember that if you ch ---- -Even when going all-in an type annotations, you will need `attr.field` for some advanced features though. +Even when going all-in on type annotations, you will need `attr.field` for some advanced features though. One of those features are the decorator-based features like defaults. It's important to remember that ``attrs`` doesn't do any magic behind your back. @@ -42,7 +42,7 @@ If you need to resolve these to real types, you can call `attrs.resolve_types` w In practice though, types show their biggest usefulness in combination with tools like mypy_, pytype_, or pyright_ that have dedicated support for ``attrs`` classes. -The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you writing *correct* and *verified self-documenting* code. +The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you write *correct* and *verified self-documenting* code. If you don't know where to start, Carl Meyer gave a great talk on `Type-checked Python in the Real World `_ at PyCon US 2018 that will help you to get started in no time. diff --git a/docs/why.rst b/docs/why.rst index 2c0ca4cd6..dbfed3d71 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -39,7 +39,7 @@ Basically what ``attrs`` was in 2015. …pydantic? ---------- -*pydantic* is first an foremost a *data validation library*. +*pydantic* is first and foremost a *data validation library*. As such, it is a capable complement to class building libraries like ``attrs`` (or Data Classes!) for parsing and validating untrusted data. However, as convenient as it might be, using it for your business or data layer `is problematic in several ways `_: @@ -89,7 +89,7 @@ Other often surprising behaviors include: - Iterability also implies that it's easy to accidentally unpack a ``namedtuple`` which leads to hard-to-find bugs. [#iter]_ - ``namedtuple``\ s have their methods *on your instances* whether you like it or not. [#pollution]_ - ``namedtuple``\ s are *always* immutable. - Not only does that mean that you can't decide for yourself whether your instances should be immutable or not, it also means that if you want to influence your class' initialization (validation? default values?), you have to implement :meth:`__new__() ` which is a particularly hacky and error-prone requirement for a very common problem. [#immutable]_ + Not only does that mean that you can't decide for yourself whether your instances should be immutable or not, it also means that if you want to influence your class' initialization (validation? default values?), you have to implement :meth:`__new__() ` which is a particularly hacky and error-prone requirement for a very common problem. [#immutable]_ - To attach methods to a ``namedtuple`` you have to subclass it. And if you follow the standard library documentation's recommendation of:: From 5f36ba9b89d4d196f80147d4f2961fb2f97ae2e5 Mon Sep 17 00:00:00 2001 From: Aaron Stephens Date: Fri, 7 Jan 2022 01:28:34 -0800 Subject: [PATCH 46/60] typos (#903) --- tests/test_make.py | 2 +- tests/test_slots.py | 2 +- tests/test_validators.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index 498c98aa4..6cc4f059f 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -310,7 +310,7 @@ class C(object): def test_multiple_inheritance_old(self): """ - Old multiple inheritance attributre collection behavior is retained. + Old multiple inheritance attribute collection behavior is retained. See #285 """ diff --git a/tests/test_slots.py b/tests/test_slots.py index b560f2332..91697a741 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -717,7 +717,7 @@ def f(self): @pytest.mark.skipif(PY2, reason="shortcut super() is PY3-only.") -def test_slots_super_property_get_shurtcut(): +def test_slots_super_property_get_shortcut(): """ On Python 3, the `super()` shortcut is allowed. """ diff --git a/tests/test_validators.py b/tests/test_validators.py index d7c6de8ba..38c39f348 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -64,7 +64,7 @@ def test_default(self): assert _config._run_validators is True @pytest.mark.parametrize("value, expected", [(True, False), (False, True)]) - def test_set_validators_diabled(self, value, expected): + def test_set_validators_disabled(self, value, expected): """ Sets `_run_validators`. """ From 9727008fd1e40bc55cdc6aee71e0f61553f33127 Mon Sep 17 00:00:00 2001 From: Laurent Kadian <17257425+lkadian@users.noreply.github.com> Date: Mon, 10 Jan 2022 23:58:47 -0500 Subject: [PATCH 47/60] fix type annotation in init.rst example (#905) --- docs/init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/init.rst b/docs/init.rst index 231577faf..4404829f3 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -478,7 +478,7 @@ That said, and as pointed out in the beginning of the chapter, a better approach client: WebClient @classmethod - def from_token(cls, token: str) -> SomeClass: + def from_token(cls, token: str) -> "APIClient": return cls(client=WebClient(token)) This makes the class more testable. From e188af7bc4df99eb84772b0ad0f80c4e11f740b4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 21 Jan 2022 07:22:58 +0100 Subject: [PATCH 48/60] Fix copy-pasted line in docs Ref https://github.com/python-attrs/attrs/pull/887#discussion_r789198858 --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 02aed52ad..89c9de95d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -441,7 +441,7 @@ Validators ---------- ``attrs`` comes with some common validators in the ``attrs.validators`` module. -All objects from ``attrs.converters`` are also available from ``attr.converters``. +All objects from ``attrs.validators`` are also available from ``attr.validators``. .. autofunction:: attrs.validators.lt From 41afe6e9bf08162eb81eaf525474303f277e3ba8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 23 Jan 2022 14:00:56 +0100 Subject: [PATCH 49/60] Add 2016 disclaimer --- docs/why.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/why.rst b/docs/why.rst index dbfed3d71..62c0394bf 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -2,7 +2,7 @@ Why not… ======== -If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs `_! +If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs `_. It predates type annotations and hence Data Classes, but it masterfully illustrates the appeal of class-building packages. …Data Classes? From 5a21484d429e3ab878864f5eb5a3df34dc632777 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 23 Jan 2022 14:39:28 +0100 Subject: [PATCH 50/60] Expand __slots__ why example --- docs/why.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/why.rst b/docs/why.rst index 62c0394bf..40f2f1b5b 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -24,9 +24,9 @@ Whether they're relevant to *you* depends on your circumstances: - ``attrs`` doesn't force type annotations on you if you don't like them. - But since it **also** supports typing, it's the best way to embrace type hints *gradually*, too. - While Data Classes are implementing features from ``attrs`` every now and then, their presence is dependent on the Python version, not the package version. - For example, support for ``__slots__`` has only been added in Python 3.10. - That is especially painful for PyPI packages that support multiple Python versions. - This includes possible implementation bugs. + For example, support for ``__slots__`` has only been added in Python 3.10, but it doesn’t do cell rewriting and therefore doesn’t support bare calls to ``super()``. + This may or may not be fixed in later Python releases, but handling all these differences is especially painful for PyPI packages that support multiple Python versions. + And of course, this includes possible implementation bugs. - ``attrs`` can and will move faster. We are not bound to any release schedules and we have a clear deprecation policy. From 69a77910e4224e2832cd0109aaa416e8ffab60cc Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sat, 29 Jan 2022 14:12:08 +0000 Subject: [PATCH 51/60] Remove incorrect statement in documentation (#915) Both `attr.define` and `attr.s` have `match_args=True` by default. --- src/attr/_next_gen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 068253688..87bcef830 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -58,7 +58,6 @@ def define( - *auto_exc=True* - *auto_detect=True* - *order=False* - - *match_args=True* - Some options that were only relevant on Python 2 or were kept around for backwards-compatibility have been removed. From 976b8287703d781590d457dffc8b581a244b09d5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 29 Jan 2022 15:27:56 +0100 Subject: [PATCH 52/60] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 88f6415e9..4133e06b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -27,6 +27,7 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file. - [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). - [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d). +- [ ] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you.