From 932902c29fd813a414697fde4c40b0a7b072a83d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 7 May 2021 11:35:44 +0200 Subject: [PATCH 001/139] Start new 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 c7d0ed88c..dcb4823ca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,15 @@ Changelog Versions follow `CalVer `_ with a strict backwards compatibility policy. The third digit is only for regressions. +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://www.attrs.org/en/latest/contributing.html#changelog for details. + .. towncrier release notes start 21.2.0 (2021-05-07) diff --git a/src/attr/__init__.py b/src/attr/__init__.py index b1ce7fe24..3a037a492 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -22,7 +22,7 @@ from ._version_info import VersionInfo -__version__ = "21.2.0" +__version__ = "21.3.0.dev0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From 8ae2d6f1c3c067c941fd9d212e52972c7d3382cc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 9 May 2021 07:07:12 +0200 Subject: [PATCH 002/139] Fix typo --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index a1912b123..ceb08f910 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1369,7 +1369,7 @@ def attrs( :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by - default but is kept off for backward-compatability. + default but is kept off for backward-compatibility. See issue `#428 `_ for more details. From 20c2d4fc0c72a844e12899d5d5e0adbc4741ce2c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 17 May 2021 09:24:46 +0200 Subject: [PATCH 003/139] Optimize the case of on_setattr=validate & no validators (#817) * Optimize the case of on_setattr=validate & no validators This is important because define/mutable have on_setattr=setters.validate on default. Fixes #816 Signed-off-by: Hynek Schlawack * Grammar --- changelog.d/817.change.rst | 1 + src/attr/_make.py | 42 +++++++++++++++++++++------------- tests/test_dunders.py | 2 +- tests/test_functional.py | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 changelog.d/817.change.rst diff --git a/changelog.d/817.change.rst b/changelog.d/817.change.rst new file mode 100644 index 000000000..3a53efc74 --- /dev/null +++ b/changelog.d/817.change.rst @@ -0,0 +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. diff --git a/src/attr/_make.py b/src/attr/_make.py index ceb08f910..f14369a99 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -654,7 +654,7 @@ class _ClassBuilder(object): "_on_setattr", "_slots", "_weakref_slot", - "_has_own_setattr", + "_wrote_own_setattr", "_has_custom_setattr", ) @@ -701,7 +701,7 @@ def __init__( self._on_setattr = on_setattr self._has_custom_setattr = has_custom_setattr - self._has_own_setattr = False + self._wrote_own_setattr = False self._cls_dict["__attrs_attrs__"] = self._attrs @@ -709,7 +709,15 @@ def __init__( self._cls_dict["__setattr__"] = _frozen_setattrs self._cls_dict["__delattr__"] = _frozen_delattrs - self._has_own_setattr = True + self._wrote_own_setattr = True + elif on_setattr == setters.validate: + for a in attrs: + if a.validator is not None: + break + else: + # If class-level on_setattr is set to validating, but there's + # no field to validate, pretend like there's no on_setattr. + self._on_setattr = None if getstate_setstate: ( @@ -759,7 +767,7 @@ def _patch_original_class(self): # If we've inherited an attrs __setattr__ and don't write our own, # reset it to object's. - if not self._has_own_setattr and getattr( + if not self._wrote_own_setattr and getattr( cls, "__attrs_own_setattr__", False ): cls.__attrs_own_setattr__ = False @@ -787,7 +795,7 @@ def _create_slots_class(self): # XXX: a non-attrs class and subclass the resulting class with an attrs # XXX: class. See `test_slotted_confused` for details. For now that's # XXX: OK with us. - if not self._has_own_setattr: + if not self._wrote_own_setattr: cd["__attrs_own_setattr__"] = False if not self._has_custom_setattr: @@ -958,8 +966,7 @@ def add_init(self): self._cache_hash, self._base_attr_map, self._is_exc, - self._on_setattr is not None - and self._on_setattr is not setters.NO_OP, + self._on_setattr, attrs_init=False, ) ) @@ -978,8 +985,7 @@ def add_attrs_init(self): self._cache_hash, self._base_attr_map, self._is_exc, - self._on_setattr is not None - and self._on_setattr is not setters.NO_OP, + self._on_setattr, attrs_init=True, ) ) @@ -1038,7 +1044,7 @@ def __setattr__(self, name, val): self._cls_dict["__attrs_own_setattr__"] = True self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) - self._has_own_setattr = True + self._wrote_own_setattr = True return self @@ -2008,10 +2014,14 @@ def _make_init( cache_hash, base_attr_map, is_exc, - has_global_on_setattr, + cls_on_setattr, attrs_init, ): - if frozen and has_global_on_setattr: + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: raise ValueError("Frozen classes can't use on_setattr.") needs_cached_setattr = cache_hash or frozen @@ -2030,7 +2040,7 @@ def _make_init( needs_cached_setattr = True elif ( - has_global_on_setattr and a.on_setattr is not setters.NO_OP + has_cls_on_setattr and a.on_setattr is not setters.NO_OP ) or _is_slot_attr(a.name, base_attr_map): needs_cached_setattr = True @@ -2046,7 +2056,7 @@ def _make_init( base_attr_map, is_exc, needs_cached_setattr, - has_global_on_setattr, + has_cls_on_setattr, attrs_init, ) if cls.__module__ in sys.modules: @@ -2183,7 +2193,7 @@ def _attrs_to_init_script( base_attr_map, is_exc, needs_cached_setattr, - has_global_on_setattr, + has_cls_on_setattr, attrs_init, ): """ @@ -2257,7 +2267,7 @@ def fmt_setter_with_converter( attr_name = a.name has_on_setattr = a.on_setattr is not None or ( - a.on_setattr is not setters.NO_OP and has_global_on_setattr + a.on_setattr is not setters.NO_OP and has_cls_on_setattr ) arg_name = a.name.lstrip("_") diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 70f26d044..ba8f3ce89 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -94,7 +94,7 @@ def _add_init(cls, frozen): cache_hash=False, base_attr_map={}, is_exc=False, - has_global_on_setattr=False, + cls_on_setattr=None, attrs_init=False, ) return cls diff --git a/tests/test_functional.py b/tests/test_functional.py index a17023ffc..e9aa3c267 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import inspect import pickle from copy import deepcopy @@ -687,3 +688,48 @@ class C(object): "2021-06-01. Please use `eq` and `order` instead." == w.message.args[0] ) + + @pytest.mark.parametrize("slots", [True, False]) + def test_no_setattr_if_validate_without_validators(self, slots): + """ + If a class has on_setattr=attr.setters.validate (default in NG APIs) + but sets no validators, don't use the (slower) setattr in __init__. + + Regression test for #816. + """ + + @attr.s(on_setattr=attr.setters.validate) + class C(object): + x = attr.ib() + + @attr.s(on_setattr=attr.setters.validate) + class D(C): + y = attr.ib() + + src = inspect.getsource(D.__init__) + + assert "setattr" not in src + assert "self.x = x" in src + assert "self.y = y" in src + assert object.__setattr__ == D.__setattr__ + + def test_on_setattr_detect_inherited_validators(self): + """ + _make_init detects the presence of a validator even if the field is + inherited. + """ + + @attr.s(on_setattr=attr.setters.validate) + class C(object): + x = attr.ib(validator=42) + + @attr.s(on_setattr=attr.setters.validate) + class D(C): + y = attr.ib() + + src = inspect.getsource(D.__init__) + + assert "_setattr = _cached_setattr" in src + assert "_setattr('x', x)" in src + assert "_setattr('y', y)" in src + assert object.__setattr__ != D.__setattr__ From 8613af97bb9b81526ca1e8385f1f48916f607588 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Tue, 18 May 2021 10:32:06 +0530 Subject: [PATCH 004/139] Add __match_args__ to support match case destructuring in Python 3.10 (#815) * Add support to generate __match_args__ for Python 3.10. * Add versionadded directive. * Update stubs. * Update changelog and add a test to typing examples. * Fix error regarding new-style classes in Python 2. * Fix lint error regarding line length. * Fix lint error regarding trailing whitespace. * Add docstrings for interrogate. * Use _has_own_attribute instead of cls.__dict__ contains check. * Update docs as per review comments. * Revert mistaken changelog update. * Add Python 3.10 pattern matching syntax test cases. * Update define signature with match_args. * Fix conftest formatting. * Fix isort formatting. * Bump to Python 3.10 to parse syntax. * Bump basepython of lint to Python 3.10 for parsing. * Move lint to py310 Co-authored-by: Hynek Schlawack --- .pre-commit-config.yaml | 7 +- changelog.d/815.changes.rst | 3 + conftest.py | 2 + docs/api.rst | 2 +- pyproject.toml | 4 ++ src/attr/__init__.pyi | 4 ++ src/attr/_make.py | 18 +++++ src/attr/_next_gen.py | 2 + tests/test_make.py | 118 +++++++++++++++++++++++++++++++++ tests/test_pattern_matching.py | 98 +++++++++++++++++++++++++++ tests/typing_example.py | 7 ++ tox.ini | 6 +- 12 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 changelog.d/815.changes.rst create mode 100644 tests/test_pattern_matching.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb59b3a36..6d042f496 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: rev: 20.8b1 hooks: - id: black + exclude: tests/test_pattern_matching.py language_version: python3.8 - repo: https://github.com/PyCQA/isort @@ -16,14 +17,15 @@ repos: rev: 3.8.4 hooks: - id: flake8 - language_version: python3.8 + language_version: python3.10 - repo: https://github.com/econchick/interrogate rev: 1.3.2 hooks: - id: interrogate + exclude: tests/test_pattern_matching.py args: [tests] - language_version: python3.8 + language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 @@ -31,4 +33,5 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements + language_version: python3.10 - id: check-toml diff --git a/changelog.d/815.changes.rst b/changelog.d/815.changes.rst new file mode 100644 index 000000000..a8f16b907 --- /dev/null +++ b/changelog.d/815.changes.rst @@ -0,0 +1,3 @@ +``__match_args__`` are now generated to support Python 3.10's +`Structural Pattern Matching `_. +This can be controlled by ``match_args`` argument to the class decorators. diff --git a/conftest.py b/conftest.py index a2c8d59f2..85659a8a2 100644 --- a/conftest.py +++ b/conftest.py @@ -23,6 +23,8 @@ def pytest_configure(config): "tests/test_next_gen.py", ] ) +if sys.version_info[:2] < (3, 10): + collect_ignore.extend(["tests/test_pattern_matching.py"]) if sys.version_info[:2] >= (3, 10): collect_ignore.extend( [ diff --git a/docs/api.rst b/docs/api.rst index 3df314504..3fd71d651 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -28,7 +28,7 @@ Core .. autodata:: attr.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) +.. 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) .. note:: diff --git a/pyproject.toml b/pyproject.toml index 14f65a366..93145c9e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,10 @@ exclude_lines = [ [tool.black] line-length = 79 +extend-exclude = ''' +# Exclude pattern matching test till black gains Python 3.10 support +.*test_pattern_matching.* +''' [tool.interrogate] diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 3503b073b..7221836c1 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -315,6 +315,7 @@ def attrs( getstate_setstate: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., ) -> _C: ... @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) @@ -341,6 +342,7 @@ def attrs( getstate_setstate: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., ) -> Callable[[_C], _C]: ... @overload @__dataclass_transform__(field_descriptors=(attrib, field)) @@ -365,6 +367,7 @@ def define( getstate_setstate: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., ) -> _C: ... @overload @__dataclass_transform__(field_descriptors=(attrib, field)) @@ -389,6 +392,7 @@ def define( getstate_setstate: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., ) -> Callable[[_C], _C]: ... mutable = define diff --git a/src/attr/_make.py b/src/attr/_make.py index f14369a99..82b0f7667 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -973,6 +973,13 @@ def add_init(self): return self + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + def add_attrs_init(self): self._cls_dict["__attrs_init__"] = self._add_method_dunders( _make_init( @@ -1198,6 +1205,7 @@ def attrs( getstate_setstate=None, on_setattr=None, field_transformer=None, + match_args=True, ): r""" A class decorator that adds `dunder @@ -1413,6 +1421,12 @@ def attrs( this, e.g., to automatically add converters or validators to fields based on their types. See `transform-fields` for more details. + :param bool match_args: + If `True`, set ``__match_args__`` on the class to support `PEP 634 + `_ (Structural Pattern + Matching). It is a tuple of all positional-only ``__init__`` parameter + names. + .. versionadded:: 16.0.0 *slots* .. versionadded:: 16.1.0 *frozen* .. versionadded:: 16.3.0 *str* @@ -1446,6 +1460,7 @@ def attrs( ``init=False`` injects ``__attrs_init__`` .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* """ if auto_detect and PY2: raise PythonTooOldError( @@ -1562,6 +1577,9 @@ def wrap(cls): " init must be True." ) + if match_args and not _has_own_attribute(cls, "__match_args__"): + builder.add_match_args() + return builder.build_class() # maybe_cls's type depends on the usage of the decorator. It's a class diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index fab0af966..1d8acac36 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -32,6 +32,7 @@ def define( getstate_setstate=None, on_setattr=None, field_transformer=None, + match_args=True, ): r""" The only behavioral differences are the handling of the *auto_attribs* @@ -72,6 +73,7 @@ def do_it(cls, auto_attribs): getstate_setstate=getstate_setstate, on_setattr=on_setattr, field_transformer=field_transformer, + match_args=match_args, ) def wrap(cls): diff --git a/tests/test_make.py b/tests/test_make.py index e54b0bb05..c1d893e3d 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -2327,3 +2327,121 @@ def __setstate__(self, state): assert True is i.called assert None is getattr(C(), "__getstate__", None) + + +class TestMatchArgs(object): + """ + Tests for match_args and __match_args__ generation. + """ + + def test_match_args(self): + """ + __match_args__ generation + """ + + @attr.s() + class C(object): + a = attr.ib() + + assert C.__match_args__ == ("a",) + + def test_explicit_match_args(self): + """ + __match_args__ manually set is not overriden. + """ + + ma = () + + @attr.s() + class C(object): + a = attr.ib() + __match_args__ = ma + + assert C(42).__match_args__ is ma + + @pytest.mark.parametrize("match_args", [True, False]) + def test_match_args_attr_set(self, match_args): + """ + __match_args__ being set depending on match_args. + """ + + @attr.s(match_args=match_args) + class C(object): + a = attr.ib() + + if match_args: + assert hasattr(C, "__match_args__") + else: + assert not hasattr(C, "__match_args__") + + def test_match_args_kw_only(self): + """ + kw_only being set doesn't generate __match_args__ + kw_only field is not included in __match_args__ + """ + + @attr.s() + class C(object): + a = attr.ib(kw_only=True) + b = attr.ib() + + assert C.__match_args__ == ("b",) + + @attr.s(match_args=True, kw_only=True) + class C(object): + a = attr.ib() + b = attr.ib() + + assert C.__match_args__ == () + + def test_match_args_argument(self): + """ + match_args being False with inheritance. + """ + + @attr.s(match_args=False) + class X(object): + a = attr.ib() + + assert "__match_args__" not in X.__dict__ + + @attr.s(match_args=False) + class Y(object): + a = attr.ib() + __match_args__ = ("b",) + + assert Y.__match_args__ == ("b",) + + @attr.s(match_args=False) + class Z(Y): + z = attr.ib() + + assert Z.__match_args__ == ("b",) + + @attr.s() + class A(object): + a = attr.ib() + z = attr.ib() + + @attr.s(match_args=False) + class B(A): + b = attr.ib() + + assert B.__match_args__ == ("a", "z") + + def test_make_class(self): + """ + match_args generation with make_class. + """ + + C1 = make_class("C1", ["a", "b"]) + assert C1.__match_args__ == ("a", "b") + + C1 = make_class("C1", ["a", "b"], match_args=False) + assert not hasattr(C1, "__match_args__") + + C1 = make_class("C1", ["a", "b"], kw_only=True) + assert C1.__match_args__ == () + + C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()}) + assert C1.__match_args__ == ("b",) diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py new file mode 100644 index 000000000..6a1cb84dc --- /dev/null +++ b/tests/test_pattern_matching.py @@ -0,0 +1,98 @@ +# flake8: noqa +# Python 3.10 issue in flake8 : https://github.com/PyCQA/pyflakes/issues/634 +import pytest + +import attr + +from attr._make import make_class + + +class TestPatternMatching(object): + """ + Pattern matching syntax test cases. + """ + + def test_simple_match_case(self): + """ + Simple match case statement + """ + + @attr.s() + class C(object): + a = attr.ib() + + assert C.__match_args__ == ("a",) + + matched = False + c = C(a=1) + match c: + case C(a): + matched = True + + assert matched + + def test_explicit_match_args(self): + """ + Manually set empty __match_args__ will not match. + """ + + ma = () + + @attr.s() + class C(object): + a = attr.ib() + __match_args__ = ma + + c = C(a=1) + + msg = r"C\(\) accepts 0 positional sub-patterns \(1 given\)" + with pytest.raises(TypeError, match=msg): + match c: + case C(a): + pass + + def test_match_args_kw_only(self): + """ + kw_only being set doesn't generate __match_args__ + kw_only field is not included in __match_args__ + """ + + @attr.s() + class C(object): + a = attr.ib(kw_only=True) + b = attr.ib() + + assert C.__match_args__ == ("b",) + + c = C(a=1, b=1) + msg = r"C\(\) accepts 1 positional sub-pattern \(2 given\)" + with pytest.raises(TypeError, match=msg): + match c: + case C(a, b): + pass + + found = False + match c: + case C(b, a=a): + found = True + + assert found + + @attr.s(match_args=True, kw_only=True) + class C(object): + a = attr.ib() + b = attr.ib() + + c = C(a=1, b=1) + msg = r"C\(\) accepts 0 positional sub-patterns \(2 given\)" + with pytest.raises(TypeError, match=msg): + match c: + case C(a, b): + pass + + found = False + match c: + case C(a=a, b=b): + found = True + + assert found diff --git a/tests/typing_example.py b/tests/typing_example.py index 13b5638db..2edbce216 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -257,3 +257,10 @@ class FactoryTest: a: List[int] = attr.ib(default=attr.Factory(list)) b: List[Any] = attr.ib(default=attr.Factory(list, False)) c: List[int] = attr.ib(default=attr.Factory((lambda s: s.a), True)) + + +# Check match_args stub +@attr.s(match_args=False) +class MatchArgs: + a: int = attr.ib() + b: int = attr.ib() diff --git a/tox.ini b/tox.ini index a9f67b296..d796d0383 100644 --- a/tox.ini +++ b/tox.ini @@ -14,9 +14,9 @@ python = 3.5: py35 3.6: py36 3.7: py37, docs - 3.8: py38, lint, manifest, typing, changelog + 3.8: py38, manifest, typing, changelog 3.9: py39, pyright - 3.10: py310 + 3.10: py310, lint pypy2: pypy2 pypy3: pypy3 @@ -71,7 +71,7 @@ commands = [testenv:lint] -basepython = python3.8 +basepython = python3.10 skip_install = true deps = pre-commit From 6731eea0edb26569892ae258a25bb15f80a50c21 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 18 May 2021 11:28:21 +0200 Subject: [PATCH 005/139] Run Python 3.10 under coverage (#818) Since the matching code is Python 3.10-specific, we run it under coverage. --- .github/workflows/main.yml | 6 +++--- conftest.py | 6 ++++-- src/attr/_compat.py | 1 + tox.ini | 15 +++++++++++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf4ea0775..a33efec2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,12 +13,12 @@ jobs: name: "Python ${{ matrix.python-version }}" runs-on: "ubuntu-latest" env: - USING_COVERAGE: "2.7,3.7,3.8" + USING_COVERAGE: "2.7,3.7,3.8,3.10.0-beta - 3.10" strategy: fail-fast: false matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-alpha - 3.10", "pypy2", "pypy3"] + python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-beta - 3.10", "pypy2", "pypy3"] steps: - uses: "actions/checkout@v2" @@ -40,7 +40,7 @@ jobs: # parsing errors in older versions for modern code. - uses: "actions/setup-python@v2" with: - python-version: "3.9" + python-version: "3.10.0-beta - 3.10" - name: "Combine coverage" run: | diff --git a/conftest.py b/conftest.py index 85659a8a2..79806cb16 100644 --- a/conftest.py +++ b/conftest.py @@ -4,6 +4,8 @@ from hypothesis import HealthCheck, settings +from attr._compat import PY310 + def pytest_configure(config): # HealthCheck.too_slow causes more trouble than good -- especially in CIs. @@ -23,9 +25,9 @@ def pytest_configure(config): "tests/test_next_gen.py", ] ) -if sys.version_info[:2] < (3, 10): +if not PY310: collect_ignore.extend(["tests/test_pattern_matching.py"]) -if sys.version_info[:2] >= (3, 10): +if PY310: collect_ignore.extend( [ "tests/test_mypy.yml", diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 6939f338d..c218e2453 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -8,6 +8,7 @@ PY2 = sys.version_info[0] == 2 PYPY = platform.python_implementation() == "PyPy" +PY310 = sys.version_info[:2] >= (3, 10) if PYPY or sys.version_info[:2] >= (3, 6): diff --git a/tox.ini b/tox.ini index d796d0383..f134364c2 100644 --- a/tox.ini +++ b/tox.ini @@ -61,10 +61,21 @@ extras = {env:TOX_AP_TEST_EXTRAS:tests} commands = coverage run -m pytest {posargs} +[testenv:py310] +# Python 3.6+ has a number of compile-time warnings on invalid string escapes. +# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. +basepython = python3.10 +install_command = pip install --no-compile {opts} {packages} +setenv = + PYTHONWARNINGS=d +extras = {env:TOX_AP_TEST_EXTRAS:tests} +commands = coverage run -m pytest {posargs} + + [testenv:coverage-report] -basepython = python3.7 +basepython = python3.10 skip_install = true -deps = coverage[toml]>=5.0.2 +deps = coverage[toml]>=5.4 commands = coverage combine coverage report From 8c9a79680faacadef62d7143a1ac99307031a8f0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 18 May 2021 11:29:29 +0200 Subject: [PATCH 006/139] Minor polish to #815 Signed-off-by: Hynek Schlawack --- changelog.d/815.changes.rst | 3 +- src/attr/_make.py | 16 ++++-- tests/test_make.py | 95 +++++++++++++++++++--------------- tests/test_pattern_matching.py | 47 +++++++++-------- 4 files changed, 92 insertions(+), 69 deletions(-) diff --git a/changelog.d/815.changes.rst b/changelog.d/815.changes.rst index a8f16b907..e6c368453 100644 --- a/changelog.d/815.changes.rst +++ b/changelog.d/815.changes.rst @@ -1,3 +1,4 @@ ``__match_args__`` are now generated to support Python 3.10's `Structural Pattern Matching `_. -This can be controlled by ``match_args`` argument to the class decorators. +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/src/attr/_make.py b/src/attr/_make.py index 82b0f7667..ad17109db 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -13,6 +13,7 @@ from . import _config, setters from ._compat import ( PY2, + PY310, PYPY, isclass, iteritems, @@ -1422,10 +1423,11 @@ def attrs( fields based on their types. See `transform-fields` for more details. :param bool match_args: - If `True`, set ``__match_args__`` on the class to support `PEP 634 - `_ (Structural Pattern - Matching). It is a tuple of all positional-only ``__init__`` parameter - names. + If `True` (default), set ``__match_args__`` on the class to support + `PEP 634 `_ (Structural + Pattern Matching). It is a tuple of all positional-only ``__init__`` + parameter names on Python 3.10 and later. Ignored on older Python + versions. .. versionadded:: 16.0.0 *slots* .. versionadded:: 16.1.0 *frozen* @@ -1577,7 +1579,11 @@ def wrap(cls): " init must be True." ) - if match_args and not _has_own_attribute(cls, "__match_args__"): + if ( + PY310 + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): builder.add_match_args() return builder.build_class() diff --git a/tests/test_make.py b/tests/test_make.py index c1d893e3d..8b4a3f9e7 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -21,7 +21,7 @@ import attr from attr import _config -from attr._compat import PY2, ordered_dict +from attr._compat import PY2, PY310, ordered_dict from attr._make import ( Attribute, Factory, @@ -2328,7 +2328,20 @@ def __setstate__(self, state): assert True is i.called assert None is getattr(C(), "__getstate__", None) + @pytest.mark.skipif(PY310, reason="Pre-3.10 only.") + def test_match_args_pre_310(self): + """ + __match_args__ is not created on Python versions older than 3.10. + """ + + @attr.s + class C(object): + a = attr.ib() + + assert None is getattr(C, "__match_args__", None) + +@pytest.mark.skipif(not PY310, reason="Structural pattern matching is 3.10+") class TestMatchArgs(object): """ Tests for match_args and __match_args__ generation. @@ -2336,25 +2349,25 @@ class TestMatchArgs(object): def test_match_args(self): """ - __match_args__ generation + __match_args__ is created by default on Python 3.10. """ - @attr.s() - class C(object): - a = attr.ib() + @attr.define + class C: + a = attr.field() - assert C.__match_args__ == ("a",) + assert ("a",) == C.__match_args__ def test_explicit_match_args(self): """ - __match_args__ manually set is not overriden. + A custom __match_args__ set is not overwritten. """ ma = () - @attr.s() - class C(object): - a = attr.ib() + @attr.define + class C: + a = attr.field() __match_args__ = ma assert C(42).__match_args__ is ma @@ -2362,12 +2375,12 @@ class C(object): @pytest.mark.parametrize("match_args", [True, False]) def test_match_args_attr_set(self, match_args): """ - __match_args__ being set depending on match_args. + __match_args__ is set depending on match_args. """ - @attr.s(match_args=match_args) - class C(object): - a = attr.ib() + @attr.define(match_args=match_args) + class C: + a = attr.field() if match_args: assert hasattr(C, "__match_args__") @@ -2376,21 +2389,21 @@ class C(object): def test_match_args_kw_only(self): """ - kw_only being set doesn't generate __match_args__ - kw_only field is not included in __match_args__ + kw_only classes don't generate __match_args__. + kw_only fields are not included in __match_args__. """ - @attr.s() - class C(object): - a = attr.ib(kw_only=True) - b = attr.ib() + @attr.define + class C: + a = attr.field(kw_only=True) + b = attr.field() assert C.__match_args__ == ("b",) - @attr.s(match_args=True, kw_only=True) - class C(object): - a = attr.ib() - b = attr.ib() + @attr.define(kw_only=True) + class C: + a = attr.field() + b = attr.field() assert C.__match_args__ == () @@ -2399,33 +2412,33 @@ def test_match_args_argument(self): match_args being False with inheritance. """ - @attr.s(match_args=False) - class X(object): - a = attr.ib() + @attr.define(match_args=False) + class X: + a = attr.field() assert "__match_args__" not in X.__dict__ - @attr.s(match_args=False) - class Y(object): - a = attr.ib() + @attr.define(match_args=False) + class Y: + a = attr.field() __match_args__ = ("b",) assert Y.__match_args__ == ("b",) - @attr.s(match_args=False) + @attr.define(match_args=False) class Z(Y): - z = attr.ib() + z = attr.field() assert Z.__match_args__ == ("b",) - @attr.s() - class A(object): - a = attr.ib() - z = attr.ib() + @attr.define + class A: + a = attr.field() + z = attr.field() - @attr.s(match_args=False) + @attr.define(match_args=False) class B(A): - b = attr.ib() + b = attr.field() assert B.__match_args__ == ("a", "z") @@ -2435,13 +2448,13 @@ def test_make_class(self): """ C1 = make_class("C1", ["a", "b"]) - assert C1.__match_args__ == ("a", "b") + assert ("a", "b") == C1.__match_args__ C1 = make_class("C1", ["a", "b"], match_args=False) assert not hasattr(C1, "__match_args__") C1 = make_class("C1", ["a", "b"], kw_only=True) - assert C1.__match_args__ == () + assert () == C1.__match_args__ C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()}) - assert C1.__match_args__ == ("b",) + assert ("b",) == C1.__match_args__ diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py index 6a1cb84dc..7c320a75d 100644 --- a/tests/test_pattern_matching.py +++ b/tests/test_pattern_matching.py @@ -1,27 +1,30 @@ # flake8: noqa -# Python 3.10 issue in flake8 : https://github.com/PyCQA/pyflakes/issues/634 +# Python 3.10 issue in flake8: https://github.com/PyCQA/pyflakes/issues/634 +# Keep this file SHORT, until Black and flake8 can handle it. import pytest import attr -from attr._make import make_class +from attr import make_class -class TestPatternMatching(object): +class TestPatternMatching: """ Pattern matching syntax test cases. """ - def test_simple_match_case(self): + @pytest.mark.parametrize("dec", [attr.s, attr.define, attr.frozen]) + def test_simple_match_case(self, dec): """ - Simple match case statement + Simple match case statement works as expected with all class + decorators. """ - @attr.s() + @dec class C(object): a = attr.ib() - assert C.__match_args__ == ("a",) + assert ("a",) == C.__match_args__ matched = False c = C(a=1) @@ -33,14 +36,14 @@ class C(object): def test_explicit_match_args(self): """ - Manually set empty __match_args__ will not match. + Does not overwrite a manually set empty __match_args__. """ ma = () - @attr.s() - class C(object): - a = attr.ib() + @attr.define + class C: + a = attr.field() __match_args__ = ma c = C(a=1) @@ -53,16 +56,16 @@ class C(object): def test_match_args_kw_only(self): """ - kw_only being set doesn't generate __match_args__ - kw_only field is not included in __match_args__ + kw_only classes don't generate __match_args__. + kw_only fields are not included in __match_args__. """ - @attr.s() - class C(object): - a = attr.ib(kw_only=True) - b = attr.ib() + @attr.define + class C: + a = attr.field(kw_only=True) + b = attr.field() - assert C.__match_args__ == ("b",) + assert ("b",) == C.__match_args__ c = C(a=1, b=1) msg = r"C\(\) accepts 1 positional sub-pattern \(2 given\)" @@ -78,10 +81,10 @@ class C(object): assert found - @attr.s(match_args=True, kw_only=True) - class C(object): - a = attr.ib() - b = attr.ib() + @attr.define(kw_only=True) + class C: + a = attr.field() + b = attr.field() c = C(a=1, b=1) msg = r"C\(\) accepts 0 positional sub-patterns \(2 given\)" From d17326e49fb7a07ac0264873e4324faacd7bffcc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 18 May 2021 16:09:16 +0200 Subject: [PATCH 007/139] Replace coverage with downloads The codecov badge is misleading because it dips whenever a build is running. --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d080f1de8..90f922003 100644 --- a/README.rst +++ b/README.rst @@ -12,12 +12,12 @@ CI Status - - Test Coverage - Code style: black + + Downloads per month +

.. teaser-begin From 0112392da9d6c8ce88749def029d7196e9accf0c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 20 May 2021 17:11:39 +0200 Subject: [PATCH 008/139] pre-commit update --- .pre-commit-config.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d042f496..cef36616b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,28 @@ --- repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.5b1 hooks: - id: black exclude: tests/test_pattern_matching.py language_version: python3.8 - repo: https://github.com/PyCQA/isort - rev: 5.7.0 + rev: 5.8.0 hooks: - id: isort additional_dependencies: [toml] + files: \.py$ + language_version: python3.10 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.2 hooks: - id: flake8 language_version: python3.10 - repo: https://github.com/econchick/interrogate - rev: 1.3.2 + rev: 1.4.0 hooks: - id: interrogate exclude: tests/test_pattern_matching.py @@ -28,7 +30,7 @@ repos: language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 0c769095ffb2cba34994a7039b04f0310ecb0fbd Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 20 May 2021 17:11:53 +0200 Subject: [PATCH 009/139] Switch to black formatting in stubs Also block isort from stubs -- it plays ping-pong with black about an empty line after imports. --- src/attr/__init__.pyi | 12 +++++++----- src/attr/_cmp.pyi | 1 - src/attr/converters.pyi | 1 - src/attr/exceptions.pyi | 1 - src/attr/filters.pyi | 1 - src/attr/setters.pyi | 1 - src/attr/validators.pyi | 1 - 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 7221836c1..610eced79 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -24,7 +24,6 @@ from . import setters as setters from . import validators as validators from ._version_info import VersionInfo - __version__: str __version_info__: VersionInfo __title__: str @@ -49,7 +48,9 @@ _OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] _OnSetAttrArgType = Union[ _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType ] -_FieldTransformer = Callable[[type, List[Attribute[Any]]], List[Attribute[Any]]] +_FieldTransformer = Callable[ + [type, List[Attribute[Any]]], List[Attribute[Any]] +] # FIXME: in reality, if multiple validators are passed they must be in a list # or tuple, but those are invariant and so would prevent subtypes of # _ValidatorType from working when passed in a list or tuple. @@ -64,7 +65,6 @@ NOTHING: object # Work around mypy issue #4554 in the common case by using an overload. if sys.version_info >= (3, 8): from typing import Literal - @overload def Factory(factory: Callable[[], _T]) -> _T: ... @overload @@ -77,6 +77,7 @@ if sys.version_info >= (3, 8): factory: Callable[[], _T], takes_self: Literal[False], ) -> _T: ... + else: @overload def Factory(factory: Callable[[], _T]) -> _T: ... @@ -117,7 +118,6 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType - def evolve(self, **changes: Any) -> "Attribute[Any]": ... # NOTE: We had several choices for the annotation to use for type arg: @@ -452,7 +452,9 @@ def asdict( filter: Optional[_FilterType[Any]] = ..., dict_factory: Type[Mapping[Any, Any]] = ..., retain_collection_types: bool = ..., - value_serializer: Optional[Callable[[type, Attribute[Any], Any], Any]] = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., ) -> Dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin diff --git a/src/attr/_cmp.pyi b/src/attr/_cmp.pyi index 7093550f0..e71aaff7a 100644 --- a/src/attr/_cmp.pyi +++ b/src/attr/_cmp.pyi @@ -2,7 +2,6 @@ from typing import Type from . import _CompareWithType - def cmp_using( eq: Optional[_CompareWithType], lt: Optional[_CompareWithType], diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi index 84a57590b..d180e4646 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -2,7 +2,6 @@ from typing import Callable, Optional, TypeVar, overload from . import _ConverterType - _T = TypeVar("_T") def pipe(*validators: _ConverterType) -> _ConverterType: ... diff --git a/src/attr/exceptions.pyi b/src/attr/exceptions.pyi index a800fb26b..f2680118b 100644 --- a/src/attr/exceptions.pyi +++ b/src/attr/exceptions.pyi @@ -1,6 +1,5 @@ from typing import Any - class FrozenError(AttributeError): msg: str = ... diff --git a/src/attr/filters.pyi b/src/attr/filters.pyi index f7b63f1bb..993866865 100644 --- a/src/attr/filters.pyi +++ b/src/attr/filters.pyi @@ -2,6 +2,5 @@ from typing import Any, Union from . import Attribute, _FilterType - def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/src/attr/setters.pyi b/src/attr/setters.pyi index a921e07de..3f5603c2b 100644 --- a/src/attr/setters.pyi +++ b/src/attr/setters.pyi @@ -2,7 +2,6 @@ from typing import Any, NewType, NoReturn, TypeVar, cast from . import Attribute, _OnSetAttrType - _T = TypeVar("_T") def frozen( diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index fe92aac42..14101bf3b 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -17,7 +17,6 @@ from typing import ( from . import _ValidatorType - _T = TypeVar("_T") _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") From 9709dd82e1cc0f5b783f4127e87498dfdd6a224a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Wed, 26 May 2021 21:57:52 +0200 Subject: [PATCH 010/139] Initial implementation of a faster repr (#819) * Initial implementation of a faster repr * Switch to positional args for _make_repr * Fix tests and changelog * Remove trailing comma for Py2 * Fix lint * __qualname__ is always present if f-strings work * Fix Py2 qualname * Revert "Fix Py2 qualname" This reverts commit eb091a31d2d3d54a2ae2a2bfc3a74998ec863b5d. * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Update changelog.d/819.changes.rst Co-authored-by: Hynek Schlawack * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Update src/attr/_make.py Co-authored-by: Hynek Schlawack * Fix syntax Co-authored-by: Hynek Schlawack --- changelog.d/819.changes.rst | 1 + src/attr/_compat.py | 5 ++ src/attr/_make.py | 175 +++++++++++++++++++++++++----------- 3 files changed, 130 insertions(+), 51 deletions(-) create mode 100644 changelog.d/819.changes.rst diff --git a/changelog.d/819.changes.rst b/changelog.d/819.changes.rst new file mode 100644 index 000000000..eb45d6168 --- /dev/null +++ b/changelog.d/819.changes.rst @@ -0,0 +1 @@ +The generated ``__repr__`` is significantly faster on Pythons with F-strings. diff --git a/src/attr/_compat.py b/src/attr/_compat.py index c218e2453..69329e996 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -8,6 +8,11 @@ PY2 = sys.version_info[0] == 2 PYPY = platform.python_implementation() == "PyPy" +HAS_F_STRINGS = ( + sys.version_info[:2] >= (3, 7) + if not PYPY + else sys.version_info[:2] >= (3, 6) +) PY310 = sys.version_info[:2] >= (3, 10) diff --git a/src/attr/_make.py b/src/attr/_make.py index ad17109db..658eb047b 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -12,6 +12,7 @@ from . import _config, setters from ._compat import ( + HAS_F_STRINGS, PY2, PY310, PYPY, @@ -888,7 +889,7 @@ def _create_slots_class(self): def add_repr(self, ns): self._cls_dict["__repr__"] = self._add_method_dunders( - _make_repr(self._attrs, ns=ns) + _make_repr(self._attrs, ns, self._cls) ) return self @@ -1873,64 +1874,136 @@ def _add_eq(cls, attrs=None): _already_repring = threading.local() +if HAS_F_STRINGS: + + def _make_repr(attrs, ns, cls): + unique_filename = "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. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r + for name, r, _ in attr_names_with_reprs + if r != repr + } + globs["_already_repring"] = _already_repring + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name + if i + else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) -def _make_repr(attrs, ns): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the full - name. - """ + if ns is None: + cls_name_fragment = ( + '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + ) + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [] + lines.append("def __repr__(self):") + lines.append(" try:") + lines.append(" working_set = _already_repring.working_set") + lines.append(" except AttributeError:") + lines.append(" working_set = {id(self),}") + lines.append(" _already_repring.working_set = working_set") + lines.append(" else:") + lines.append(" if id(self) in working_set:") + lines.append(" return '...'") + lines.append(" else:") + lines.append(" working_set.add(id(self))") + lines.append(" try:") + lines.append( + " return f'%s(%s)'" % (cls_name_fragment, repr_fragment) + ) + lines.append(" finally:") + lines.append(" working_set.remove(id(self))") - # 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. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) - def __repr__(self): + +else: + + def _make_repr(attrs, ns, _): """ - Automatically created by attrs. + Make a repr method that includes relevant *attrs*, adding *ns* to the + full name. """ - try: - working_set = _already_repring.working_set - except AttributeError: - working_set = set() - _already_repring.working_set = working_set - if id(self) in working_set: - return "..." - real_cls = self.__class__ - if ns is None: - qualname = getattr(real_cls, "__qualname__", None) - if qualname is not None: - class_name = qualname.rsplit(">.", 1)[-1] - else: - class_name = real_cls.__name__ - else: - class_name = ns + "." + real_cls.__name__ + # 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. + attr_names_with_reprs = tuple( + (a.name, repr if a.repr is True else a.repr) + for a in attrs + if a.repr is not False + ) - # Since 'self' remains on the stack (i.e.: strongly referenced) for the - # duration of this call, it's safe to depend on id(...) stability, and - # not need to track the instance and therefore worry about properties - # like weakref- or hash-ability. - working_set.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False + def __repr__(self): + """ + Automatically created by attrs. + """ + try: + working_set = _already_repring.working_set + except AttributeError: + working_set = set() + _already_repring.working_set = working_set + + if id(self) in working_set: + return "..." + real_cls = self.__class__ + if ns is None: + qualname = getattr(real_cls, "__qualname__", None) + if qualname is not None: # pragma: no cover + # This case only happens on Python 3.5 and 3.6. We exclude + # it from coverage, because we don't want to slow down our + # test suite by running them under coverage too for this + # one line. + class_name = qualname.rsplit(">.", 1)[-1] else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - working_set.remove(id(self)) + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # Since 'self' remains on the stack (i.e.: strongly referenced) + # for the duration of this call, it's safe to depend on id(...) + # stability, and not need to track the instance and therefore + # worry about properties like weakref- or hash-ability. + working_set.add(id(self)) + try: + result = [class_name, "("] + first = True + for name, attr_repr in attr_names_with_reprs: + if first: + first = False + else: + result.append(", ") + result.extend( + (name, "=", attr_repr(getattr(self, name, NOTHING))) + ) + return "".join(result) + ")" + finally: + working_set.remove(id(self)) - return __repr__ + return __repr__ def _add_repr(cls, ns=None, attrs=None): @@ -1940,7 +2013,7 @@ def _add_repr(cls, ns=None, attrs=None): if attrs is None: attrs = cls.__attrs_attrs__ - cls.__repr__ = _make_repr(attrs, ns) + cls.__repr__ = _make_repr(attrs, ns, cls) return cls From 9a6dc6599a61fdb3e81b6d6f4b380d7baf25c349 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 1 Jun 2021 11:46:40 +0200 Subject: [PATCH 011/139] Simplify badges Especially remove everything that's apparent from the GitHub UI. --- README.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 90f922003..4e465e7f7 100644 --- a/README.rst +++ b/README.rst @@ -7,13 +7,10 @@

- Documentation Status + Documentation - - CI Status - - - Code style: black + + Downloads per month From e25a24c77c3faaad1eef3682f749b97610b7ec9a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 1 Jun 2021 12:15:23 +0200 Subject: [PATCH 012/139] Add license badge The color is complimentary to the blue of the version badge. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 4e465e7f7..92a24f42e 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,9 @@ Documentation + + License: MIT + From 464d6e42b7e9a192f8ddb5e638aab9cff5196bdc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 3 Jun 2021 08:40:49 +0200 Subject: [PATCH 013/139] Harmonize badge label color --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 92a24f42e..2ab4cff35 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ - Downloads per month + Downloads per month

From 7cc31216173deeed7e4b7ea69ef5b0976a8ed72f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 3 Jun 2021 08:41:29 +0200 Subject: [PATCH 014/139] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cef36616b..63603e634 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/psf/black - rev: 21.5b1 + rev: 21.5b2 hooks: - id: black exclude: tests/test_pattern_matching.py From ec249f62b3b0c88daee29e3dd56015de8fb23c06 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 14 Jun 2021 10:17:33 -0700 Subject: [PATCH 015/139] Fix the mypy tests by double quoting. (#825) --- tests/test_mypy.yml | 192 ++++++++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 1dd0f9a7f..ca17b0a66 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -35,7 +35,7 @@ def foo(self): return self.a - reveal_type(A) # N: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: Any, b: Any, c: Any =, d: Any =) -> main.A" A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) @@ -53,7 +53,7 @@ _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" @@ -71,7 +71,7 @@ _d = attr.ib(validator=None, default=18) # type: int E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" @@ -89,7 +89,7 @@ _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" @@ -102,10 +102,10 @@ import attr @attr.s class A: - a = attr.ib() # E: Need type annotation for 'a' - _b = attr.ib() # E: Need type annotation for '_b' - c = attr.ib(18) # E: Need type annotation for 'c' - _d = attr.ib(validator=None, default=18) # E: Need type annotation for '_d' + a = attr.ib() # E: Need type annotation for "a" + _b = attr.ib() # E: Need type annotation for "_b" + c = attr.ib(18) # E: Need type annotation for "c" + _d = attr.ib(validator=None, default=18) # E: Need type annotation for "_d" E = 18 - case: testAttrsWrongReturnValue @@ -143,7 +143,7 @@ c = attrib(18) _d = attrib(validator=None, default=18) CLASS_VAR = 18 - reveal_type(A) # N: Revealed type is 'def (a: Any, b: builtins.list[builtins.int], c: Any =, d: Any =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: Any, b: builtins.list[builtins.int], c: Any =, d: Any =) -> main.A" A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" @@ -190,7 +190,7 @@ _b: int c: int = 18 _d: int = attrib(validator=None, default=18) - reveal_type(A) # N: Revealed type is 'def () -> main.A' + reveal_type(A) # N: Revealed type is "def () -> main.A" A() A(1, [2]) # E: Too many arguments for "A" A(1, [2], '3', 4) # E: Too many arguments for "A" @@ -202,7 +202,7 @@ class A: a = attrib(init=False) b = attrib() - reveal_type(A) # N: Revealed type is 'def (b: Any) -> main.A' + reveal_type(A) # N: Revealed type is "def (b: Any) -> main.A" - case: testAttrsCmpTrue main: | @@ -210,11 +210,11 @@ @attrs(auto_attribs=True) class A: a: int - reveal_type(A) # N: Revealed type is 'def (a: builtins.int) -> main.A' - reveal_type(A.__lt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(A.__le__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(A.__gt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(A.__ge__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' + reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A" + reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(A.__le__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(A.__gt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(A.__ge__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" A(1) < A(2) A(1) <= A(2) @@ -243,9 +243,9 @@ @attrs(auto_attribs=True, eq=False) class A: a: int - reveal_type(A) # N: Revealed type is 'def (a: builtins.int) -> main.A' - reveal_type(A.__eq__) # N: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' - reveal_type(A.__ne__) # N: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' + reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A" + reveal_type(A.__eq__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool" + reveal_type(A.__ne__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool" A(1) < A(2) # E: Unsupported left operand type for < ("A") A(1) <= A(2) # E: Unsupported left operand type for <= ("A") @@ -274,7 +274,7 @@ @attrs(auto_attribs=True, order=False) class A: a: int - reveal_type(A) # N: Revealed type is 'def (a: builtins.int) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A" A(1) < A(2) # E: Unsupported left operand type for < ("A") A(1) <= A(2) # E: Unsupported left operand type for <= ("A") @@ -308,7 +308,7 @@ class DeprecatedFalse: ... - @attrs(cmp=False, eq=True) # E: Don't mix `cmp` with `eq' and `order` + @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order" class Mixed: ... @@ -329,7 +329,7 @@ @attr.s class C(A, B): c: bool = attr.ib() - reveal_type(C) # N: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> main.C' + reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.str, c: builtins.bool) -> main.C" - case: testAttrsNestedInClasses main: | @@ -340,8 +340,8 @@ @attr.s class D: x: int = attr.ib() - reveal_type(C) # N: Revealed type is 'def (y: Any) -> main.C' - reveal_type(C.D) # N: Revealed type is 'def (x: builtins.int) -> main.C.D' + reveal_type(C) # N: Revealed type is "def (y: Any) -> main.C" + reveal_type(C.D) # N: Revealed type is "def (x: builtins.int) -> main.C.D" - case: testAttrsInheritanceOverride main: | @@ -362,9 +362,9 @@ c: bool = attr.ib() # No error here because the x below overwrites the x above. x: int = attr.ib() - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, x: builtins.int) -> main.A' - reveal_type(B) # N: Revealed type is 'def (a: builtins.int, b: builtins.str, x: builtins.int =) -> main.B' - reveal_type(C) # N: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool, x: builtins.int) -> main.C' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, x: builtins.int) -> main.A" + reveal_type(B) # N: Revealed type is "def (a: builtins.int, b: builtins.str, x: builtins.int =) -> main.B" + reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.str, c: builtins.bool, x: builtins.int) -> main.C" - case: testAttrsTypeEquals main: | @@ -374,7 +374,7 @@ class A: a = attr.ib(type=int) b = attr.ib(18, type=int) - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.int =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.int =) -> main.A" - case: testAttrsFrozen main: | @@ -420,10 +420,10 @@ b = field() # TODO: Next Gen hasn't shipped with mypy yet so the following are wrong - reveal_type(A) # N: Revealed type is 'def (a: Any) -> main.A' - reveal_type(B) # N: Revealed type is 'def (a: builtins.int) -> main.B' - reveal_type(C) # N: Revealed type is 'def (a: builtins.int, b: Any) -> main.C' - reveal_type(D) # N: Revealed type is 'def (b: Any) -> main.D' + reveal_type(A) # N: Revealed type is "def (a: Any) -> main.A" + reveal_type(B) # N: Revealed type is "def (a: builtins.int) -> main.B" + reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: Any) -> main.C" + reveal_type(D) # N: Revealed type is "def (b: Any) -> main.D" - case: testAttrsDataClass main: | @@ -437,7 +437,7 @@ _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> main.A" A(1, ['2']) - case: testAttrsTypeAlias @@ -450,7 +450,7 @@ Alias2 = List[str] x: Alias y: Alias2 = attr.ib() - reveal_type(A) # N: Revealed type is 'def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> main.A' + reveal_type(A) # N: Revealed type is "def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> main.A" - case: testAttrsGeneric main: | @@ -467,11 +467,11 @@ return self.x[0] def problem(self) -> T: return self.x # E: Incompatible return value type (got "List[T]", expected "T") - reveal_type(A) # N: Revealed type is 'def [T] (x: builtins.list[T`1], y: T`1) -> main.A[T`1]' + reveal_type(A) # N: Revealed type is "def [T] (x: builtins.list[T`1], y: T`1) -> main.A[T`1]" a = A([1], 2) - reveal_type(a) # N: Revealed type is 'main.A[builtins.int*]' - reveal_type(a.x) # N: Revealed type is 'builtins.list[builtins.int*]' - reveal_type(a.y) # N: Revealed type is 'builtins.int*' + reveal_type(a) # N: Revealed type is "main.A[builtins.int*]" + reveal_type(a.x) # N: Revealed type is "builtins.list[builtins.int*]" + reveal_type(a.y) # N: Revealed type is "builtins.int*" A(['str'], 7) # E: Cannot infer type argument 1 of "A" A([1], '2') # E: Cannot infer type argument 1 of "A" @@ -493,8 +493,8 @@ pass sub = Sub(attr=1) - reveal_type(sub) # N: Revealed type is 'main.Sub' - reveal_type(sub.attr) # N: Revealed type is 'Any' + reveal_type(sub) # N: Revealed type is "main.Sub" + reveal_type(sub.attr) # N: Revealed type is "Any" skip: True # Need to investigate why this is broken - case: testAttrsGenericInheritance @@ -514,12 +514,12 @@ pass sub_int = Sub[int](attr=1) - reveal_type(sub_int) # N: Revealed type is 'main.Sub[builtins.int*]' - reveal_type(sub_int.attr) # N: Revealed type is 'builtins.int*' + reveal_type(sub_int) # N: Revealed type is "main.Sub[builtins.int*]" + reveal_type(sub_int.attr) # N: Revealed type is "builtins.int*" sub_str = Sub[str](attr='ok') - reveal_type(sub_str) # N: Revealed type is 'main.Sub[builtins.str*]' - reveal_type(sub_str.attr) # N: Revealed type is 'builtins.str*' + reveal_type(sub_str) # N: Revealed type is "main.Sub[builtins.str*]" + reveal_type(sub_str.attr) # N: Revealed type is "builtins.str*" - case: testAttrsGenericInheritance2 main: | @@ -541,10 +541,10 @@ pass sub = Sub(one=1, two='ok', three=3.14) - reveal_type(sub) # N: Revealed type is 'main.Sub' - reveal_type(sub.one) # N: Revealed type is 'builtins.int*' - reveal_type(sub.two) # N: Revealed type is 'builtins.str*' - reveal_type(sub.three) # N: Revealed type is 'builtins.float*' + reveal_type(sub) # N: Revealed type is "main.Sub" + reveal_type(sub.one) # N: Revealed type is "builtins.int*" + reveal_type(sub.two) # N: Revealed type is "builtins.str*" + reveal_type(sub.three) # N: Revealed type is "builtins.float*" skip: True # Need to investigate why this is broken - case: testAttrsMultiGenericInheritance @@ -571,9 +571,9 @@ reveal_type(Sub.__init__) sub = Sub(base_attr=1, middle_attr='ok') - reveal_type(sub) # N: Revealed type is 'main.Sub' - reveal_type(sub.base_attr) # N: Revealed type is 'builtins.int*' - reveal_type(sub.middle_attr) # N: Revealed type is 'builtins.str*' + reveal_type(sub) # N: Revealed type is "main.Sub" + reveal_type(sub.base_attr) # N: Revealed type is "builtins.int*" + reveal_type(sub.middle_attr) # N: Revealed type is "builtins.str*" skip: True # Need to investigate why this is broken - case: testAttrsGenericClassmethod @@ -586,7 +586,7 @@ x: Optional[T] @classmethod def clsmeth(cls) -> None: - reveal_type(cls) # N: Revealed type is 'Type[main.A[T`1]]' + reveal_type(cls) # N: Revealed type is "Type[main.A[T`1]]" - case: testAttrsForwardReference main: | @@ -600,8 +600,8 @@ class B: parent: Optional[A] - reveal_type(A) # N: Revealed type is 'def (parent: main.B) -> main.A' - reveal_type(B) # N: Revealed type is 'def (parent: Union[main.A, None]) -> main.B' + reveal_type(A) # N: Revealed type is "def (parent: main.B) -> main.A" + reveal_type(B) # N: Revealed type is "def (parent: Union[main.A, None]) -> main.B" A(B(None)) - case: testAttrsForwardReferenceInClass @@ -616,14 +616,14 @@ class B: parent: Optional[A] - reveal_type(A) # N: Revealed type is 'def (parent: main.A.B) -> main.A' - reveal_type(A.B) # N: Revealed type is 'def (parent: Union[main.A, None]) -> main.A.B' + reveal_type(A) # N: Revealed type is "def (parent: main.A.B) -> main.A" + reveal_type(A.B) # N: Revealed type is "def (parent: Union[main.A, None]) -> main.A.B" A(A.B(None)) - case: testAttrsImporting main: | from helper import A - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.str) -> helper.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> helper.A" files: - path: helper.py content: | @@ -642,16 +642,16 @@ b: str = attr.ib() @classmethod def new(cls) -> A: - reveal_type(cls) # N: Revealed type is 'Type[main.A]' + reveal_type(cls) # N: Revealed type is "Type[main.A]" return cls(6, 'hello') @classmethod def bad(cls) -> A: return cls(17) # E: Missing positional argument "b" in call to "A" def foo(self) -> int: return self.a - reveal_type(A) # N: Revealed type is 'def (a: builtins.int, b: builtins.str) -> main.A' + reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> main.A" a = A.new() - reveal_type(a.foo) # N: Revealed type is 'def () -> builtins.int' + reveal_type(a.foo) # N: Revealed type is "def () -> builtins.int" - case: testAttrsOtherOverloads main: | @@ -677,12 +677,12 @@ @classmethod def foo(cls, x: Union[int, str]) -> Union[int, str]: - reveal_type(cls) # N: Revealed type is 'Type[main.A]' - reveal_type(cls.other()) # N: Revealed type is 'builtins.str' + reveal_type(cls) # N: Revealed type is "Type[main.A]" + reveal_type(cls.other()) # N: Revealed type is "builtins.str" return x - reveal_type(A.foo(3)) # N: Revealed type is 'builtins.int' - reveal_type(A.foo("foo")) # N: Revealed type is 'builtins.str' + reveal_type(A.foo(3)) # N: Revealed type is "builtins.int" + reveal_type(A.foo("foo")) # N: Revealed type is "builtins.str" - case: testAttrsDefaultDecorator main: | @@ -736,8 +736,8 @@ AOrB = Union[A, B] - reveal_type(A) # N: Revealed type is 'def (frob: builtins.list[Union[main.A, main.B]]) -> main.A' - reveal_type(B) # N: Revealed type is 'def () -> main.B' + reveal_type(A) # N: Revealed type is "def (frob: builtins.list[Union[main.A, main.B]]) -> main.A" + reveal_type(B) # N: Revealed type is "def () -> main.B" A([B()]) @@ -755,8 +755,8 @@ y: str = attr.ib(converter=converter2) # Because of the converter the __init__ takes an int, but the variable is a str. - reveal_type(C) # N: Revealed type is 'def (x: builtins.int, y: builtins.int) -> main.C' - reveal_type(C(15, 16).x) # N: Revealed type is 'builtins.str' + reveal_type(C) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> main.C" + reveal_type(C(15, 16).x) # N: Revealed type is "builtins.str" files: - path: helper.py content: | @@ -789,7 +789,7 @@ main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:16: error: Cannot determine __init__ type from converter main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" - main:17: note: Revealed type is 'def (bad: Any, bad_overloaded: Any) -> main.A' + main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingBadConverterReprocess mypy_config: @@ -818,7 +818,7 @@ main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:17: error: Cannot determine __init__ type from converter main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" - main:18: note: Revealed type is 'def (bad: Any, bad_overloaded: Any) -> main.A' + main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingUnsupportedConverter main: | @@ -834,7 +834,7 @@ x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions and types are currently supported y: str = attr.ib(converter=lambda x: x) # E: Unsupported converter, only named functions and types are currently supported z: str = attr.ib(converter=factory(8)) # E: Unsupported converter, only named functions and types are currently supported - reveal_type(C) # N: Revealed type is 'def (x: Any, y: Any, z: Any) -> main.C' + reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> main.C" - case: testAttrsUsingConverterAndSubclass main: | @@ -852,8 +852,8 @@ pass # Because of the convert the __init__ takes an int, but the variable is a str. - reveal_type(A) # N: Revealed type is 'def (x: builtins.int) -> main.A' - reveal_type(A(15).x) # N: Revealed type is 'builtins.str' + reveal_type(A) # N: Revealed type is "def (x: builtins.int) -> main.A" + reveal_type(A(15).x) # N: Revealed type is "builtins.str" - case: testAttrsUsingConverterWithTypes main: | @@ -885,10 +885,10 @@ @attr.s class D(A): pass - reveal_type(A.__lt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(B.__lt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(C.__lt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' - reveal_type(D.__lt__) # N: Revealed type is 'def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool' + reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(B.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(C.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(D.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" A() < A() B() < B() @@ -922,8 +922,8 @@ @attr.s class A(C): z: int = attr.ib(default=18) - reveal_type(C) # N: Revealed type is 'def (x: builtins.int =, y: builtins.int =) -> main.C' - reveal_type(A) # N: Revealed type is 'def (x: builtins.int =, y: builtins.int =, z: builtins.int =) -> main.A' + reveal_type(C) # N: Revealed type is "def (x: builtins.int =, y: builtins.int =) -> main.C" + reveal_type(A) # N: Revealed type is "def (x: builtins.int =, y: builtins.int =, z: builtins.int =) -> main.A" - case: testAttrsMultiAssign main: | @@ -931,7 +931,7 @@ @attr.s class A: x, y, z = attr.ib(), attr.ib(type=int), attr.ib(default=17) - reveal_type(A) # N: Revealed type is 'def (x: Any, y: builtins.int, z: Any =) -> main.A' + reveal_type(A) # N: Revealed type is "def (x: Any, y: builtins.int, z: Any =) -> main.A" - case: testAttrsMultiAssign2 main: | @@ -957,9 +957,9 @@ a: int b = 17 # The following forms are not allowed with auto_attribs=True - c = attr.ib() # E: Need type annotation for 'c' - d, e = attr.ib(), attr.ib() # E: Need type annotation for 'd' # E: Need type annotation for 'e' - f = g = attr.ib() # E: Need type annotation for 'f' # E: Need type annotation for 'g' + c = attr.ib() # E: Need type annotation for "c" + d, e = attr.ib(), attr.ib() # E: Need type annotation for "d" # E: Need type annotation for "e" + f = g = attr.ib() # E: Need type annotation for "f" # E: Need type annotation for "g" - case: testAttrsRepeatedName main: | @@ -969,19 +969,19 @@ a = attr.ib(default=8) b = attr.ib() a = attr.ib() - reveal_type(A) # N: Revealed type is 'def (b: Any, a: Any) -> main.A' + reveal_type(A) # N: Revealed type is "def (b: Any, a: Any) -> main.A" @attr.s class B: a: int = attr.ib(default=8) b: int = attr.ib() - a: int = attr.ib() # E: Name 'a' already defined on line 10 - reveal_type(B) # N: Revealed type is 'def (b: builtins.int, a: builtins.int) -> main.B' + a: int = attr.ib() # E: Name "a" already defined on line 10 + reveal_type(B) # N: Revealed type is "def (b: builtins.int, a: builtins.int) -> main.B" @attr.s(auto_attribs=True) class C: a: int = 8 b: int - a: int = attr.ib() # E: Name 'a' already defined on line 16 - reveal_type(C) # N: Revealed type is 'def (a: builtins.int, b: builtins.int) -> main.C' + a: int = attr.ib() # E: Name "a" already defined on line 16 + reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.int) -> main.C" - case: testAttrsNewStyleClassPy2 mypy_config: @@ -1088,7 +1088,7 @@ import attr @attr.s class A: - x: int = attr.ib(factory=int, default=7) # E: Can't pass both `default` and `factory`. + x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory". - case: testAttrsFactoryBadReturn main: | @@ -1244,7 +1244,7 @@ class C(List[C]): pass - reveal_type(B) # N: Revealed type is 'def (x: main.C) -> main.B' + reveal_type(B) # N: Revealed type is "def (x: main.C) -> main.B" - case: testDisallowUntypedWorksForwardBad mypy_config: @@ -1254,9 +1254,9 @@ @attr.s class B: - x = attr.ib() # E: Need type annotation for 'x' + x = attr.ib() # E: Need type annotation for "x" - reveal_type(B) # N: Revealed type is 'def (x: Any) -> main.B' + reveal_type(B) # N: Revealed type is "def (x: Any) -> main.B" - case: testAttrsDefaultDecoratorDeferred main: | @@ -1296,7 +1296,7 @@ @attr.s class C: - total = attr.ib(type=Bad) # E: Name 'Bad' is not defined + total = attr.ib(type=Bad) # E: Name "Bad" is not defined - case: testTypeInAttrForwardInRuntime main: | @@ -1306,7 +1306,7 @@ class C: total = attr.ib(type=Forward) - reveal_type(C.total) # N: Revealed type is 'main.Forward' + reveal_type(C.total) # N: Revealed type is "main.Forward" C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "Forward" class Forward: ... @@ -1330,7 +1330,7 @@ @attr.s(frozen=True) class C: - total = attr.ib(type=Bad) # E: Name 'Bad' is not defined + total = attr.ib(type=Bad) # E: Name "Bad" is not defined C(0).total = 1 # E: Property "total" defined in "C" is read-only @@ -1392,4 +1392,4 @@ class B(A): foo = x - reveal_type(B) # N: Revealed type is 'def (foo: builtins.int) -> main.B' + reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> main.B" From fb154878ebd2758d2a3b4dc518d21fd4f73e12d2 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 14 Jun 2021 10:58:33 -0700 Subject: [PATCH 016/139] Fix mypy tests on 3.10 (#771) Looks like this was a bug fixed upstream --- conftest.py | 6 ------ tox.ini | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/conftest.py b/conftest.py index 79806cb16..14ee0c10b 100644 --- a/conftest.py +++ b/conftest.py @@ -27,9 +27,3 @@ def pytest_configure(config): ) if not PY310: collect_ignore.extend(["tests/test_pattern_matching.py"]) -if PY310: - collect_ignore.extend( - [ - "tests/test_mypy.yml", - ] - ) diff --git a/tox.ini b/tox.ini index f134364c2..83755e23f 100644 --- a/tox.ini +++ b/tox.ini @@ -128,7 +128,7 @@ commands = towncrier --draft [testenv:typing] basepython = python3.8 -deps = mypy>=0.800 +deps = mypy>=0.902 commands = mypy src/attr/__init__.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi mypy tests/typing_example.py From 38580632ceac1cd6e477db71e1d190a4130beed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Wed, 30 Jun 2021 08:28:56 +0200 Subject: [PATCH 017/139] Rework linecache handling (#828) * Rework linecache handling * lint * Add changelog --- changelog.d/828.changes.rst | 1 + src/attr/_make.py | 56 +++++++++++++++---------------------- tests/test_dunders.py | 28 +++++++++++++++++-- 3 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 changelog.d/828.changes.rst diff --git a/changelog.d/828.changes.rst b/changelog.d/828.changes.rst new file mode 100644 index 000000000..b4a5454c8 --- /dev/null +++ b/changelog.d/828.changes.rst @@ -0,0 +1 @@ +Generated source code is now cached more efficiently for identical classes. diff --git a/src/attr/_make.py b/src/attr/_make.py index 658eb047b..95a37ea64 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -5,7 +5,6 @@ import linecache import sys import threading -import uuid import warnings from operator import itemgetter @@ -329,16 +328,25 @@ def _make_method(name, script, filename, globs=None): if globs is None: globs = {} - _compile_and_eval(script, globs, locs, filename) - # In order of debuggers like PDB being able to step through the code, # we add a fake linecache entry. - linecache.cache[filename] = ( - len(script), - None, - script.splitlines(True), - filename, - ) + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + else: + filename = "{}-{}>".format(base_filename[:-1], count) + count += 1 + + _compile_and_eval(script, globs, locs, filename) return locs[name] @@ -1632,30 +1640,12 @@ def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_id = uuid.uuid4() - extra = "" - count = 1 - - while True: - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), - extra, - ) - # To handle concurrency we essentially "reserve" our spot in - # the linecache with a dummy line. The caller can then - # set this value correctly. - cache_line = (1, None, (str(unique_id),), unique_filename) - if ( - linecache.cache.setdefault(unique_filename, cache_line) - == cache_line - ): - return unique_filename - - # Looks like this spot is taken. Try again. - count += 1 - extra = "-{0}".format(count) + unique_filename = "".format( + func_name, + cls.__module__, + getattr(cls, "__qualname__", cls.__name__), + ) + return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): diff --git a/tests/test_dunders.py b/tests/test_dunders.py index ba8f3ce89..3a8300399 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -936,6 +936,16 @@ class C(object): pass +CopyC = C + + +@attr.s(hash=True, order=True) +class C(object): + """A different class, to generate different methods.""" + + a = attr.ib() + + class TestFilenames(object): def test_filenames(self): """ @@ -953,15 +963,27 @@ def test_filenames(self): OriginalC.__hash__.__code__.co_filename == "" ) + assert ( + CopyC.__init__.__code__.co_filename + == "" + ) + assert ( + CopyC.__eq__.__code__.co_filename + == "" + ) + assert ( + CopyC.__hash__.__code__.co_filename + == "" + ) assert ( C.__init__.__code__.co_filename - == "" + == "" ) assert ( C.__eq__.__code__.co_filename - == "" + == "" ) assert ( C.__hash__.__code__.co_filename - == "" + == "" ) From 2ca7aada707167cda9b3c8bbc2fd195e4f1aa422 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 7 Aug 2021 08:50:52 +0200 Subject: [PATCH 018/139] Run pypy on pypy2 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 83755e23f..3dfd139c9 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ python = 3.8: py38, manifest, typing, changelog 3.9: py39, pyright 3.10: py310, lint - pypy2: pypy2 + pypy2: pypy pypy3: pypy3 From e84b57ea687942e5344badfe41a2728e24037431 Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Tue, 10 Aug 2021 01:45:28 -0400 Subject: [PATCH 019/139] Inline distutils.util.strtobool in tests (closes #813) (#830) * Inline distutils.util.strtobool in tests (#813) `distutils` is deprecated in Python 3.10 and slated for removal in Python 3.12. Fortunately, `attrs` only uses `distutils` once and it's trivial to remove. As suggested by @sscherfke, add the `to_bool` converter to `converters.py`. Closes #813 Co-authored-by: Stefan Scherfke * Use :raises: directive in docstring * Remove f-strings for Py2.7 and 3.5 support * Add to_bool tests Co-authored-by: Stefan Scherfke Co-authored-by: Hynek Schlawack --- docs/api.rst | 22 +++++++++++++++++++++ src/attr/converters.py | 41 ++++++++++++++++++++++++++++++++++++++++ src/attr/converters.pyi | 1 + tests/test_converters.py | 37 +++++++++++++++++++++++++++++------- tests/typing_example.py | 14 ++++++++++++++ 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 3fd71d651..8ffd2dc95 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -551,6 +551,28 @@ Converters C(x='') +.. autofunction:: attr.converters.to_bool + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib( + ... converter=attr.converters.to_bool + ... ) + >>> C("yes") + C(x=True) + >>> C(0) + C(x=False) + >>> C("foo") + Traceback (most recent call last): + File "", line 1, in + ValueError: Cannot convert value to bool: foo + + + .. _api_setters: Setters diff --git a/src/attr/converters.py b/src/attr/converters.py index 2777db6d0..366b8728a 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -109,3 +109,44 @@ def default_if_none_converter(val): return default return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (e.g., from env. vars.) to real booleans. + + Values mapping to :code:`True`: + + - :code:`True` + - :code:`"true"` / :code:`"t"` + - :code:`"yes"` / :code:`"y"` + - :code:`"on"` + - :code:`"1"` + - :code:`1` + + Values mapping to :code:`False`: + + - :code:`False` + - :code:`"false"` / :code:`"f"` + - :code:`"no"` / :code:`"n"` + - :code:`"off"` + - :code:`"0"` + - :code:`0` + + :raises ValueError: for any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + truthy = {True, "true", "t", "yes", "y", "on", "1", 1} + falsy = {False, "false", "f", "no", "n", "off", "0", 0} + try: + if val in truthy: + return True + if val in falsy: + return False + except TypeError: + # Raised when "val" is not hashable (e.g., lists) + pass + raise ValueError("Cannot convert value to bool: {}".format(val)) diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi index d180e4646..0f58088a3 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -10,3 +10,4 @@ def optional(converter: _ConverterType) -> _ConverterType: ... def default_if_none(default: _T) -> _ConverterType: ... @overload def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... +def to_bool(val: str) -> bool: ... diff --git a/tests/test_converters.py b/tests/test_converters.py index f86e07e29..82c62005a 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -4,14 +4,12 @@ from __future__ import absolute_import -from distutils.util import strtobool - import pytest import attr from attr import Factory, attrib -from attr.converters import default_if_none, optional, pipe +from attr.converters import default_if_none, optional, pipe, to_bool class TestOptional(object): @@ -106,7 +104,7 @@ def test_success(self): """ Succeeds if all wrapped converters succeed. """ - c = pipe(str, strtobool, bool) + c = pipe(str, to_bool, bool) assert True is c("True") is c(True) @@ -114,7 +112,7 @@ def test_fail(self): """ Fails if any wrapped converter fails. """ - c = pipe(str, strtobool) + c = pipe(str, to_bool) # First wrapped converter fails: with pytest.raises(ValueError): @@ -131,8 +129,33 @@ def test_sugar(self): @attr.s class C(object): - a1 = attrib(default="True", converter=pipe(str, strtobool, bool)) - a2 = attrib(default=True, converter=[str, strtobool, bool]) + a1 = attrib(default="True", converter=pipe(str, to_bool, bool)) + a2 = attrib(default=True, converter=[str, to_bool, bool]) c = C() assert True is c.a1 is c.a2 + + +class TestToBool(object): + def test_unhashable(self): + """ + Fails if value is unhashable. + """ + with pytest.raises(ValueError, match="Cannot convert value to bool"): + to_bool([]) + + def test_truthy(self): + """ + Fails if truthy values are incorrectly converted. + """ + assert to_bool("t") + assert to_bool("yes") + assert to_bool("on") + + def test_falsy(self): + """ + Fails if falsy values are incorrectly converted. + """ + assert not to_bool("f") + assert not to_bool("no") + assert not to_bool("off") diff --git a/tests/typing_example.py b/tests/typing_example.py index 2edbce216..9d33ca3f2 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -118,6 +118,20 @@ class Error(Exception): # ConvCDefaultIfNone(None) +# @attr.s +# class ConvCToBool: +# x: int = attr.ib(converter=attr.converters.to_bool) + + +# ConvCToBool(1) +# ConvCToBool(True) +# ConvCToBool("on") +# ConvCToBool("yes") +# ConvCToBool(0) +# ConvCToBool(False) +# ConvCToBool("n") + + # Validators @attr.s class Validated: From fcb7393f53f12a80ff9fcf931b0d16686ed98c58 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 10 Aug 2021 07:48:50 +0200 Subject: [PATCH 020/139] Add changelog for #830, fix filenames --- changelog.d/{815.changes.rst => 815.change.rst} | 0 changelog.d/{819.changes.rst => 819.change.rst} | 0 changelog.d/{828.changes.rst => 828.change.rst} | 0 changelog.d/830.change.rst | 1 + 4 files changed, 1 insertion(+) rename changelog.d/{815.changes.rst => 815.change.rst} (100%) rename changelog.d/{819.changes.rst => 819.change.rst} (100%) rename changelog.d/{828.changes.rst => 828.change.rst} (100%) create mode 100644 changelog.d/830.change.rst diff --git a/changelog.d/815.changes.rst b/changelog.d/815.change.rst similarity index 100% rename from changelog.d/815.changes.rst rename to changelog.d/815.change.rst diff --git a/changelog.d/819.changes.rst b/changelog.d/819.change.rst similarity index 100% rename from changelog.d/819.changes.rst rename to changelog.d/819.change.rst diff --git a/changelog.d/828.changes.rst b/changelog.d/828.change.rst similarity index 100% rename from changelog.d/828.changes.rst rename to changelog.d/828.change.rst diff --git a/changelog.d/830.change.rst b/changelog.d/830.change.rst new file mode 100644 index 000000000..06d454498 --- /dev/null +++ b/changelog.d/830.change.rst @@ -0,0 +1 @@ +Added ``attr.converters.to_bool()``. From b8f5cd780bc2b00566eb6547b654ef6ebb8dc2f8 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sun, 19 Sep 2021 08:09:35 -0700 Subject: [PATCH 021/139] Fix mypy tests. (#844) Looks like the ordering changed --- tests/test_mypy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index ca17b0a66..fb2d1ecbf 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -785,10 +785,10 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:15: error: Cannot determine __init__ type from converter main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" - main:16: error: Cannot determine __init__ type from converter + main:15: error: Cannot determine __init__ type from converter main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" + main:16: error: Cannot determine __init__ type from converter main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingBadConverterReprocess @@ -814,10 +814,10 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:16: error: Cannot determine __init__ type from converter main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" - main:17: error: Cannot determine __init__ type from converter + main:16: error: Cannot determine __init__ type from converter main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" + main:17: error: Cannot determine __init__ type from converter main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingUnsupportedConverter From 7c99a4953085186790a28dd441a93dc665f61269 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 19 Sep 2021 17:47:46 +0100 Subject: [PATCH 022/139] update NASA url (#840) Co-authored-by: Hynek Schlawack --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2ab4cff35..818dd63ed 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ .. teaser-begin ``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder `_ methods). -`Trusted by NASA `_ for Mars missions since 2020! +`Trusted by NASA `_ for Mars missions since 2020! Its main goal is to help you to write **concise** and **correct** software without slowing down your code. From f31bb2850a520993396ebdae598056fb3e3af6f8 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sun, 19 Sep 2021 12:04:55 -0700 Subject: [PATCH 023/139] Fix bug in resolve_types with subclasses (#843) * Fix bug in resolve_types with subclasses * lint * changelog * lint again, like we did last summer * Moar coverage * lint ... why Co-authored-by: Hynek Schlawack --- changelog.d/843.change.rst | 2 ++ src/attr/_funcs.py | 12 ++++++------ tests/test_annotations.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 changelog.d/843.change.rst diff --git a/changelog.d/843.change.rst b/changelog.d/843.change.rst new file mode 100644 index 000000000..746950180 --- /dev/null +++ b/changelog.d/843.change.rst @@ -0,0 +1,2 @@ +``attr.resolve_types()`` now resolves types of subclasses after the parents are resolved. +`#842 `_ diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index fda508c5c..73271c5d5 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -377,11 +377,9 @@ class and you didn't pass any attribs. .. versionadded:: 21.1.0 *attribs* """ - try: - # Since calling get_type_hints is expensive we cache whether we've - # done it already. - cls.__attrs_types_resolved__ - except AttributeError: + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: import typing hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) @@ -389,7 +387,9 @@ class and you didn't pass any attribs. if field.name in hints: # Since fields have been frozen we must work around it. _obj_setattr(field, "type", hints[field.name]) - cls.__attrs_types_resolved__ = True + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls # Return the class so you can use it as a decorator too. return cls diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 2df6311b2..dd815228d 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -618,6 +618,40 @@ class C: with pytest.raises(NameError): typing.get_type_hints(C.__init__) + def test_inheritance(self): + """ + Subclasses can be resolved after the parent is resolved. + """ + + @attr.define() + class A: + n: "int" + + @attr.define() + class B(A): + pass + + attr.resolve_types(A) + attr.resolve_types(B) + + assert int == attr.fields(A).n.type + assert int == attr.fields(B).n.type + + def test_resolve_twice(self): + """ + You can call resolve_types as many times as you like. + This test is here mostly for coverage. + """ + + @attr.define() + class A: + n: "int" + + attr.resolve_types(A) + assert int == attr.fields(A).n.type + attr.resolve_types(A) + assert int == attr.fields(A).n.type + @pytest.mark.parametrize( "annot", From f57b6a6759827a0ec56729da27ec39e9e3b006f3 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Wed, 22 Sep 2021 21:58:32 +0200 Subject: [PATCH 024/139] Convert transformed attrs to AttrsClass (#824) * Convert transformed attrs to AttrsClass Fixes: #821 * Add cangelog entry * Only call AttrsClass once * Calm mypy by inline the AttrsClass call * Defer AttrsClass creation as long as possible Co-authored-by: Hynek Schlawack --- changelog.d/824.changes.rst | 1 + src/attr/_make.py | 14 ++++++++------ tests/test_hooks.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 changelog.d/824.changes.rst diff --git a/changelog.d/824.changes.rst b/changelog.d/824.changes.rst new file mode 100644 index 000000000..4d3e6acda --- /dev/null +++ b/changelog.d/824.changes.rst @@ -0,0 +1 @@ +Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again. diff --git a/src/attr/_make.py b/src/attr/_make.py index 95a37ea64..ac6bbc10e 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -581,15 +581,11 @@ def _transform_attrs( cls, {a.name for a in own_attrs} ) - attr_names = [a.name for a in base_attrs + own_attrs] - - AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) - if kw_only: own_attrs = [a.evolve(kw_only=True) for a in own_attrs] base_attrs = [a.evolve(kw_only=True) for a in base_attrs] - attrs = AttrsClass(base_attrs + own_attrs) + attrs = base_attrs + own_attrs # Mandatory vs non-mandatory attr order only matters when they are part of # the __init__ signature and when they aren't kw_only (which are moved to @@ -608,7 +604,13 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) - return _Attributes((attrs, base_attrs, base_attr_map)) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) if PYPY: diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 5fcb10047..7e5ac3d9e 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -117,6 +117,22 @@ class Sub(Base): assert attr.asdict(Sub(2)) == {"y": 2} + def test_attrs_attrclass(self): + """ + The list of attrs returned by a field_transformer is converted to + "AttrsClass" again. + + Regression test for #821. + """ + + @attr.s(auto_attribs=True, field_transformer=lambda c, a: list(a)) + class C: + x: int + + fields_type = type(attr.fields(C)) + assert fields_type.__name__ == "CAttributes" + assert issubclass(fields_type, tuple) + class TestAsDictHook: def test_asdict(self): From 52fcad29bbb148606b13f6ef00e91bf45b90f75c Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Fri, 24 Sep 2021 12:47:19 +0200 Subject: [PATCH 025/139] Add additonal validators (#845) * Add additonal validators * Python 2 \o/ * Add changelog entry * Add "versionadded" tags * More python 2 * Add doctests and rename maxlen to max_len Co-authored-by: Hynek Schlawack --- changelog.d/845.change.rst | 1 + docs/api.rst | 80 +++++++++++++++++++ src/attr/validators.py | 111 ++++++++++++++++++++++++++ src/attr/validators.pyi | 5 ++ tests/test_validators.py | 158 ++++++++++++++++++++++++++++++++++++- 5 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 changelog.d/845.change.rst diff --git a/changelog.d/845.change.rst b/changelog.d/845.change.rst new file mode 100644 index 000000000..2dab9cb32 --- /dev/null +++ b/changelog.d/845.change.rst @@ -0,0 +1 @@ +Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and `maxlen(n)`. diff --git a/docs/api.rst b/docs/api.rst index 8ffd2dc95..817c60c6d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -345,6 +345,86 @@ Validators ``attrs`` comes with some common validators in the ``attrs.validators`` module: +.. autofunction:: attr.validators.lt + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.lt(42)) + >>> C(41) + C(x=41) + >>> C(42) + Traceback (most recent call last): + ... + ValueError: ("'x' must be < 42: 42") + +.. autofunction:: attr.validators.le + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.le(42)) + >>> C(42) + C(x=42) + >>> C(43) + Traceback (most recent call last): + ... + ValueError: ("'x' must be <= 42: 43") + +.. autofunction:: attr.validators.ge + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.ge(42)) + >>> C(42) + C(x=42) + >>> C(41) + Traceback (most recent call last): + ... + ValueError: ("'x' must be => 42: 41") + +.. autofunction:: attr.validators.gt + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.gt(42)) + >>> C(43) + C(x=43) + >>> C(42) + Traceback (most recent call last): + ... + ValueError: ("'x' must be > 42: 42") + +.. autofunction:: attr.validators.max_len + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(validator=attr.validators.max_len(4)) + >>> C("spam") + C(x='spam') + >>> C("bacon") + Traceback (most recent call last): + ... + ValueError: ("Length of 'x' must be <= 4: 5") + .. autofunction:: attr.validators.instance_of diff --git a/src/attr/validators.py b/src/attr/validators.py index b9a73054e..8c28fb057 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import operator import re from ._make import _AndValidator, and_, attrib, attrs @@ -14,10 +15,15 @@ "and_", "deep_iterable", "deep_mapping", + "ge", + "gt", "in_", "instance_of", "is_callable", + "le", + "lt", "matches_re", + "max_len", "optional", "provides", ] @@ -377,3 +383,108 @@ def deep_mapping(key_validator, value_validator, mapping_validator=None): :raises TypeError: if any sub-validators fail """ return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator(object): + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + raise ValueError( + "'{name}' must be {op} {bound}: {value}".format( + name=attr.name, + op=self.compare_op, + bound=self.bound, + value=value, + ) + ) + + def __repr__(self): + return "".format( + op=self.compare_op, bound=self.bound + ) + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number larger or equal to *val*. + + :param val: Exclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number greater than *val*. + + :param val: Inclusive upper bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller than *val*. + + :param val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called + with a number smaller or equal to *val*. + + :param val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator(object): + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + raise ValueError( + "Length of '{name}' must be <= {max}: {len}".format( + name=attr.name, max=self.max_length, len=len(value) + ) + ) + + def __repr__(self): + return "".format(max=self.max_length) + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + :param int length: Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 14101bf3b..0f7d093e1 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -65,3 +65,8 @@ def deep_mapping( mapping_validator: Optional[_ValidatorType[_M]] = ..., ) -> _ValidatorType[_M]: ... def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... diff --git a/tests/test_validators.py b/tests/test_validators.py index 4aeec9990..bee4d70a8 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -10,17 +10,22 @@ import attr -from attr import has +from attr import fields, has from attr import validators as validator_module from attr._compat import PY2, TYPE from attr.validators import ( and_, deep_iterable, deep_mapping, + ge, + gt, in_, instance_of, is_callable, + le, + lt, matches_re, + max_len, optional, provides, ) @@ -709,3 +714,154 @@ def test_hashability(): hash_func = getattr(obj, "__hash__", None) assert hash_func is not None assert hash_func is not object.__hash__ + + +class TestLtLeGeGt: + """ + Tests for `max_len`. + """ + + BOUND = 4 + + def test_in_all(self): + """ + validator is in ``__all__``. + """ + assert all( + f.__name__ in validator_module.__all__ for f in [lt, le, ge, gt] + ) + + @pytest.mark.parametrize("v", [lt, le, ge, gt]) + def test_retrieve_bound(self, v): + """ + The configured bound for the comparison can be extracted from the + Attribute. + """ + + @attr.s + class Tester(object): + value = attr.ib(validator=v(self.BOUND)) + + assert fields(Tester).value.validator.bound == self.BOUND + + @pytest.mark.parametrize( + "v, value", + [ + (lt, 3), + (le, 3), + (le, 4), + (ge, 4), + (ge, 5), + (gt, 5), + ], + ) + def test_check_valid(self, v, value): + """Silent if value {op} bound.""" + + @attr.s + class Tester(object): + value = attr.ib(validator=v(self.BOUND)) + + Tester(value) # shouldn't raise exceptions + + @pytest.mark.parametrize( + "v, value", + [ + (lt, 4), + (le, 5), + (ge, 3), + (gt, 4), + ], + ) + def test_check_invalid(self, v, value): + """Raise ValueError if value {op} bound.""" + + @attr.s + class Tester(object): + value = attr.ib(validator=v(self.BOUND)) + + with pytest.raises(ValueError): + Tester(value) + + @pytest.mark.parametrize("v", [lt, le, ge, gt]) + def test_repr(self, v): + """ + __repr__ is meaningful. + """ + nv = v(23) + assert repr(nv) == "".format( + op=nv.compare_op, bound=23 + ) + + +class TestMaxLen: + """ + Tests for `max_len`. + """ + + MAX_LENGTH = 4 + + def test_in_all(self): + """ + validator is in ``__all__``. + """ + assert max_len.__name__ in validator_module.__all__ + + def test_retrieve_max_len(self): + """ + The configured max. length can be extracted from the Attribute + """ + + @attr.s + class Tester(object): + value = attr.ib(validator=max_len(self.MAX_LENGTH)) + + assert fields(Tester).value.validator.max_length == self.MAX_LENGTH + + @pytest.mark.parametrize( + "value", + [ + "", + "foo", + "spam", + [], + list(range(MAX_LENGTH)), + {"spam": 3, "eggs": 4}, + ], + ) + def test_check_valid(self, value): + """ + Silent if len(value) <= max_len. + Values can be strings and other iterables. + """ + + @attr.s + class Tester(object): + value = attr.ib(validator=max_len(self.MAX_LENGTH)) + + Tester(value) # shouldn't raise exceptions + + @pytest.mark.parametrize( + "value", + [ + "bacon", + list(range(6)), + ], + ) + def test_check_invalid(self, value): + """ + Raise ValueError if len(value) > max_len. + """ + + @attr.s + class Tester(object): + value = attr.ib(validator=max_len(self.MAX_LENGTH)) + + with pytest.raises(ValueError): + Tester(value) + + def test_repr(self): + """ + __repr__ is meaningful. + """ + assert repr(max_len(23)) == "" From 407c5e3d9e87246497ebe17da6bac72bacfb3af6 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 2 Oct 2021 14:25:49 +0200 Subject: [PATCH 026/139] pre-commit autoupdate --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63603e634..3062c4645 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,28 @@ --- repos: - repo: https://github.com/psf/black - rev: 21.5b2 + rev: 21.9b0 hooks: - id: black exclude: tests/test_pattern_matching.py language_version: python3.8 - repo: https://github.com/PyCQA/isort - rev: 5.8.0 + rev: 5.9.3 hooks: - id: isort additional_dependencies: [toml] files: \.py$ language_version: python3.10 - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://gitlab.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 language_version: python3.10 - repo: https://github.com/econchick/interrogate - rev: 1.4.0 + rev: 1.5.0 hooks: - id: interrogate exclude: tests/test_pattern_matching.py From 44ea327ab7f3a9c33578b1895f65314a81858dae Mon Sep 17 00:00:00 2001 From: Jasper Spaans Date: Mon, 4 Oct 2021 16:23:50 +0200 Subject: [PATCH 027/139] Make `from attr import *` work again on recent python versions. (#848) * Make `from attr import *` work again on recent python versions. * Move import * test into a function and mark some flake8 exceptions. * Please the linters. * Update attr_import_star.py test was renamed, rename it in the comment as well --- src/attr/__init__.py | 4 ++-- tests/attr_import_star.py | 8 ++++++++ tests/test_import.py | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/attr_import_star.py create mode 100644 tests/test_import.py diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 3a037a492..391036bf0 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -73,6 +73,6 @@ ] if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable + from ._next_gen import define, field, frozen, mutable # noqa: F401 - __all__.extend((define, field, frozen, mutable)) + __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/tests/attr_import_star.py b/tests/attr_import_star.py new file mode 100644 index 000000000..810f6c07c --- /dev/null +++ b/tests/attr_import_star.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from attr import * # noqa: F401,F403 + + +# This is imported by test_import::test_from_attr_import_star; this must +# be done indirectly because importing * is only allowed on module level, +# so can't be done inside a test. diff --git a/tests/test_import.py b/tests/test_import.py new file mode 100644 index 000000000..bd2cb4aec --- /dev/null +++ b/tests/test_import.py @@ -0,0 +1,8 @@ +class TestImportStar(object): + def test_from_attr_import_star(self): + """ + import * from attr + """ + # attr_import_star contains `from attr import *`, which cannot + # be done here because *-imports are only allowed on module level. + from . import attr_import_star # noqa: F401 From 124c20cde0c7099494449e62f56781c8cb499570 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 6 Oct 2021 12:21:36 +0200 Subject: [PATCH 028/139] Document Attribute.eq_key & .order_key (#847) * Document Attribute.eq_key & .order_key Fixes #839 Signed-off-by: Hynek Schlawack * Add for example --- docs/api.rst | 2 ++ docs/comparison.rst | 2 ++ src/attr/_make.py | 23 +++++++++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 817c60c6d..c25e5d54f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -99,6 +99,8 @@ Core .. autoclass:: attr.Attribute :members: evolve + For example: + .. doctest:: >>> import attr diff --git a/docs/comparison.rst b/docs/comparison.rst index b2b457bab..c82f014ba 100644 --- a/docs/comparison.rst +++ b/docs/comparison.rst @@ -7,6 +7,8 @@ For that, ``attrs`` writes ``__eq__`` and ``__ne__`` methods for you. Additionally, if you pass ``order=True`` (which is the default if you use the `attr.s` decorator), ``attrs`` will also create a full set of ordering methods that are based on the defined fields: ``__le__``, ``__lt__``, ``__ge__``, and ``__gt__``. +.. _custom-comparison: + Customization ------------- diff --git a/src/attr/_make.py b/src/attr/_make.py index ac6bbc10e..9dba4e835 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2573,19 +2573,26 @@ class Attribute(object): """ *Read-only* representation of an attribute. + The class has *all* arguments of `attr.ib` (except for ``factory`` + which is only syntactic sugar for ``default=Factory(...)`` plus the + following: + + - ``name`` (`str`): The name of the attribute. + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables + that are used for comparing and ordering objects by this attribute, + respectively. These are set by passing a callable to `attr.ib`'s ``eq``, + ``order``, or ``cmp`` arguments. See also :ref:`comparison customization + `. + Instances of this class are frequently used for introspection purposes like: - `fields` returns a tuple of them. - Validators get them passed as the first argument. - - The *field transformer* hook receives a list of them. - - :attribute name: The name of the attribute. - :attribute inherited: Whether or not that attribute has been inherited from - a base class. - - Plus *all* arguments of `attr.ib` (except for ``factory`` - which is only syntactic sugar for ``default=Factory(...)``. + - The :ref:`field transformer ` hook receives a list of + them. .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* From 68be70662131907cabaa1276664ff53f6bc4d4a1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 12 Oct 2021 01:42:46 -0400 Subject: [PATCH 029/139] Avoid attr.attr.frozen and attr.attr.mutable in Sphinx object inventory (#851) https://github.com/altendky/qtrio/issues/269 --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c25e5d54f..d712e4d31 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -715,13 +715,13 @@ Since the Python ecosystem has settled on the term ``field`` for defining attrib The new APIs build on top of them. .. autofunction:: attr.define -.. function:: attr.mutable(same_as_define) +.. function:: mutable(same_as_define) Alias for `attr.define`. .. versionadded:: 20.1.0 -.. function:: attr.frozen(same_as_define) +.. function:: frozen(same_as_define) Behaves the same as `attr.define` but sets *frozen=True* and *on_setattr=None*. From 2eb5d97ef3e4cef5e813eb2ebbeb010bfc255ec5 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 12 Oct 2021 08:55:26 -0400 Subject: [PATCH 030/139] create a :mod:`attr` and :mod:`attrs` for intersphinx links to the package itself (#850) * Try to create a :mod:`attr` and :mod:`attrs` https://github.com/python-attrs/attrs/issues/849 * Update index.rst * Update index.rst * Update index.rst --- docs/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 2700045ef..679f498dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,6 @@ +.. module:: attr +.. module:: attrs + ====================================== ``attrs``: Classes Without Boilerplate ====================================== From 6e5c6175b5a14d4cea28f1ef114494f1dfa56dc2 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 14 Oct 2021 11:35:40 +0200 Subject: [PATCH 031/139] Switch codecov to new version (#852) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a33efec2d..c740a2e78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: if: "contains(env.USING_COVERAGE, matrix.python-version)" - name: "Upload coverage to Codecov" if: "contains(env.USING_COVERAGE, matrix.python-version)" - uses: "codecov/codecov-action@v1" + uses: "codecov/codecov-action@v2" with: fail_ci_if_error: true From 03c9615a55c8defe1f5243a3ab7c8ece318daec5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 14 Oct 2021 11:37:14 +0200 Subject: [PATCH 032/139] Python 3.10 is final --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c740a2e78..2bbd1ce2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,12 +13,12 @@ jobs: name: "Python ${{ matrix.python-version }}" runs-on: "ubuntu-latest" env: - USING_COVERAGE: "2.7,3.7,3.8,3.10.0-beta - 3.10" + USING_COVERAGE: "2.7,3.7,3.8,3.10" strategy: fail-fast: false matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-beta - 3.10", "pypy2", "pypy3"] + python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy2", "pypy3"] steps: - uses: "actions/checkout@v2" @@ -40,7 +40,7 @@ jobs: # parsing errors in older versions for modern code. - uses: "actions/setup-python@v2" with: - python-version: "3.10.0-beta - 3.10" + python-version: "3.10" - name: "Combine coverage" run: | From 5c78fbc1547e18ace227085600f1fc11090e072a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 14 Oct 2021 13:02:59 +0200 Subject: [PATCH 033/139] Fix typo --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bbd1ce2c..b64d7514e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,7 +65,7 @@ jobs: with: python-version: "3.9" - - name: "Install build, check-wheel-content, and twine" + - name: "Install build, check-wheel-contents, and twine" run: "python -m pip install build twine check-wheel-contents" - name: "Build package" run: "python -m build --sdist --wheel ." From 95b70bde2ed18f4ddd78cb42ebc0bb578f111b56 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Oct 2021 07:00:01 +0200 Subject: [PATCH 034/139] Clarify that validators/converters only run on init --- docs/examples.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index 0fac312a0..4e40fac32 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -440,6 +440,9 @@ Therefore if you use ``@attr.s(auto_attribs=True)``, it is *not* enough to decor ... 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 `attr.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. @@ -458,6 +461,8 @@ This can be useful for doing type-conversions on values that you don't want to f >>> o.x 1 +Please note that converters only run on initialization. + Check out `converters` for more details. From 84eb79905f5bd67694901b144f63ef3c20e67c2c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Oct 2021 07:06:57 +0200 Subject: [PATCH 035/139] Fix Python 2/2020 tense --- docs/python-2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python-2.rst b/docs/python-2.rst index 4fdf2c9b2..7ec9e5112 100644 --- a/docs/python-2.rst +++ b/docs/python-2.rst @@ -1,7 +1,7 @@ Python 2 Statement ================== -While ``attrs`` has always been a Python 3-first package, we the maintainers are aware that Python 2 will not magically disappear in 2020. +While ``attrs`` has always been a Python 3-first package, we the maintainers are aware that Python 2 has not magically disappeared in 2020. We are also aware that ``attrs`` is an important building block in many people's systems and livelihoods. As such, we do **not** have any immediate plans to drop Python 2 support in ``attrs``. From 8592ea6aa5069c7a101c2ad54ce205050f799216 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Oct 2021 07:11:16 +0200 Subject: [PATCH 036/139] Stack Overflow is two words --- .github/CONTRIBUTING.rst | 2 +- README.rst | 2 +- docs/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 5bbb94910..c97c5cf46 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -12,7 +12,7 @@ Support ------- In case you'd like to help out but don't want to deal with GitHub, there's a great opportunity: -help your fellow developers on `StackOverflow `_! +help your fellow developers on `Stack Overflow `_! The official tag is ``python-attrs`` and helping out in support frees us up to improve ``attrs`` instead! diff --git a/README.rst b/README.rst index 818dd63ed..2f77fc641 100644 --- a/README.rst +++ b/README.rst @@ -90,7 +90,7 @@ Never again violate the `single responsibility principle `_ to get help. +Please use the ``python-attrs`` tag on `Stack Overflow `_ to get help. Answering questions of your fellow developers is also a great way to help the project! diff --git a/docs/index.rst b/docs/index.rst index 679f498dc..6b9948cd4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ The next three steps should bring you up and running in no time: - If at any point you get confused by some terminology, please check out our `glossary`. -If you need any help while getting started, feel free to use the ``python-attrs`` tag on `StackOverflow `_ and someone will surely help you out! +If you need any help while getting started, feel free to use the ``python-attrs`` tag on `Stack Overflow `_ and someone will surely help you out! Day-to-Day Usage From e1d156239f2c8c2f73e499fba206015641810ccc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Oct 2021 07:16:37 +0200 Subject: [PATCH 037/139] Fix rst --- changelog.d/845.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/845.change.rst b/changelog.d/845.change.rst index 2dab9cb32..80f3f7da9 100644 --- a/changelog.d/845.change.rst +++ b/changelog.d/845.change.rst @@ -1 +1 @@ -Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and `maxlen(n)`. +Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and ``maxlen(n)``. From 82ab0c1d11e53eaedfff1ea94878f0866fddcff4 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sat, 30 Oct 2021 12:05:49 -0700 Subject: [PATCH 038/139] Fix mypy tests. Looks like the tests are error order sensitive (#856) --- tests/test_mypy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index fb2d1ecbf..ca17b0a66 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -785,10 +785,10 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:15: error: Cannot determine __init__ type from converter - main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" + main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:16: error: Cannot determine __init__ type from converter + main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingBadConverterReprocess @@ -814,10 +814,10 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:16: error: Cannot determine __init__ type from converter - main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" + main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" main:17: error: Cannot determine __init__ type from converter + main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingUnsupportedConverter From 5a368d82997570bdad2ba371e897cc777809461f Mon Sep 17 00:00:00 2001 From: paul fisher Date: Tue, 2 Nov 2021 03:04:26 -0400 Subject: [PATCH 039/139] Enable use of f-strings in Python 3.6. (#858) CPython 3.6 has f-strings: ``` Python 3.6.13 |Anaconda, Inc.| (default, Jun 4 2021, 14:25:59) [GCC 7.5.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> f"hello" 'hello' ``` --- src/attr/_compat.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 69329e996..cc20246cb 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -8,15 +8,12 @@ PY2 = sys.version_info[0] == 2 PYPY = platform.python_implementation() == "PyPy" -HAS_F_STRINGS = ( - sys.version_info[:2] >= (3, 7) - if not PYPY - else sys.version_info[:2] >= (3, 6) -) +PY36 = sys.version_info[:2] >= (3, 6) +HAS_F_STRINGS = PY36 PY310 = sys.version_info[:2] >= (3, 10) -if PYPY or sys.version_info[:2] >= (3, 6): +if PYPY or PY36: ordered_dict = dict else: from collections import OrderedDict From 9eccd7057162c42666d0bb849a30e09e9fa8cd27 Mon Sep 17 00:00:00 2001 From: paul fisher Date: Wed, 3 Nov 2021 13:44:44 -0400 Subject: [PATCH 040/139] Fix #824's changelog entry filename. (#860) --- changelog.d/{824.changes.rst => 824.change.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{824.changes.rst => 824.change.rst} (100%) diff --git a/changelog.d/824.changes.rst b/changelog.d/824.change.rst similarity index 100% rename from changelog.d/824.changes.rst rename to changelog.d/824.change.rst From 554d6f293d768b7af17b1c6261568b38f2895265 Mon Sep 17 00:00:00 2001 From: paul fisher Date: Thu, 4 Nov 2021 01:52:10 -0400 Subject: [PATCH 041/139] Pull thread-local into _compat module to fix cloudpickling. (#857) Because cloudpickle tries to pickle a function's globals, when it pickled an attrs instance, it would try to pickle the `__repr__` method and its globals, which included a `threading.local`. This broke cloudpickle for all attrs classes unless they explicitly specified `repr=False`. Modules, however, are pickled by reference, not by value, so moving the repr into a different module means we can put `_compat` into the function's globals and not worry about direct references. Includes a test to ensure that attrs and cloudpickle remain compatible. Also adds an explanation of the reason we even *have* that global thread-local variable. It wasn't completely obvious to a reader why the thread-local was needed to track reference cycles in `__repr__` calls, and the test did not previously contain a cycle that touched a non-attrs value. This change adds a comment explaining the need and tests a cycle that contains non-attrs values. Fixes: - https://github.com/python-attrs/attrs/issues/458 - https://github.com/cloudpipe/cloudpickle/issues/320 --- changelog.d/847.change.rst | 2 ++ setup.py | 2 ++ src/attr/_compat.py | 15 ++++++++++ src/attr/_make.py | 56 ++++++++++++++++++------------------- tests/test_compatibility.py | 26 +++++++++++++++++ tests/test_dunders.py | 17 +++++++++++ 6 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 changelog.d/847.change.rst create mode 100644 tests/test_compatibility.py diff --git a/changelog.d/847.change.rst b/changelog.d/847.change.rst new file mode 100644 index 000000000..c4657bffd --- /dev/null +++ b/changelog.d/847.change.rst @@ -0,0 +1,2 @@ +Attrs classes are now fully compatible with cloudpickle. +`#847 `_ diff --git a/setup.py b/setup.py index 79e45bfda..0314ba007 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,8 @@ EXTRAS_REQUIRE = { "docs": ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"], "tests_no_zope": [ + # For regression test to ensure cloudpickle compat doesn't break. + "cloudpickle", # 5.0 introduced toml; parallel was broken until 5.0.2 "coverage[toml]>=5.0.2", "hypothesis", diff --git a/src/attr/_compat.py b/src/attr/_compat.py index cc20246cb..9d03ac196 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -2,6 +2,7 @@ import platform import sys +import threading import types import warnings @@ -243,3 +244,17 @@ def func(): set_closure_cell = make_set_closure_cell() + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() diff --git a/src/attr/_make.py b/src/attr/_make.py index 9dba4e835..c2a92cef3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -4,12 +4,13 @@ import inspect import linecache import sys -import threading import warnings from operator import itemgetter -from . import _config, setters +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters from ._compat import ( HAS_F_STRINGS, PY2, @@ -1864,8 +1865,6 @@ def _add_eq(cls, attrs=None): return cls -_already_repring = threading.local() - if HAS_F_STRINGS: def _make_repr(attrs, ns, cls): @@ -1883,7 +1882,7 @@ def _make_repr(attrs, ns, cls): for name, r, _ in attr_names_with_reprs if r != repr } - globs["_already_repring"] = _already_repring + globs["_compat"] = _compat globs["AttributeError"] = AttributeError globs["NOTHING"] = NOTHING attribute_fragments = [] @@ -1908,24 +1907,23 @@ def _make_repr(attrs, ns, cls): else: cls_name_fragment = ns + ".{self.__class__.__name__}" - lines = [] - lines.append("def __repr__(self):") - lines.append(" try:") - lines.append(" working_set = _already_repring.working_set") - lines.append(" except AttributeError:") - lines.append(" working_set = {id(self),}") - lines.append(" _already_repring.working_set = working_set") - lines.append(" else:") - lines.append(" if id(self) in working_set:") - lines.append(" return '...'") - lines.append(" else:") - lines.append(" working_set.add(id(self))") - lines.append(" try:") - lines.append( - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment) - ) - lines.append(" finally:") - lines.append(" working_set.remove(id(self))") + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + " finally:", + " already_repring.remove(id(self))", + ] return _make_method( "__repr__", "\n".join(lines), unique_filename, globs=globs @@ -1954,12 +1952,12 @@ def __repr__(self): Automatically created by attrs. """ try: - working_set = _already_repring.working_set + already_repring = _compat.repr_context.already_repring except AttributeError: - working_set = set() - _already_repring.working_set = working_set + already_repring = set() + _compat.repr_context.already_repring = already_repring - if id(self) in working_set: + if id(self) in already_repring: return "..." real_cls = self.__class__ if ns is None: @@ -1979,7 +1977,7 @@ def __repr__(self): # for the duration of this call, it's safe to depend on id(...) # stability, and not need to track the instance and therefore # worry about properties like weakref- or hash-ability. - working_set.add(id(self)) + already_repring.add(id(self)) try: result = [class_name, "("] first = True @@ -1993,7 +1991,7 @@ def __repr__(self): ) return "".join(result) + ")" finally: - working_set.remove(id(self)) + already_repring.remove(id(self)) return __repr__ diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py new file mode 100644 index 000000000..0dab852ec --- /dev/null +++ b/tests/test_compatibility.py @@ -0,0 +1,26 @@ +""" +Tests for compatibility against other Python modules. +""" + +import cloudpickle + +from hypothesis import given + +from .strategies import simple_classes + + +class TestCloudpickleCompat(object): + """ + Tests for compatibility with ``cloudpickle``. + """ + + @given(simple_classes()) + def test_repr(self, cls): + """ + attrs instances can be pickled and un-pickled with cloudpickle. + """ + inst = cls() + # Exact values aren't a concern so long as neither direction + # raises an exception. + pkl = cloudpickle.dumps(inst) + cloudpickle.loads(pkl) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 3a8300399..57d33bef1 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -367,6 +367,23 @@ class Cycle(object): cycle.cycle = cycle assert "Cycle(value=7, cycle=...)" == repr(cycle) + def test_infinite_recursion_long_cycle(self): + """ + A cyclic graph can pass through other non-attrs objects, and repr will + still emit an ellipsis and not raise an exception. + """ + + @attr.s + class LongCycle(object): + value = attr.ib(default=14) + cycle = attr.ib(default=None) + + cycle = LongCycle() + # Ensure that the reference cycle passes through a non-attrs object. + # This demonstrates the need for a thread-local "global" ID tracker. + cycle.cycle = {"cycle": [cycle]} + assert "LongCycle(value=14, cycle={'cycle': [...]})" == repr(cycle) + def test_underscores(self): """ repr does not strip underscores. From 11c66eff32ddf0c44cca59a9da40ddddbc2336c4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 4 Nov 2021 06:53:23 +0100 Subject: [PATCH 042/139] Update 847.change.rst --- changelog.d/847.change.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.d/847.change.rst b/changelog.d/847.change.rst index c4657bffd..d8c4e9bd2 100644 --- a/changelog.d/847.change.rst +++ b/changelog.d/847.change.rst @@ -1,2 +1 @@ -Attrs classes are now fully compatible with cloudpickle. -`#847 `_ +``attrs`` classes are now fully compatible with `cloudpickle `_ (no need to disabled ``repr`` anymore). From d884af5e6d4125ade46e516859f100c194838484 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 4 Nov 2021 06:57:07 +0100 Subject: [PATCH 043/139] Rename 847.change.rst to 857.change.rst --- changelog.d/{847.change.rst => 857.change.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{847.change.rst => 857.change.rst} (100%) diff --git a/changelog.d/847.change.rst b/changelog.d/857.change.rst similarity index 100% rename from changelog.d/847.change.rst rename to changelog.d/857.change.rst From f788c84c6cb2dee3303d2dcdbb13c8081c3b67f8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 10 Nov 2021 07:15:48 +0100 Subject: [PATCH 044/139] At least move the readme to NG Signed-off-by: Hynek Schlawack --- README.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 2f77fc641..bfdac26db 100644 --- a/README.rst +++ b/README.rst @@ -35,12 +35,13 @@ For that, it gives you a class decorator and a way to declaratively define the a .. code-block:: pycon + >>> from typing import List >>> import attr - >>> @attr.s - ... class SomeClass(object): - ... a_number = attr.ib(default=42) - ... list_of_numbers = attr.ib(factory=list) + >>> @attr.define + ... class SomeClass: + ... a_number: int = 42 + ... list_of_numbers: List[int] = attr.Factory(list) ... ... def hard_math(self, another_number): ... return self.a_number + sum(self.list_of_numbers) * another_number @@ -78,12 +79,15 @@ After *declaring* your attributes ``attrs`` gives you: *without* writing dull boilerplate code again and again and *without* runtime performance penalties. -On Python 3.6 and later, you can often even drop the calls to ``attr.ib()`` by using `type annotations `_. - 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. +---- + +In case you're wondering: this example uses ``attrs``'s `modern APIs `_ that have been introduced in version 20.1.0. +The old APIs (``@attr.s``, ``attr.ib``) will remain indefinitely. +`Type annotations `_ will also stay entirely **optional** forever. .. -getting-help- From b0e6c2c5244b04c5725e8bf83449fd4c25679714 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 10 Nov 2021 07:25:01 +0100 Subject: [PATCH 045/139] Euphemism + serious business --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bfdac26db..4eb77100c 100644 --- a/README.rst +++ b/README.rst @@ -86,7 +86,7 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. -The old APIs (``@attr.s``, ``attr.ib``) will remain indefinitely. +The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, and ``attr.attrib``) will remain indefinitely. `Type annotations `_ will also stay entirely **optional** forever. .. -getting-help- From 322e1904c1ac6f25b835b691e10cf06972c70e23 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 11 Nov 2021 05:58:46 +0100 Subject: [PATCH 046/139] Move docs to 3.8 This is the latest version currently supported by RTD. --- .readthedocs.yml | 2 +- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 511ae165f..009da9ffa 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,7 @@ version: 2 python: # Keep version in sync with tox.ini (docs and gh-actions). - version: 3.7 + version: 3.8 install: - method: pip diff --git a/tox.ini b/tox.ini index 3dfd139c9..b8924623c 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ python = 2.7: py27 3.5: py35 3.6: py36 - 3.7: py37, docs - 3.8: py38, manifest, typing, changelog + 3.7: py37 + 3.8: py38, manifest, typing, changelog, docs 3.9: py39, pyright 3.10: py310, lint pypy2: pypy @@ -93,7 +93,7 @@ commands = [testenv:docs] # Keep basepython in sync with gh-actions and .readthedocs.yml. -basepython = python3.7 +basepython = python3.8 extras = docs commands = sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html From ad5da29d664a2846bff6aa53fdc7041c97f55a87 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 13 Nov 2021 07:43:32 +0100 Subject: [PATCH 047/139] Enable other doc formats --- .readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 009da9ffa..d80f42170 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,7 @@ --- version: 2 +formats: all + python: # Keep version in sync with tox.ini (docs and gh-actions). version: 3.8 From 031ba3604894bf6e87137b43a418c5a2b451e424 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 15 Nov 2021 09:58:33 +0100 Subject: [PATCH 048/139] Add epub metadata --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index ae65fbbc2..1ae842349 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -159,7 +159,7 @@ def find_version(*file_paths): u"attrs Documentation", u"Hynek Schlawack", "attrs", - "One line description of project.", + "Python Clases Without Boilerplate", "Miscellaneous", ) ] From a76f750d7785e228e85212554272eef34c51c3a0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 15 Nov 2021 09:59:21 +0100 Subject: [PATCH 049/139] We build docs only on Python 3 --- docs/conf.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1ae842349..0a1eeb330 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import codecs import os import re @@ -66,8 +64,8 @@ def find_version(*file_paths): master_doc = "index" # General information about the project. -project = u"attrs" -copyright = u"2015, Hynek Schlawack" +project = "attrs" +copyright = "2015, Hynek Schlawack" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -75,7 +73,7 @@ def find_version(*file_paths): # # The short X.Y version. release = find_version("../src/attr/__init__.py") -version = release.rsplit(u".", 1)[0] +version = release.rsplit(".", 1)[0] # The full version, including alpha/beta/rc tags. # List of patterns, relative to source directory, that match files and @@ -142,9 +140,7 @@ def find_version(*file_paths): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ("index", "attrs", u"attrs Documentation", [u"Hynek Schlawack"], 1) -] +man_pages = [("index", "attrs", "attrs Documentation", ["Hynek Schlawack"], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -156,8 +152,8 @@ def find_version(*file_paths): ( "index", "attrs", - u"attrs Documentation", - u"Hynek Schlawack", + "attrs Documentation", + "Hynek Schlawack", "attrs", "Python Clases Without Boilerplate", "Miscellaneous", From c4587c07935eea8bd7b520da52fd30e082d63a30 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 15 Nov 2021 10:08:16 +0100 Subject: [PATCH 050/139] Add missing author metadata --- docs/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0a1eeb330..7d5838b10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,8 @@ def find_version(*file_paths): # General information about the project. project = "attrs" -copyright = "2015, Hynek Schlawack" +author = "Hynek Schlawack" +copyright = f"2015, {author}" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -160,6 +161,8 @@ def find_version(*file_paths): ) ] +epub_description = "Python Clases Without Boilerplate" + intersphinx_mapping = { "https://docs.python.org/3": None, } From c6143d518bb4dd41d231e272d799b9a5b458d942 Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Wed, 17 Nov 2021 07:05:01 +0100 Subject: [PATCH 051/139] Add "no_run_validators()" context manager (#859) * Add "no_run_validators()" context manager * Move functions to validators module * Update changelog entry * Add a few docstring improvements and fixes * Update tests/test_validators.py * Minor polish Signed-off-by: Hynek Schlawack Co-authored-by: Hynek Schlawack --- changelog.d/859.change.rst | 4 +++ docs/api.rst | 8 +++++ docs/init.rst | 14 +++++++-- src/attr/_config.py | 8 +++++ src/attr/validators.py | 54 +++++++++++++++++++++++++++++++++ src/attr/validators.pyi | 5 +++ tests/test_validators.py | 62 +++++++++++++++++++++++++++++++++++++- tests/typing_example.py | 15 +++++++++ 8 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 changelog.d/859.change.rst diff --git a/changelog.d/859.change.rst b/changelog.d/859.change.rst new file mode 100644 index 000000000..a79bd984f --- /dev/null +++ b/changelog.d/859.change.rst @@ -0,0 +1,4 @@ +Added new context manager ``attr.validators.disabled()`` and functions ``attr.validators.(set|get)_disabled()``. +They deprecate ``attr.(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/docs/api.rst b/docs/api.rst index d712e4d31..2306b1aa9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -590,6 +590,14 @@ Validators ... TypeError: ("'x' must be (got 7 that is a ).", Attribute(name='x', default=NOTHING, validator=> to >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), , 7) +Validators can be both globally and locally disabled: + +.. autofunction:: attr.validators.set_disabled + +.. autofunction:: attr.validators.get_disabled + +.. autofunction:: attr.validators.disabled + Converters ---------- diff --git a/docs/init.rst b/docs/init.rst index fdbf0e126..bda39f119 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -242,10 +242,20 @@ If you define validators both ways for an attribute, they are both ran: And finally you can disable validators globally: - >>> attr.set_run_validators(False) + >>> attr.validators.set_disabled(True) >>> C("128") C(x='128') - >>> attr.set_run_validators(True) + >>> attr.validators.set_disabled(False) + >>> C("128") + Traceback (most recent call last): + ... + TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), , '128') + +You can achieve the same by using the context manager: + + >>> with attr.validators.disabled(): + ... C("128") + C(x='128') >>> C("128") Traceback (most recent call last): ... diff --git a/src/attr/_config.py b/src/attr/_config.py index 8ec920962..6503f6fb0 100644 --- a/src/attr/_config.py +++ b/src/attr/_config.py @@ -9,6 +9,10 @@ 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()` + instead. """ if not isinstance(run, bool): raise TypeError("'run' must be bool.") @@ -19,5 +23,9 @@ def set_run_validators(run): 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()` + instead. """ return _run_validators diff --git a/src/attr/validators.py b/src/attr/validators.py index 8c28fb057..1eb257225 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -7,6 +7,9 @@ import operator import re +from contextlib import contextmanager + +from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs from .exceptions import NotCallableError @@ -15,7 +18,9 @@ "and_", "deep_iterable", "deep_mapping", + "disabled", "ge", + "get_disabled", "gt", "in_", "instance_of", @@ -26,9 +31,58 @@ "max_len", "optional", "provides", + "set_disabled", ] +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + :param disabled: If ``True``, disable running all validators. + :type disabled: bool + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + :return: ``True`` if validators are currently disabled. + :rtype: bool + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(False) + try: + yield + finally: + set_run_validators(True) + + @attrs(repr=False, slots=True, hash=True) class _InstanceOfValidator(object): type = attrib() diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 0f7d093e1..a4393327c 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -3,6 +3,7 @@ from typing import ( AnyStr, Callable, Container, + ContextManager, Iterable, List, Mapping, @@ -26,6 +27,10 @@ _K = TypeVar("_K") _V = TypeVar("_V") _M = TypeVar("_M", bound=Mapping) +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + # To be more precise on instance_of use some overloads. # If there are more than 3 items in the tuple then we fall back to Any @overload diff --git a/tests/test_validators.py b/tests/test_validators.py index bee4d70a8..e1b83ed42 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -10,7 +10,7 @@ import attr -from attr import fields, has +from attr import _config, fields, has from attr import validators as validator_module from attr._compat import PY2, TYPE from attr.validators import ( @@ -46,6 +46,66 @@ def zope_interface(): return zope.interface +class TestDisableValidators(object): + @pytest.fixture(autouse=True) + def reset_default(self): + """ + Make sure validators are always enabled after a test. + """ + yield + _config._run_validators = True + + def test_default(self): + """ + Run validators by default. + """ + assert _config._run_validators is True + + @pytest.mark.parametrize("value, expected", [(True, False), (False, True)]) + def test_set_validators_diabled(self, value, expected): + """ + Sets `_run_validators`. + """ + validator_module.set_disabled(value) + + assert _config._run_validators is expected + + @pytest.mark.parametrize("value, expected", [(True, False), (False, True)]) + def test_disabled(self, value, expected): + """ + Returns `_run_validators`. + """ + _config._run_validators = value + + assert validator_module.get_disabled() is expected + + def test_disabled_ctx(self): + """ + The `disabled` context manager disables running validators, + but only within its context. + """ + assert _config._run_validators is True + + with validator_module.disabled(): + assert _config._run_validators is False + + assert _config._run_validators is True + + def test_disabled_ctx_with_errors(self): + """ + Running validators is re-enabled even if an error is raised. + """ + assert _config._run_validators is True + + with pytest.raises(ValueError): + with validator_module.disabled(): + assert _config._run_validators is False + + raise ValueError("haha!") + + assert _config._run_validators is True + + class TestInstanceOf(object): """ Tests for `instance_of`. diff --git a/tests/typing_example.py b/tests/typing_example.py index 9d33ca3f2..af124661a 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -183,6 +183,21 @@ class Validated: ) +@attr.define +class Validated2: + num: int = attr.field(validator=attr.validators.ge(0)) + + +with attr.validators.disabled(): + Validated2(num=-1) + +try: + attr.validators.set_disabled(True) + Validated2(num=-1) +finally: + attr.validators.set_disabled(False) + + # Custom repr() @attr.s class WithCustomRepr: From 9bb97cb58e48f4fc2c41952bc00b7ea7949d2d2f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 19 Nov 2021 14:51:46 +0100 Subject: [PATCH 052/139] Repo Janitoring (#868) * Use proper pypy versions * Python 3.10 as main, better/more expressive names Signed-off-by: Hynek Schlawack * Ditch Codecov Signed-off-by: Hynek Schlawack * Don't need this anymore * Streamline tox.ini Signed-off-by: Hynek Schlawack --- .github/workflows/main.yml | 78 +++++++++++++++++++++++--------------- codecov.yml | 10 ----- tox.ini | 59 ++++++++++------------------ 3 files changed, 68 insertions(+), 79 deletions(-) delete mode 100644 codecov.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b64d7514e..c4e2a6490 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,9 +8,14 @@ on: branches: ["main"] workflow_dispatch: +env: + FORCE_COLOR: "1" # Make tools pretty. + TOX_TESTENV_PASSENV: "FORCE_COLOR" + + jobs: tests: - name: "Python ${{ matrix.python-version }}" + name: "tox on ${{ matrix.python-version }}" runs-on: "ubuntu-latest" env: USING_COVERAGE: "2.7,3.7,3.8,3.10" @@ -18,16 +23,16 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy2", "pypy3"] + 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" with: python-version: "${{ matrix.python-version }}" + - name: "Install dependencies" run: | - set -xe python -VV python -m site python -m pip install --upgrade pip setuptools wheel @@ -36,24 +41,41 @@ jobs: - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" - # We always use a modern Python version for combining coverage to prevent - # parsing errors in older versions for modern code. - - uses: "actions/setup-python@v2" + - name: Upload coverage data + uses: "actions/upload-artifact@v2" with: - python-version: "3.10" + name: coverage-data + path: ".coverage.*" + if-no-files-found: ignore - - name: "Combine coverage" - run: | - set -xe - python -m pip install coverage[toml] - python -m coverage combine - python -m coverage xml - if: "contains(env.USING_COVERAGE, matrix.python-version)" - - name: "Upload coverage to Codecov" - if: "contains(env.USING_COVERAGE, matrix.python-version)" - uses: "codecov/codecov-action@v2" + + coverage: + runs-on: "ubuntu-latest" + needs: tests + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: "3.10" # Use latest, so it understands all syntax. + + - run: "python -m pip install --upgrade coverage[toml]" + + - name: "Download coverage data" + uses: actions/download-artifact@v2 + with: + name: coverage-data + + - run: python -m coverage combine + - run: python -m coverage html --skip-covered --skip-empty + + - name: "Upload coverage report" + uses: "actions/upload-artifact@v2" with: - fail_ci_if_error: true + name: html-report + path: htmlcov + + - run: python -m coverage report --fail-under=100 package: name: "Build & verify package" @@ -63,32 +85,28 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: - python-version: "3.9" + python-version: "3.10" - - name: "Install build, check-wheel-contents, and twine" + - name: "Install build and lint tools" run: "python -m pip install build twine check-wheel-contents" - - name: "Build package" - run: "python -m build --sdist --wheel ." - - name: "List result" - run: "ls -l dist" - - name: "Check wheel contents" - run: "check-wheel-contents dist/*.whl" + - 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 }}" strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] - name: "Verify dev env" - runs-on: "${{ matrix.os }}" - steps: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: - python-version: "3.9" + python-version: "3.10" - name: "Install in dev mode" run: "python -m pip install -e .[dev]" - name: "Import package" diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 60a1e5c12..000000000 --- a/codecov.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -comment: false -coverage: - status: - patch: - default: - target: "100" - project: - default: - target: "100" diff --git a/tox.ini b/tox.ini index b8924623c..85420117f 100644 --- a/tox.ini +++ b/tox.ini @@ -14,11 +14,11 @@ python = 3.5: py35 3.6: py36 3.7: py37 - 3.8: py38, manifest, typing, changelog, docs + 3.8: py38, manifest, changelog, docs 3.9: py39, pyright - 3.10: py310, lint - pypy2: pypy - pypy3: pypy3 + 3.10: py310, lint, typing + pypy-2: pypy + pypy-3: pypy3 [tox] @@ -26,38 +26,28 @@ envlist = typing,lint,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,man isolated_build = True +[testenv:docs] +# Keep basepython in sync with gh-actions and .readthedocs.yml. +basepython = python3.8 +extras = docs +commands = + sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html + python -m doctest README.rst + + [testenv] -# Prevent random setuptools/pip breakages like -# https://github.com/pypa/setuptools/issues/1042 from breaking our builds. -setenv = - VIRTUALENV_NO_DOWNLOAD=1 -extras = {env:TOX_AP_TEST_EXTRAS:tests} +extras = tests commands = python -m pytest {posargs} [testenv:py27] -extras = {env:TOX_AP_TEST_EXTRAS:tests} +extras = tests commands = coverage run -m pytest {posargs} [testenv:py37] -# Python 3.6+ has a number of compile-time warnings on invalid string escapes. -# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. -install_command = pip install --no-compile {opts} {packages} -setenv = - PYTHONWARNINGS=d -extras = {env:TOX_AP_TEST_EXTRAS:tests} -commands = coverage run -m pytest {posargs} - - -[testenv:py38] -# Python 3.6+ has a number of compile-time warnings on invalid string escapes. -# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. -basepython = python3.8 -install_command = pip install --no-compile {opts} {packages} -setenv = - PYTHONWARNINGS=d -extras = {env:TOX_AP_TEST_EXTRAS:tests} +extras = tests commands = coverage run -m pytest {posargs} @@ -68,12 +58,13 @@ basepython = python3.10 install_command = pip install --no-compile {opts} {packages} setenv = PYTHONWARNINGS=d -extras = {env:TOX_AP_TEST_EXTRAS:tests} +extras = tests commands = coverage run -m pytest {posargs} [testenv:coverage-report] basepython = python3.10 +depends = py27,py37,py310 skip_install = true deps = coverage[toml]>=5.4 commands = @@ -91,16 +82,6 @@ commands = pre-commit run --all-files -[testenv:docs] -# Keep basepython in sync with gh-actions and .readthedocs.yml. -basepython = python3.8 -extras = docs -commands = - sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html - python -m doctest README.rst - - [testenv:manifest] basepython = python3.8 deps = check-manifest @@ -127,7 +108,7 @@ commands = towncrier --draft [testenv:typing] -basepython = python3.8 +basepython = python3.10 deps = mypy>=0.902 commands = mypy src/attr/__init__.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi From f9835889777bbf246901a6deaf76acbf15d9cf82 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 20 Nov 2021 16:19:23 +0100 Subject: [PATCH 053/139] Don't check dev env on macOS It's slow and I inevitably notice if something breaks. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4e2a6490..a745bb1dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -100,7 +100,7 @@ jobs: runs-on: "${{ matrix.os }}" strategy: matrix: - os: ["ubuntu-latest", "windows-latest", "macos-latest"] + os: ["ubuntu-latest", "windows-latest"] steps: - uses: "actions/checkout@v2" From 675afb8f73b88a3185e79832ac38da8f54894189 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 20 Nov 2021 16:20:07 +0100 Subject: [PATCH 054/139] Don't run pre-commit in CI We've got pre-commit.ci which is much better and faster. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 85420117f..664ec88dc 100644 --- a/tox.ini +++ b/tox.ini @@ -16,13 +16,13 @@ python = 3.7: py37 3.8: py38, manifest, changelog, docs 3.9: py39, pyright - 3.10: py310, lint, typing + 3.10: py310, typing pypy-2: pypy pypy-3: pypy3 [tox] -envlist = typing,lint,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report +envlist = typing,pre-commit,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report isolated_build = True @@ -72,7 +72,7 @@ commands = coverage report -[testenv:lint] +[testenv:pre-commit] basepython = python3.10 skip_install = true deps = From 3d1a07ac9596370e3d1b7b2a15073f032fbc6c75 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 20 Nov 2021 16:20:42 +0100 Subject: [PATCH 055/139] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3062c4645..7282844fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ --- repos: - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.11b1 hooks: - id: black exclude: tests/test_pattern_matching.py language_version: python3.8 - repo: https://github.com/PyCQA/isort - rev: 5.9.3 + rev: 5.10.1 hooks: - id: isort additional_dependencies: [toml] From cf24de8e9baa7615304dc4917d8607111bbec778 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 21 Nov 2021 15:16:28 +0100 Subject: [PATCH 056/139] Rename for clarity --- tests/{test_compatibility.py => test_3rd_party.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_compatibility.py => test_3rd_party.py} (100%) diff --git a/tests/test_compatibility.py b/tests/test_3rd_party.py similarity index 100% rename from tests/test_compatibility.py rename to tests/test_3rd_party.py From d7d9c5dde7486d82204b4c1b85b85fa04bd64088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Mon, 22 Nov 2021 07:35:36 +0100 Subject: [PATCH 057/139] Switch to NG APIs in docs (#863) * Switch to NG APIs in docs * Review feedback * Convert examples.rst * Tweak doctest * Doctest fixes * Tweak some more * Fix * Update docs/init.rst Co-authored-by: Hynek Schlawack * Fix doctest * Fix README * Update docs/examples.rst Co-authored-by: Hynek Schlawack * Review feedback * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix link Co-authored-by: Hynek Schlawack Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.rst | 10 +- docs/comparison.rst | 34 +-- docs/conf.py | 4 + docs/examples.rst | 430 +++++++++++++++++++------------------- docs/extending.rst | 65 +++--- docs/glossary.rst | 18 +- docs/how-does-it-work.rst | 11 +- docs/init.rst | 138 ++++++------ 8 files changed, 362 insertions(+), 348 deletions(-) diff --git a/README.rst b/README.rst index 4eb77100c..263033a59 100644 --- a/README.rst +++ b/README.rst @@ -36,12 +36,12 @@ For that, it gives you a class decorator and a way to declaratively define the a .. code-block:: pycon >>> from typing import List - >>> import attr + >>> from attr import asdict, define, make_class, Factory - >>> @attr.define + >>> @define ... class SomeClass: ... a_number: int = 42 - ... list_of_numbers: List[int] = attr.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 @@ -58,13 +58,13 @@ For that, it gives you a class decorator and a way to declaratively define the a >>> sc != SomeClass(2, [3, 2, 1]) True - >>> attr.asdict(sc) + >>> asdict(sc) {'a_number': 1, 'list_of_numbers': [1, 2, 3]} >>> SomeClass() SomeClass(a_number=42, list_of_numbers=[]) - >>> C = attr.make_class("C", ["a", "b"]) + >>> C = make_class("C", ["a", "b"]) >>> C("foo", "bar") C(a='foo', b='bar') diff --git a/docs/comparison.rst b/docs/comparison.rst index c82f014ba..87a47d2f1 100644 --- a/docs/comparison.rst +++ b/docs/comparison.rst @@ -16,11 +16,13 @@ As with other features, you can exclude fields from being involved in comparison .. doctest:: - >>> import attr - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib(eq=False) + >>> from attr import define, field + + >>> @define + ... class C: + ... x: int + ... y: int = field(eq=False) + >>> C(1, 2) == C(1, 3) True @@ -29,15 +31,19 @@ It is then used as a key function like you may know from `sorted`: .. doctest:: - >>> import attr - >>> @attr.s - ... class S(object): - ... x = attr.ib(eq=str.lower) + >>> from attr import define, field + + >>> @define + ... class S: + ... x: str = field(eq=str.lower) + >>> S("foo") == S("FOO") True - >>> @attr.s - ... class C(object): - ... x = attr.ib(order=int) + + >>> @define(order=True) + ... class C: + ... x: str = field(order=int) + >>> C("10") > C("2") True @@ -49,9 +55,9 @@ For NumPy arrays it would look like this:: import numpy - @attr.s(order=False) + @define(order=False) class C: - an_array = attr.ib(eq=attr.cmp_using(eq=numpy.array_equal)) + an_array = field(eq=attr.cmp_using(eq=numpy.array_equal)) .. warning:: diff --git a/docs/conf.py b/docs/conf.py index 7d5838b10..42af10f84 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,10 @@ def find_version(*file_paths): # -- General configuration ------------------------------------------------ +doctest_global_setup = """ +from attr import define, frozen, field, validators, Factory +""" + linkcheck_ignore = [ r"https://github.com/.*/(issues|pull)/\d+", ] diff --git a/docs/examples.rst b/docs/examples.rst index 4e40fac32..3d2194ff1 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -9,9 +9,9 @@ The simplest possible usage is: .. doctest:: - >>> import attr - >>> @attr.s - ... class Empty(object): + >>> from attr import define + >>> @define + ... class Empty: ... pass >>> Empty() Empty() @@ -26,10 +26,10 @@ But you'll usually want some data on your classes, so let's add some: .. doctest:: - >>> @attr.s - ... class Coordinates(object): - ... x = attr.ib() - ... y = attr.ib() + >>> @define + ... class Coordinates: + ... x: int + ... y: int By default, all features are added, so you immediately have a fully functional data class with a nice ``repr`` string and comparison methods. @@ -46,27 +46,13 @@ By default, all features are added, so you immediately have a fully functional d As shown, the generated ``__init__`` method allows for both positional and keyword arguments. -If playful naming turns you off, ``attrs`` comes with serious-business aliases: - -.. doctest:: - - >>> from attr import attrs, attrib - >>> @attrs - ... class SeriousCoordinates(object): - ... x = attrib() - ... y = attrib() - >>> SeriousCoordinates(1, 2) - SeriousCoordinates(x=1, y=2) - >>> attr.fields(Coordinates) == attr.fields(SeriousCoordinates) - True - For private attributes, ``attrs`` will strip the leading underscores for keyword arguments: .. doctest:: - >>> @attr.s - ... class C(object): - ... _x = attr.ib() + >>> @define + ... class C: + ... _x: int >>> C(x=1) C(_x=1) @@ -74,9 +60,9 @@ If you want to initialize your private attributes yourself, you can do that too: .. doctest:: - >>> @attr.s - ... class C(object): - ... _x = attr.ib(init=False, default=42) + >>> @define + ... class C: + ... _x: int = field(init=False, default=42) >>> C() C(_x=42) >>> C(23) @@ -89,12 +75,12 @@ This is useful in times when you want to enhance classes that are not yours (nic .. doctest:: - >>> class SomethingFromSomeoneElse(object): + >>> class SomethingFromSomeoneElse: ... def __init__(self, x): ... self.x = x - >>> SomethingFromSomeoneElse = attr.s( + >>> SomethingFromSomeoneElse = define( ... these={ - ... "x": attr.ib() + ... "x": field() ... }, init=False)(SomethingFromSomeoneElse) >>> SomethingFromSomeoneElse(1) SomethingFromSomeoneElse(x=1) @@ -104,17 +90,17 @@ This is useful in times when you want to enhance classes that are not yours (nic .. doctest:: - >>> @attr.s - ... class A(object): - ... a = attr.ib() + >>> @define(slots=False) + ... class A: + ... a: int ... def get_a(self): ... return self.a - >>> @attr.s - ... class B(object): - ... b = attr.ib() - >>> @attr.s - ... class C(A, B): - ... c = attr.ib() + >>> @define(slots=False) + ... class B: + ... b: int + >>> @define(slots=False) + ... class C(B, A): + ... c: int >>> i = C(1, 2, 3) >>> i C(a=1, b=2, c=3) @@ -123,25 +109,9 @@ This is useful in times when you want to enhance classes that are not yours (nic >>> i.get_a() 1 -The order of the attributes is defined by the `MRO `_. - -In Python 3, classes defined within other classes are `detected `_ and reflected in the ``__repr__``. -In Python 2 though, it's impossible. -Therefore ``@attr.s`` comes with the ``repr_ns`` option to set it manually: - -.. doctest:: - - >>> @attr.s - ... class C(object): - ... @attr.s(repr_ns="C") - ... class D(object): - ... pass - >>> C.D() - C.D() - -``repr_ns`` works on both Python 2 and 3. -On Python 3 it overrides the implicit detection. +:term:`Slotted classes `, which are the default for the new APIs, don't play well with multiple inheritance so we don't use them in the example. +The order of the attributes is defined by the `MRO `_. Keyword-only Attributes ~~~~~~~~~~~~~~~~~~~~~~~ @@ -150,9 +120,9 @@ You can also add `keyword-only >> @attr.s + >>> @define ... class A: - ... a = attr.ib(kw_only=True) + ... a: int = field(kw_only=True) >>> A() Traceback (most recent call last): ... @@ -160,14 +130,14 @@ You can also add `keyword-only >> A(a=1) A(a=1) -``kw_only`` may also be specified at via ``attr.s``, and will apply to all attributes: +``kw_only`` may also be specified at via ``define``, and will apply to all attributes: .. doctest:: - >>> @attr.s(kw_only=True) + >>> @define(kw_only=True) ... class A: - ... a = attr.ib() - ... b = attr.ib() + ... a: int + ... b: int >>> A(1, 2) Traceback (most recent call last): ... @@ -183,12 +153,12 @@ Keyword-only attributes allow subclasses to add attributes without default value .. doctest:: - >>> @attr.s + >>> @define ... class A: - ... a = attr.ib(default=0) - >>> @attr.s + ... a: int = 0 + >>> @define ... class B(A): - ... b = attr.ib(kw_only=True) + ... b: int = field(kw_only=True) >>> B(b=1) B(a=0, b=1) >>> B() @@ -200,15 +170,15 @@ If you don't set ``kw_only=True``, then there's is no valid attribute ordering a .. doctest:: - >>> @attr.s + >>> @define ... class A: - ... a = attr.ib(default=0) - >>> @attr.s + ... a: int = 0 + >>> @define ... class B(A): - ... b = attr.ib() + ... b: int Traceback (most recent call last): ... - ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=None, kw_only=False) + ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=int, kw_only=False) .. _asdict: @@ -219,7 +189,9 @@ When you have a class with data, it often is very convenient to transform that c .. doctest:: - >>> attr.asdict(Coordinates(x=1, y=2)) + >>> from attr import asdict + + >>> asdict(Coordinates(x=1, y=2)) {'x': 1, 'y': 2} Some fields cannot or should not be transformed. @@ -227,38 +199,48 @@ For that, `attr.asdict` offers a callback that decides whether an attribute shou .. doctest:: - >>> @attr.s - ... class UserList(object): - ... users = attr.ib() - >>> @attr.s + >>> from typing import List + >>> from attr import asdict + + >>> @define ... class User(object): - ... email = attr.ib() - ... password = attr.ib() - >>> attr.asdict(UserList([User("jane@doe.invalid", "s33kred"), - ... User("joe@doe.invalid", "p4ssw0rd")]), - ... filter=lambda attr, value: attr.name != "password") + ... email: str + ... password: str + + >>> @define + ... class UserList: + ... users: List[User] + + >>> asdict(UserList([User("jane@doe.invalid", "s33kred"), + ... User("joe@doe.invalid", "p4ssw0rd")]), + ... filter=lambda attr, value: attr.name != "password") {'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]} For the common case where you want to `include ` or `exclude ` certain types or attributes, ``attrs`` ships with a few helpers: .. doctest:: - >>> @attr.s - ... class User(object): - ... login = attr.ib() - ... password = attr.ib() - ... id = attr.ib() - >>> attr.asdict( + >>> from attr import asdict, filters, fields + + >>> @define + ... class User: + ... login: str + ... password: str + ... id: int + + >>> asdict( ... User("jane", "s33kred", 42), - ... filter=attr.filters.exclude(attr.fields(User).password, int)) + ... filter=filters.exclude(fields(User).password, int)) {'login': 'jane'} - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - ... z = attr.ib() - >>> attr.asdict(C("foo", "2", 3), - ... filter=attr.filters.include(int, attr.fields(C).x)) + + >>> @define + ... class C: + ... x: str + ... y: str + ... z: int + + >>> asdict(C("foo", "2", 3), + ... filter=filters.include(int, fields(C).x)) {'x': 'foo', 'z': 3} Other times, all you want is a tuple and ``attrs`` won't let you down: @@ -266,45 +248,50 @@ Other times, all you want is a tuple and ``attrs`` won't let you down: .. doctest:: >>> import sqlite3 - >>> import attr - >>> @attr.s + >>> from attr import astuple + + >>> @define ... class Foo: - ... a = attr.ib() - ... b = attr.ib() + ... a: int + ... b: int + >>> foo = Foo(2, 3) >>> with sqlite3.connect(":memory:") as conn: ... c = conn.cursor() ... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS - ... c.execute("INSERT INTO foo VALUES (?, ?)", attr.astuple(foo)) #doctest: +ELLIPSIS + ... c.execute("INSERT INTO foo VALUES (?, ?)", astuple(foo)) #doctest: +ELLIPSIS ... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone()) >>> foo == foo2 True +For more advanced transformations and conversions, we recommend you look at a companion library (such as `cattrs `_). Defaults -------- Sometimes you want to have default values for your initializer. -And sometimes you even want mutable objects as default values (ever used accidentally ``def f(arg=[])``?). +And sometimes you even want mutable objects as default values (ever accidentally used ``def f(arg=[])``?). ``attrs`` has you covered in both cases: .. doctest:: >>> import collections - >>> @attr.s - ... class Connection(object): - ... socket = attr.ib() + + >>> @define + ... class Connection: + ... socket: int ... @classmethod ... def connect(cls, db_string): ... # ... connect somehow to db_string ... ... return cls(socket=42) - >>> @attr.s - ... class ConnectionPool(object): - ... db_string = attr.ib() - ... pool = attr.ib(default=attr.Factory(collections.deque)) - ... debug = attr.ib(default=False) + + >>> @define + ... class ConnectionPool: + ... db_string: str + ... pool: collections.deque = Factory(collections.deque) + ... debug: bool = False ... def get_connection(self): ... try: ... return self.pool.pop() @@ -329,31 +316,22 @@ And sometimes you even want mutable objects as default values (ever used acciden More information on why class methods for constructing objects are awesome can be found in this insightful `blog post `_. -Default factories can also be set using a decorator. +Default factories can also be set using the ``factory`` argument to ``field``, and using a decorator. The method receives the partially initialized instance which enables you to base a default value on other attributes: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(default=1) - ... y = attr.ib() + >>> @define + ... class C: + ... x: int = 1 + ... y: int = field() ... @y.default ... def _any_name_except_a_name_of_an_attribute(self): ... return self.x + 1 + ... z: list = field(factory=list) >>> C() - C(x=1, y=2) - + C(x=1, y=2, z=[]) -And since the case of ``attr.ib(default=attr.Factory(f))`` is so common, ``attrs`` also comes with syntactic sugar for it: - -.. doctest:: - - >>> @attr.s - ... class C(object): - ... x = attr.ib(factory=list) - >>> C() - C(x=[]) .. _examples_validators: @@ -368,9 +346,9 @@ You can use a decorator: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() + >>> @define + ... class C: + ... x: int = field() ... @x.validator ... def check(self, attribute, value): ... if value > 42: @@ -386,14 +364,16 @@ You can use a decorator: .. doctest:: + >>> from attr import validators + >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: ... raise ValueError("'x' has to be smaller than 'y'!") - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=[attr.validators.instance_of(int), - ... x_smaller_than_y]) - ... y = attr.ib() + >>> @define + ... class C: + ... x: int = field(validator=[validators.instance_of(int), + ... x_smaller_than_y]) + ... y: int >>> C(x=3, y=4) C(x=3, y=4) >>> C(x=4, y=3) @@ -405,9 +385,9 @@ You can use a decorator: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @define + ... class C: + ... x: int = field(validator=validators.instance_of(int)) ... @x.validator ... def fits_byte(self, attribute, value): ... if not 0 <= value < 256: @@ -417,22 +397,22 @@ You can use a decorator: >>> C("128") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), , '128') + TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=int, converter=None, kw_only=False), , '128') >>> C(256) Traceback (most recent call last): ... ValueError: value out of bounds -Please note that the decorator approach only works if -- and only if! -- the attribute in question has an ``attr.ib`` assigned. -Therefore if you use ``@attr.s(auto_attribs=True)``, it is *not* enough to decorate said attribute with a type. +Please note that the decorator approach only works if -- and only if! -- the attribute in question has a ``field`` assigned. +Therefore if you use ``@default``, it is *not* enough to annotate said attribute with a type. ``attrs`` ships with a bunch of validators, make sure to `check them out ` before writing your own: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @define + ... class C: + ... x: int = field(validator=validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -440,7 +420,7 @@ Therefore if you use ``@attr.s(auto_attribs=True)``, it is *not* enough to decor ... 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 `attr.define`) to define your class, validators only run on initialization by default. +Please note that if you use `attr.s` (and not `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. @@ -454,9 +434,9 @@ This can be useful for doing type-conversions on values that you don't want to f .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(converter=int) + >>> @define + ... class C: + ... x: int = field(converter=int) >>> o = C("1") >>> o.x 1 @@ -475,12 +455,14 @@ All ``attrs`` attributes may include arbitrary metadata in the form of a read-on .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(metadata={'my_metadata': 1}) - >>> attr.fields(C).x.metadata + >>> from attr import fields + + >>> @define + ... class C: + ... x = field(metadata={'my_metadata': 1}) + >>> fields(C).x.metadata mappingproxy({'my_metadata': 1}) - >>> attr.fields(C).x.metadata['my_metadata'] + >>> fields(C).x.metadata['my_metadata'] 1 Metadata is not used by ``attrs``, and is meant to enable rich functionality in third-party libraries. @@ -497,36 +479,41 @@ Types .. doctest:: - >>> @attr.s + >>> from attr import attrib, fields + + >>> @define ... class C: - ... x = attr.ib(type=int) - ... y: int = attr.ib() - >>> attr.fields(C).x.type + ... x: int + >>> fields(C).x.type - >>> attr.fields(C).y.type + + >>> @define + ... class C: + ... x = attrib(type=int) + >>> fields(C).x.type -If you don't mind annotating *all* attributes, you can even drop the `attr.ib` and assign default values instead: +If you don't mind annotating *all* attributes, you can even drop the `field` and assign default values instead: .. doctest:: >>> import typing - >>> @attr.s(auto_attribs=True) + >>> from attr import fields + + >>> @define ... class AutoC: ... cls_var: typing.ClassVar[int] = 5 # this one is ignored - ... l: typing.List[int] = attr.Factory(list) + ... l: typing.List[int] = Factory(list) ... x: int = 1 - ... foo: str = attr.ib( - ... default="every attrib needs a type if auto_attribs=True" - ... ) + ... foo: str = "every attrib needs a type if auto_attribs=True" ... bar: typing.Any = None - >>> attr.fields(AutoC).l.type + >>> fields(AutoC).l.type typing.List[int] - >>> attr.fields(AutoC).x.type + >>> fields(AutoC).x.type - >>> attr.fields(AutoC).foo.type + >>> fields(AutoC).foo.type - >>> attr.fields(AutoC).bar.type + >>> fields(AutoC).bar.type typing.Any >>> AutoC() AutoC(l=[], x=1, foo='every attrib needs a type if auto_attribs=True', bar=None) @@ -542,24 +529,26 @@ This will replace the *type* attribute in the respective fields. .. doctest:: >>> import typing - >>> @attr.s(auto_attribs=True) + >>> from attr import fields, resolve_types + + >>> @define ... class A: ... a: typing.List['A'] ... b: 'B' ... - >>> @attr.s(auto_attribs=True) + >>> @define ... class B: ... a: A ... - >>> attr.fields(A).a.type + >>> fields(A).a.type typing.List[ForwardRef('A')] - >>> attr.fields(A).b.type + >>> fields(A).b.type 'B' - >>> attr.resolve_types(A, globals(), locals()) + >>> resolve_types(A, globals(), locals()) - >>> attr.fields(A).a.type + >>> fields(A).a.type typing.List[A] - >>> attr.fields(A).b.type + >>> fields(A).b.type .. warning:: @@ -572,14 +561,16 @@ Slots ----- :term:`Slotted classes ` have several advantages on CPython. -Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of passing ``slots=True``: +Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `define` or passing ``slots=True`` to `attr.s`: .. doctest:: + >>> import attr + >>> @attr.s(slots=True) - ... class Coordinates(object): - ... x = attr.ib() - ... y = attr.ib() + ... class Coordinates: + ... x: int + ... y: int Immutability @@ -591,9 +582,9 @@ If you'd like to enforce it, ``attrs`` will try to help: .. doctest:: - >>> @attr.s(frozen=True) - ... class C(object): - ... x = attr.ib() + >>> @frozen + ... class C: + ... x: int >>> i = C(1) >>> i.x = 2 Traceback (most recent call last): @@ -610,14 +601,16 @@ In Clojure that function is called `assoc >> @attr.s(frozen=True) - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() + >>> from attr import evolve + + >>> @frozen + ... class C: + ... x: int + ... y: int >>> i1 = C(1, 2) >>> i1 C(x=1, y=2) - >>> i2 = attr.evolve(i1, y=3) + >>> i2 = evolve(i1, y=3) >>> i2 C(x=1, y=3) >>> i1 == i2 @@ -632,21 +625,24 @@ Sometimes you may want to create a class programmatically. .. doctest:: - >>> @attr.s - ... class C1(object): - ... x = attr.ib() - ... y = attr.ib() - >>> C2 = attr.make_class("C2", ["x", "y"]) - >>> attr.fields(C1) == attr.fields(C2) + >>> from attr import fields, make_class + >>> @define + ... class C1: + ... x = field() + ... y = field() + >>> C2 = make_class("C2", ["x", "y"]) + >>> fields(C1) == fields(C2) True -You can still have power over the attributes if you pass a dictionary of name: ``attr.ib`` mappings and can pass arguments to ``@attr.s``: +You can still have power over the attributes if you pass a dictionary of name: ``field`` mappings and can pass arguments to ``@attr.s``: .. doctest:: - >>> C = attr.make_class("C", {"x": attr.ib(default=42), - ... "y": attr.ib(default=attr.Factory(list))}, - ... repr=False) + >>> from attr import make_class + + >>> C = make_class("C", {"x": field(default=42), + ... "y": field(default=Factory(list))}, + ... repr=False) >>> i = C() >>> i # no repr added! <__main__.C object at ...> @@ -659,26 +655,28 @@ If you need to dynamically make a class with `attr.make_class` and it needs to b .. doctest:: - >>> class D(object): - ... def __eq__(self, other): - ... return True # arbitrary example - >>> C = attr.make_class("C", {}, bases=(D,), cmp=False) - >>> isinstance(C(), D) - True + >>> from attr import make_class + + >>> class D: + ... def __eq__(self, other): + ... return True # arbitrary example + >>> C = make_class("C", {}, bases=(D,), cmp=False) + >>> isinstance(C(), D) + True Sometimes, you want to have your class's ``__init__`` method do more than just the initialization, validation, etc. that gets done for you automatically when -using ``@attr.s``. +using ``@define``. To do this, just define a ``__attrs_post_init__`` method in your class. It will get called at the end of the generated ``__init__`` method. .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - ... z = attr.ib(init=False) + >>> @define + ... class C: + ... x: int + ... y: int + ... z: int = field(init=False) ... ... def __attrs_post_init__(self): ... self.z = self.x + self.y @@ -690,10 +688,10 @@ You can exclude single attributes from certain methods: .. doctest:: - >>> @attr.s - ... class C(object): - ... user = attr.ib() - ... password = attr.ib(repr=False) + >>> @define + ... class C: + ... user: str + ... password: str = field(repr=False) >>> C("me", "s3kr3t") C(user='me') @@ -701,9 +699,9 @@ Alternatively, to influence how the generated ``__repr__()`` method formats a sp .. doctest:: - >>> @attr.s - ... class C(object): - ... user = attr.ib() - ... password = attr.ib(repr=lambda value: '***') + >>> @define + ... class C: + ... user: str + ... password: str = field(repr=lambda value: '***') >>> C("me", "s3kr3t") C(user='me', password=***) diff --git a/docs/extending.rst b/docs/extending.rst index 8994940f0..d229f1595 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -8,20 +8,20 @@ So it is fairly simple to build your own decorators on top of ``attrs``: .. doctest:: - >>> import attr + >>> from attr import define >>> def print_attrs(cls): ... print(cls.__attrs_attrs__) ... return cls >>> @print_attrs - ... @attr.s - ... class C(object): - ... a = attr.ib() - (Attribute(name='a', 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),) + ... @define + ... class C: + ... a: int + (Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=, converter=None, kw_only=False, inherited=False, on_setattr=None),) .. warning:: - The `attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! + The `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 @@ -131,14 +131,14 @@ This information is available to you: .. doctest:: - >>> import attr - >>> @attr.s - ... class C(object): - ... x: int = attr.ib() - ... y = attr.ib(type=str) - >>> attr.fields(C).x.type + >>> from attr import attrib, define, field, fields + >>> @define + ... class C: + ... x: int = field() + ... y = attrib(type=str) + >>> fields(C).x.type - >>> attr.fields(C).y.type + >>> fields(C).y.type Currently, ``attrs`` doesn't do anything with this information but it's very useful if you'd like to write your own validators or serializers! @@ -160,36 +160,37 @@ Here are some tips for effective use of metadata: from mylib import MY_METADATA_KEY - @attr.s - class C(object): - x = attr.ib(metadata={MY_METADATA_KEY: 1}) + @define + class C: + x = field(metadata={MY_METADATA_KEY: 1}) Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways. -- Expose ``attr.ib`` wrappers for your specific metadata. +- Expose ``field`` wrappers for your specific metadata. This is a more graceful approach if your users don't require metadata from other libraries. .. doctest:: + >>> from attr import fields, NOTHING >>> MY_TYPE_METADATA = '__my_type_metadata' >>> >>> def typed( - ... cls, default=attr.NOTHING, validator=None, repr=True, + ... cls, default=NOTHING, validator=None, repr=True, ... eq=True, order=None, hash=None, init=True, metadata={}, - ... type=None, converter=None + ... converter=None ... ): ... metadata = dict() if not metadata else metadata ... metadata[MY_TYPE_METADATA] = cls - ... return attr.ib( + ... return field( ... default=default, validator=validator, repr=repr, ... eq=eq, order=order, hash=hash, init=init, - ... metadata=metadata, type=type, converter=converter + ... metadata=metadata, converter=converter ... ) >>> - >>> @attr.s - ... class C(object): - ... x = typed(int, default=1, init=False) - >>> attr.fields(C).x.metadata[MY_TYPE_METADATA] + >>> @define + ... class C: + ... x: int = typed(int, default=1, init=False) + >>> fields(C).x.metadata[MY_TYPE_METADATA] @@ -204,7 +205,7 @@ 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[attr.Attribute]) -> list[attr.Attribute] :noindex: - *cls* is your class right *before* it is being converted into an attrs class. @@ -221,7 +222,7 @@ For example, let's assume that you really don't like floats: >>> def drop_floats(cls, fields): ... return [f for f in fields if f.type not in {float, 'float'}] ... - >>> @attr.frozen(field_transformer=drop_floats) + >>> @frozen(field_transformer=drop_floats) ... class Data: ... a: int ... b: float @@ -249,7 +250,7 @@ A more realistic example would be to automatically convert data that you, e.g., ... results.append(field.evolve(converter=converter)) ... return results ... - >>> @attr.frozen(field_transformer=auto_convert) + >>> @frozen(field_transformer=auto_convert) ... class Data: ... a: int ... b: str @@ -270,12 +271,13 @@ However, the result can not always be serialized since most data types will rema >>> import json >>> import datetime + >>> from attr import asdict >>> - >>> @attr.frozen + >>> @frozen ... class Data: ... dt: datetime.datetime ... - >>> data = attr.asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) + >>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) >>> data {'dt': datetime.datetime(2020, 5, 4, 13, 37)} >>> json.dumps(data) @@ -291,12 +293,13 @@ It has the signature .. doctest:: + >>> from attr import asdict >>> def serialize(inst, field, value): ... if isinstance(value, datetime.datetime): ... return value.isoformat() ... return value ... - >>> data = attr.asdict( + >>> data = asdict( ... Data(datetime.datetime(2020, 5, 4, 13, 37)), ... value_serializer=serialize, ... ) diff --git a/docs/glossary.rst b/docs/glossary.rst index 8bd53556b..8dab480f5 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -22,11 +22,11 @@ Glossary .. doctest:: - >>> import attr - >>> @attr.s(slots=True) - ... class Coordinates(object): - ... x = attr.ib() - ... y = attr.ib() + >>> from attr import define + >>> @define + ... class Coordinates: + ... x: int + ... y: int ... >>> c = Coordinates(x=1, y=2) >>> c.z = 3 @@ -47,9 +47,9 @@ Glossary .. doctest:: >>> import attr, unittest.mock - >>> @attr.s(slots=True) - ... class Slotted(object): - ... x = attr.ib() + >>> @define + ... class Slotted: + ... x: int ... ... def method(self): ... return self.x @@ -61,7 +61,7 @@ Glossary Traceback (most recent call last): ... AttributeError: 'Slotted' object attribute 'method' is read-only - >>> @attr.s # implies 'slots=False' + >>> @define(slots=False) ... class Dicted(Slotted): ... pass >>> d = Dicted(42) diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index 8519c8119..e9dfcd62c 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -24,10 +24,11 @@ To be very clear: if you define a class with a single attribute without a defaul .. doctest:: - >>> import attr, inspect - >>> @attr.s - ... class C(object): - ... x = attr.ib() + >>> import inspect + >>> from attr import define + >>> @define + ... class C: + ... x: int >>> print(inspect.getsource(C.__init__)) def __init__(self, x): self.x = x @@ -102,6 +103,6 @@ Pick what's more important to you. Summary +++++++ -You should avoid instantiating lots of frozen slotted classes (i.e. ``@attr.s(slots=True, frozen=True)``) in performance-critical code. +You should avoid instantiating lots of frozen slotted classes (i.e. ``@frozen``) in performance-critical code. Frozen dict classes have barely a performance impact, unfrozen slotted classes are even *faster* than unfrozen dict classes (i.e. regular classes). diff --git a/docs/init.rst b/docs/init.rst index bda39f119..d4da16982 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -17,10 +17,10 @@ So assuming you use an ORM and want to extract 2D points from a row object, do n Instead, write a `classmethod` that will extract it for you:: - @attr.s - class Point(object): - x = attr.ib() - y = attr.ib() + @define + class Point: + x: float + y: float @classmethod def from_row(cls, row): @@ -52,20 +52,21 @@ One thing people tend to find confusing is the treatment of private attributes t .. doctest:: >>> import inspect, attr - >>> @attr.s - ... class C(object): - ... _x = attr.ib() + >>> from attr import define + >>> @define + ... class C: + ... _x: int >>> inspect.signature(C.__init__) - None> + None> There really isn't a right or wrong, it's a matter of taste. But it's important to be aware of it because it can lead to surprising syntax errors: .. doctest:: - >>> @attr.s - ... class C(object): - ... _1 = attr.ib() + >>> @define + ... class C: + ... _1: int Traceback (most recent call last): ... SyntaxError: invalid syntax @@ -83,13 +84,14 @@ This is when default values come into play: .. doctest:: - >>> import attr - >>> @attr.s - ... class C(object): - ... a = attr.ib(default=42) - ... b = attr.ib(default=attr.Factory(list)) - ... c = attr.ib(factory=list) # syntactic sugar for above - ... d = attr.ib() + >>> from attr import define, field, Factory + + >>> @define + ... class C: + ... a: int = 42 + ... b: list = field(factory=list) + ... c: list = Factory(list) # syntactic sugar for above + ... d: dict = field() ... @d.default ... def _any_name_except_a_name_of_an_attribute(self): ... return {} @@ -97,15 +99,14 @@ This is when default values come into play: C(a=42, b=[], c=[], d={}) It's important that the decorated method -- or any other method or property! -- doesn't have the same name as the attribute, otherwise it would overwrite the attribute definition. -You also cannot use type annotations to elide the `attr.ib` call for ``d`` as explained in `types`. Please note that as with function and method signatures, ``default=[]`` will *not* do what you may think it might do: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(default=[]) + >>> @define + ... class C: + ... x = [] >>> i = C() >>> k = C() >>> i.x.append(42) @@ -147,9 +148,9 @@ The method has to accept three arguments: If the value does not pass the validator's standards, it just raises an appropriate exception. - >>> @attr.s - ... class C(object): - ... x = attr.ib() + >>> @define + ... class C: + ... x: int = field() ... @x.validator ... def _check_x(self, attribute, value): ... if value > 42: @@ -161,28 +162,28 @@ 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 you can't elide the call to `attr.ib`. +Again, it's important that the decorated method doesn't have the same name as the attribute and that the `field()` helper is used. Callables ~~~~~~~~~ -If you want to re-use your validators, you should have a look at the ``validator`` argument to `attr.ib`. +If you want to re-use your validators, you should have a look at the ``validator`` argument to `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. -Since the validators runs *after* the instance is initialized, you can refer to other attributes while validating: +Since the validators run *after* the instance is initialized, you can refer to other attributes while validating: .. doctest:: >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: ... raise ValueError("'x' has to be smaller than 'y'!") - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=[attr.validators.instance_of(int), - ... x_smaller_than_y]) - ... y = attr.ib() + >>> @define + ... class C: + ... x = field(validator=[attr.validators.instance_of(int), + ... x_smaller_than_y]) + ... y = field() >>> C(x=3, y=4) C(x=3, y=4) >>> C(x=4, y=3) @@ -193,12 +194,12 @@ Since the validators runs *after* the instance is initialized, you can refer to 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. ``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. .. doctest:: >>> i = C(4, 5) - >>> i.x = 5 # works, no magic here - >>> attr.validate(i) + >>> i.x = 5 Traceback (most recent call last): ... ValueError: 'x' has to be smaller than 'y'! @@ -207,9 +208,9 @@ This example also shows of some syntactic sugar for using the `attr.validators.a .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @define + ... class C: + ... x = field(validator=attr.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -222,9 +223,9 @@ If you define validators both ways for an attribute, they are both ran: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @define + ... class C: + ... x = field(validator=attr.validators.instance_of(int)) ... @x.validator ... def fits_byte(self, attribute, value): ... if not 0 <= value < 256: @@ -275,9 +276,9 @@ This can be useful for doing type-conversions on values that you don't want to f .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(converter=int) + >>> @define + ... class C: + ... x = field(converter=int) >>> o = C("1") >>> o.x 1 @@ -289,9 +290,9 @@ Converters are run *before* validators, so you can use validators to check the f >>> def validate_x(instance, attribute, value): ... if value < 0: ... raise ValueError("x must be at least 0.") - >>> @attr.s - ... class C(object): - ... x = attr.ib(converter=int, validator=validate_x) + >>> @define + ... class C: + ... x = field(converter=int, validator=validate_x) >>> o = C("0") >>> o.x 0 @@ -318,9 +319,9 @@ A converter will override an explicit type annotation or ``type`` argument. >>> def str2int(x: str) -> int: ... return int(x) - >>> @attr.s - ... class C(object): - ... x = attr.ib(converter=str2int) + >>> @define + ... class C: + ... x = field(converter=str2int) >>> C.__init__.__annotations__ {'return': None, 'x': } @@ -348,9 +349,9 @@ The sole reason for the existance of ``__attrs_pre_init__`` is to give users the .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() + >>> @define + ... class C: + ... x: int ... def __attrs_pre_init__(self): ... super().__init__() >>> C(42) @@ -369,9 +370,10 @@ Here's an example of a manual default value: .. doctest:: >>> from typing import Optional - >>> @attr.s(auto_detect=True) # or init=False - ... class C(object): - ... x = attr.ib() + + >>> @define + ... class C: + ... x: int ... ... def __init__(self, x: int = 42): ... self.__attrs_init__(x) @@ -384,10 +386,10 @@ Post Init .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib(init=False) + >>> @define + ... class C: + ... x: int + ... y: int = field(init=False) ... def __attrs_post_init__(self): ... self.y = self.x + 1 >>> C(1) @@ -397,10 +399,10 @@ Please note that you can't directly set attributes on frozen classes: .. doctest:: - >>> @attr.s(frozen=True) - ... class FrozenBroken(object): - ... x = attr.ib() - ... y = attr.ib(init=False) + >>> @frozen + ... class FrozenBroken: + ... x: int + ... y: int = field(init=False) ... def __attrs_post_init__(self): ... self.y = self.x + 1 >>> FrozenBroken(1) @@ -412,10 +414,10 @@ If you need to set attributes on a frozen class, you'll have to resort to the `s .. doctest:: - >>> @attr.s(frozen=True) - ... class Frozen(object): - ... x = attr.ib() - ... y = attr.ib(init=False) + >>> @define + ... class Frozen: + ... x: int + ... y: int = field(init=False) ... def __attrs_post_init__(self): ... object.__setattr__(self, "y", self.x + 1) >>> Frozen(1) From f6fd889148897e66f9a91d5ea7d6e96f57a07345 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 20:30:07 +0100 Subject: [PATCH 058/139] [pre-commit.ci] pre-commit autoupdate (#870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://gitlab.com/PyCQA/flake8 → https://github.com/PyCQA/flake8 - [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7282844fe..1e51b4979 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,8 +15,8 @@ repos: files: \.py$ language_version: python3.10 - - repo: https://gitlab.com/PyCQA/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 language_version: python3.10 From a24860bf30ed57c473413644a4653a702698cdb5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 23 Nov 2021 06:34:07 +0100 Subject: [PATCH 059/139] Run pre-commit autoupdate only monthly --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e51b4979..d61a5a106 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,7 @@ --- +ci: + autoupdate_schedule: monthly + repos: - repo: https://github.com/psf/black rev: 21.11b1 From 649a196877be1c374e4979e76a67e515a9d99ed5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 23 Nov 2021 09:49:41 +0100 Subject: [PATCH 060/139] Shuffle tox environments and Python versions a bit --- .github/workflows/main.yml | 4 ++-- .pre-commit-config.yaml | 2 +- tox.ini | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a745bb1dd..be6fc6d5d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,8 +17,6 @@ jobs: tests: name: "tox on ${{ matrix.python-version }}" runs-on: "ubuntu-latest" - env: - USING_COVERAGE: "2.7,3.7,3.8,3.10" strategy: fail-fast: false @@ -77,6 +75,7 @@ jobs: - run: python -m coverage report --fail-under=100 + package: name: "Build & verify package" runs-on: "ubuntu-latest" @@ -95,6 +94,7 @@ jobs: - name: "Check long_description" run: "python -m twine check dist/*" + install-dev: name: "Verify dev env" runs-on: "${{ matrix.os }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d61a5a106..218b70497 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: hooks: - id: black exclude: tests/test_pattern_matching.py - language_version: python3.8 + language_version: python3.10 - repo: https://github.com/PyCQA/isort rev: 5.10.1 diff --git a/tox.ini b/tox.ini index 664ec88dc..85d53773d 100644 --- a/tox.ini +++ b/tox.ini @@ -14,9 +14,9 @@ python = 3.5: py35 3.6: py36 3.7: py37 - 3.8: py38, manifest, changelog, docs + 3.8: py38, changelog, docs 3.9: py39, pyright - 3.10: py310, typing + 3.10: py310, manifest, typing pypy-2: pypy pypy-3: pypy3 @@ -83,7 +83,7 @@ commands = [testenv:manifest] -basepython = python3.8 +basepython = python3.10 deps = check-manifest skip_install = true commands = check-manifest From 6e04e869e659fe5e2690b6609abb38c009b37e3f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 23 Nov 2021 13:56:38 +0100 Subject: [PATCH 061/139] Fix links --- docs/examples.rst | 2 +- src/attr/_make.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 3d2194ff1..03d7e38d3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -314,7 +314,7 @@ And sometimes you even want mutable objects as default values (ever accidentally >>> cp ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False) -More information on why class methods for constructing objects are awesome can be found in this insightful `blog post `_. +More information on why class methods for constructing objects are awesome can be found in this insightful `blog post `_. Default factories can also be set using the ``factory`` argument to ``field``, and using a decorator. The method receives the partially initialized instance which enables you to base a default value on other attributes: diff --git a/src/attr/_make.py b/src/attr/_make.py index c2a92cef3..89ae96c94 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -868,7 +868,7 @@ def _create_slots_class(self): cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) # The following is a fix for - # https://github.com/python-attrs/attrs/issues/102. On Python 3, + # . On Python 3, # if a method mentions `__class__` or uses the no-arg super(), the # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a From 29447f81af01944b88419087caa002a175aeb564 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 24 Nov 2021 09:39:41 +0100 Subject: [PATCH 062/139] Document the various core names and why they exist (#871) * Document the various core names and why they exist Signed-off-by: Hynek Schlawack * Remove stray backtick * Add example of new API * Link PEP 526 * future is now * phrase and typos * typo/grammar * typo * Better phrasing * Add missing and * less words = better * polish * phrasing * looks weird but is correct * comma spliiiiceeeee * example * make it sound less dry * fix * phrasing --- README.rst | 6 +- docs/api.rst | 7 +-- docs/glossary.rst | 9 +++ docs/how-does-it-work.rst | 4 +- docs/index.rst | 4 +- docs/names.rst | 117 ++++++++++++++++++++++++++++++++++++++ docs/overview.rst | 33 +---------- 7 files changed, 139 insertions(+), 41 deletions(-) create mode 100644 docs/names.rst diff --git a/README.rst b/README.rst index 263033a59..3b0db4241 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ .. teaser-begin -``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder `_ methods). +``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder methods `_). `Trusted by NASA `_ for Mars missions since 2020! Its main goal is to help you to write **concise** and **correct** software without slowing down your code. @@ -86,9 +86,11 @@ 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``, and ``attr.attrib``) will remain indefinitely. +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. + .. -getting-help- Getting Help diff --git a/docs/api.rst b/docs/api.rst index 2306b1aa9..afbc87025 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,16 +3,13 @@ API Reference .. currentmodule:: attr -``attrs`` works by decorating a class using `attr.s` and then optionally defining attributes on the class using `attr.ib`. +``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. -.. note:: - - When this documentation speaks about "``attrs`` attributes" it means those attributes that are defined using `attr.ib` in the class body. +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`. - Core ---- diff --git a/docs/glossary.rst b/docs/glossary.rst index 8dab480f5..5fd01f4fb 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -3,6 +3,15 @@ Glossary .. glossary:: + dunder methods + "Dunder" is a contraction of "double underscore". + + It's methods like ``__init__`` or ``__eq__`` that are sometimes also called *magic methods* or it's said that they implement an *object protocol*. + + In spoken form, you'd call ``__init__`` just "dunder init". + + Its first documented use is a `mailing list posting `_ by Mark Jackson from 2002. + dict classes A regular class whose attributes are stored in the `object.__dict__` attribute of every single instance. This is quite wasteful especially for objects with very few data attributes and the space consumption can become significant when creating large numbers of instances. diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index e9dfcd62c..08367cbfd 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -15,9 +15,9 @@ Internally they're a representation of the data passed into ``attr.ib`` along wi 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*. -It will write dunder methods to work on *all* of those attributes which also has performance benefits due to fewer function calls. +It will write :term:`dunder methods` to work on *all* of those attributes which also has performance benefits due to fewer function calls. -Once ``attrs`` knows what attributes it has to work on, it writes the requested dunder methods and -- depending on whether you wish to have a :term:`dict ` or :term:`slotted ` class -- creates a new class for you (``slots=True``) or attaches them to the original class (``slots=False``). +Once ``attrs`` knows what attributes it has to work on, it writes the requested :term:`dunder methods` and -- depending on whether you wish to have a :term:`dict ` or :term:`slotted ` class -- creates a new class for you (``slots=True``) or attaches them to the original class (``slots=False``). While creating new classes is more elegant, we've run into several edge cases surrounding metaclasses that make it impossible to go this route unconditionally. To be very clear: if you define a class with a single attribute without a default value, the generated ``__init__`` will look *exactly* how you'd expect: diff --git a/docs/index.rst b/docs/index.rst index 6b9948cd4..29082f234 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,9 +25,10 @@ The recommended installation method is `pip `_-i The next three steps should bring you up and running in no time: - `overview` will show you a simple example of ``attrs`` in action and introduce you to its philosophy. - Afterwards, you can start writing your own classes, understand what drives ``attrs``'s design, and know what ``@attr.s`` and ``attr.ib()`` stand for. + Afterwards, you can start writing your own classes and understand what drives ``attrs``'s design. - `examples` will give you a comprehensive tour of ``attrs``'s features. After reading, you will know about our advanced features and how to use them. +- If you're confused by all the ``attr.s``, ``attr.ib``, ``attrs``, ``attrib``, ``define``, ``frozen``, and ``field``, head over to `names` for a very short explanation, and optionally a quick history lesson. - Finally `why` gives you a rundown of potential alternatives and why we think ``attrs`` is superior. Yes, we've heard about ``namedtuple``\ s and Data Classes! - If at any point you get confused by some terminology, please check out our `glossary`. @@ -77,6 +78,7 @@ Full Table of Contents api extending how-does-it-work + names glossary diff --git a/docs/names.rst b/docs/names.rst new file mode 100644 index 000000000..56d7d4458 --- /dev/null +++ b/docs/names.rst @@ -0,0 +1,117 @@ +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()`. + +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. + +And what even is ``attr.dataclass`` that's not documented but commonly used!? + + +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. + +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. + +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**. + +``attrs`` will **never** force you to use type annotations. + + +A Short History Lesson +---------------------- + +At this point, ``attrs`` is an old project. +It had its first release in April 2015 -- back when most Python code was on Python 2.7 and Python 3.4 was the first Python 3 release that showed promise. +``attrs`` was always Python 3-first, but `type annotations `_ came only into Python 3.5 that was released in September 2015 and were largely ignored until years later. + +At this time, if you didn't want to implement all the :term:`dunder methods`, the most common way to create a class with some attributes on it was to subclass a `collections.namedtuple`, or one of the many hacks that allowed you to access dictionary keys using attribute lookup. + +But ``attrs`` history goes even a bit further back, to the now-forgotten `characteristic `_ that came out in May 2014 and already used a class decorator, but was overall too unergonomic. + +In the wake of all of that, `glyph `_ and `Hynek `_ came together on IRC and brainstormed how to take the good ideas of ``characteristic``, but make them easier to use and read. +At this point the plan was not to make ``attrs`` what it is now -- a flexible class building kit. +All we wanted was an ergonomic little library to succinctly define classes with attributes. + +Under the impression of of the unwieldy ``characteristic`` name, we went to the other side and decided to make the package name part of the API, and keep the API functions very short. +This led to the infamous `attr.s` and `attr.ib` which some found confusing and pronounced it as "attr dot s" or used a singular ``@s`` as the decorator. +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. +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. + +Being able to just write:: + + @attr.s + class Point(object): + x = attr.ib() + y = attr.ib() + +was a big step for those who wanted to write small, focused classes. + + +Dataclasses Enter The Arena +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A big change happened in May 2017 when Hynek sat down with `Guido van Rossum `_ and `Eric V. Smith `_ at PyCon US 2017. + +Type annotations for class attributes have `just landed `_ 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 `_. +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. + +----- + +Due to backward-compatibility concerns, this feature is off by default in the `attr.s` decorator and has to be activated using ``@attr.s(auto_attribs=True)``, though. +As a little easter egg and to save ourselves some typing, we've also `added `_ an alias called ``attr.dataclasses`` that just set ``auto_attribs=True``. +It was never documented, but people found it and used it and loved it. + +Over the next months and years it became clear that type annotations have become the popular way to define classes and their attributes. +However, it has also become clear that some people viscerally hate type annotations. +We're determined to serve both. + + +``attrs`` TNG +^^^^^^^^^^^^^ + +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. + +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`. + +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. +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. + +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. + +We hope you like the result:: + + @define + class Point: + x: int + y: int + + +.. [#attr] We considered calling the PyPI package just ``attr`` too, but the name was already taken by an *ostensibly* inactive `package on PyPI `_. +.. [#stdlib] The highly readable PEP also explains why ``attrs`` wasn't just added to the standard library. + Don't believe the myths and rumors. diff --git a/docs/overview.rst b/docs/overview.rst index 3b2ce6f60..b35f66f2d 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -26,7 +26,7 @@ Philosophy An ``attrs`` class in runtime is indistinguishable from a regular class: because it *is* a regular class with a few boilerplate-y methods attached. **Be light on API impact.** - As convenient as it seems at first, ``attrs`` will *not* tack on any methods to your classes save the dunder ones. + As convenient as it seems at first, ``attrs`` will *not* tack on any methods to your classes except for the :term:`dunder ones `. Hence all the useful `tools ` that come with ``attrs`` live in functions that operate on top of instances. Since they take an ``attrs`` instance as their first argument, you can attach them to your classes with one line of code. @@ -50,38 +50,9 @@ What ``attrs`` Is Not All ``attrs`` does is: 1. take your declaration, -2. write dunder methods based on that information, +2. write :term:`dunder methods` based on that information, 3. and attach them to your class. It does *nothing* dynamic at runtime, hence zero runtime overhead. It's still *your* class. Do with it as you please. - - -On the ``attr.s`` and ``attr.ib`` Names -======================================= - -The ``attr.s`` decorator and the ``attr.ib`` function aren't any obscure abbreviations. -They are a *concise* and highly *readable* way to write ``attrs`` and ``attrib`` with an *explicit namespace*. - -At first, some people have a negative gut reaction to that; resembling the reactions to Python's significant whitespace. -And as with that, once one gets used to it, the readability and explicitness of that API prevails and delights. - -For those who can't swallow that API at all, ``attrs`` comes with serious business aliases: ``attr.attrs`` and ``attr.attrib``. - -Therefore, the following class definition is identical to the previous one: - -.. doctest:: - - >>> from attr import attrs, attrib, Factory - >>> @attrs - ... class SomeClass(object): - ... a_number = attrib(default=42) - ... list_of_numbers = attrib(default=Factory(list)) - ... - ... def hard_math(self, another_number): - ... return self.a_number + sum(self.list_of_numbers) * another_number - >>> SomeClass(1, [1, 2, 3]) - SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) - -Use whichever variant fits your taste better. From 6954086207b264c820552ee5d7cb223db5f2986b Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 24 Nov 2021 09:57:32 +0100 Subject: [PATCH 063/139] Switch to markdown version of CoC so GitHub finds it Signed-off-by: Hynek Schlawack --- .github/CODE_OF_CONDUCT.md | 133 ++++++++++++++++++++++++++++++++++++ .github/CODE_OF_CONDUCT.rst | 55 --------------- .github/CONTRIBUTING.rst | 2 +- docs/contributing.rst | 2 - 4 files changed, 134 insertions(+), 58 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.md delete mode 100644 .github/CODE_OF_CONDUCT.rst diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..1d8ad1833 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CODE_OF_CONDUCT.rst b/.github/CODE_OF_CONDUCT.rst deleted file mode 100644 index 56e8914ce..000000000 --- a/.github/CODE_OF_CONDUCT.rst +++ /dev/null @@ -1,55 +0,0 @@ -Contributor Covenant Code of Conduct -==================================== - -Our Pledge ----------- - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -Our Standards -------------- - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -Our Responsibilities --------------------- - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -Scope ------ - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. -Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. -Representation of a project may be further defined and clarified by project maintainers. - -Enforcement ------------ - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hs@ox.cx. -All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. -The project team is obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -Attribution ------------ - -This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.4, available at . diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index c97c5cf46..6fe7946d2 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -232,7 +232,7 @@ Thank you for considering contributing to ``attrs``! .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ .. _`PEP 257`: https://www.python.org/dev/peps/pep-0257/ .. _`good test docstrings`: https://jml.io/pages/test-docstrings.html -.. _`Code of Conduct`: https://github.com/python-attrs/attrs/blob/main/.github/CODE_OF_CONDUCT.rst +.. _`Code of Conduct`: https://github.com/python-attrs/attrs/blob/main/.github/CODE_OF_CONDUCT.md .. _changelog: https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst .. _`backward compatibility`: https://www.attrs.org/en/latest/backward-compatibility.html .. _tox: https://tox.readthedocs.io/ diff --git a/docs/contributing.rst b/docs/contributing.rst index acb527b23..8fbb03c9e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,5 +1,3 @@ .. _contributing: .. include:: ../.github/CONTRIBUTING.rst - -.. include:: ../.github/CODE_OF_CONDUCT.rst From c9d0b1522da83305dcfdfc82c28f2032162c0998 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 24 Nov 2021 12:06:11 +0100 Subject: [PATCH 064/139] Test metadata_proxy properties independently from hypothesis strategies Occasionally they fail to cover all bases and break our coverage job on Python 2.7. Signed-off-by: Hynek Schlawack --- tests/test_compat.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_compat.py diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..4318cd3f0 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,50 @@ +import pytest + +from attr._compat import metadata_proxy + + +@pytest.fixture(name="mp") +def _mp(): + return metadata_proxy({"x": 42, "y": "foo"}) + + +class TestMetadataProxy: + """ + Ensure properties of metadata_proxy independently of hypothesis strategies. + """ + + def test_repr(self, mp): + """ + repr makes sense and is consistent across Python versions. + """ + assert any( + [ + "mappingproxy({'x': 42, 'y': 'foo'})" == repr(mp), + "mappingproxy({'y': 'foo', 'x': 42})" == repr(mp), + ] + ) + + def test_immutable(self, mp): + """ + All mutating methods raise errors. + """ + with pytest.raises(TypeError, match="not support item assignment"): + mp["z"] = 23 + + with pytest.raises(TypeError, match="not support item deletion"): + del mp["x"] + + with pytest.raises(AttributeError, match="no attribute 'update'"): + mp.update({}) + + with pytest.raises(AttributeError, match="no attribute 'clear'"): + mp.clear() + + with pytest.raises(AttributeError, match="no attribute 'pop'"): + mp.pop("x") + + with pytest.raises(AttributeError, match="no attribute 'popitem'"): + mp.popitem("x") + + with pytest.raises(AttributeError, match="no attribute 'setdefault'"): + mp.setdefault("x") From 22970bdeca474022efbd7c51368d4009f67e555d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 24 Nov 2021 12:10:13 +0100 Subject: [PATCH 065/139] Fix Python 2 --- tests/test_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_compat.py b/tests/test_compat.py index 4318cd3f0..43ba374bf 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -44,7 +44,7 @@ def test_immutable(self, mp): mp.pop("x") with pytest.raises(AttributeError, match="no attribute 'popitem'"): - mp.popitem("x") + mp.popitem() with pytest.raises(AttributeError, match="no attribute 'setdefault'"): mp.setdefault("x") From ae4bd837ecaa4593a5e3c79827550192ec204c09 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 06:11:50 +0100 Subject: [PATCH 066/139] Add a summary heading to avoid checklist-only PRs --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a9c069507..35f3b62a0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,8 @@ +# Summary + +Please tell us what your pull request is about here. + + # Pull Request Check List This is just a friendly reminder about the most common mistakes. Please make sure that you tick all boxes. But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles! From 16cf4c50a0e532486974b5c02306182c1e6cd1dc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 09:31:17 +0100 Subject: [PATCH 067/139] GitHub really wants us to use Markdown Signed-off-by: Hynek Schlawack --- .github/CONTRIBUTING.md | 225 +++++++++++++++++++++++++++++++++++ .github/CONTRIBUTING.rst | 250 --------------------------------------- CHANGELOG.rst | 2 +- README.rst | 2 +- docs/contributing.rst | 3 - docs/index.rst | 1 - 6 files changed, 227 insertions(+), 256 deletions(-) create mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/CONTRIBUTING.rst delete mode 100644 docs/contributing.rst diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..f2d32fe00 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,225 @@ +# How To Contribute + +First off, thank you for considering contributing to `attrs`! +It's people like *you* who make it such a great tool for everyone. + +This document intends to make contribution more accessible by codifying tribal knowledge and expectations. +Don't be afraid to open half-finished PRs, and ask questions if something is unclear! + +Please note that this project is released with a Contributor [Code of Conduct](https://github.com/python-attrs/attrs/blob/main/.github/CODE_OF_CONDUCT.md). +By participating in this project you agree to abide by its terms. +Please report any harm to [Hynek Schlawack] in any way you find appropriate. + + +## Support + +In case you'd like to help out but don't want to deal with GitHub, there's a great opportunity: +help your fellow developers on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs)! + +The official tag is `python-attrs` and helping out in support frees us up to improve `attrs` instead! + + +## Workflow + +- No contribution is too small! + Please submit as many fixes for typos and grammar bloopers as you can! +- Try to limit each pull request to *one* change only. +- Since we squash on merge, it's up to you how you handle updates to the main branch. + Whether you prefer to rebase on main or merge main into your branch, do whatever is more comfortable for you. +- *Always* add tests and docs for your code. + 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. +- 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 [backward compatibility](https://www.attrs.org/en/latest/backward-compatibility.html). + + +## Code + +- Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/). + We use the `"""`-on-separate-lines style for docstrings: + + ```python + def func(x): + """ + Do something. + + :param str x: A very important parameter. + + :rtype: str + """ + ``` +- If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. +- We use [*isort*](https://github.com/PyCQA/isort) to sort our imports, and we use [*Black*](https://github.com/psf/black) with line length of 79 characters to format our code. + As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both -- see [*Local Development Environment*](#local-development-environment) below), you won't have to spend any time on formatting your code at all. + If you don't, [CI] will catch it for you -- but that seems like a waste of your time! + + +## Tests + +- Write your asserts as `expected == actual` to line them up nicely: + + ```python + x = f() + + assert 42 == x.some_attribute + assert "foo" == x._a_private_attribute + ``` + +- To run the test suite, all you need is a recent [*tox*]. + It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. + If you lack some Python versions, you can can always limit the environments like `tox -e py27,py38` or make it a non-failure using `tox --skip-missing-interpreters`. + + In that case you should look into [*asdf*](https://asdf-vm.com) or [*pyenv*](https://github.com/pyenv/pyenv), which make it very easy to install many different Python versions in parallel. +- Write [good test docstrings](https://jml.io/pages/test-docstrings.html). +- To ensure new features work well with the rest of the system, they should be also added to our [*Hypothesis*](https://hypothesis.readthedocs.io/) testing strategy, which can be found in `tests/strategies.py`. +- If you've changed or added public APIs, please update our type stubs (files ending in `.pyi`). + + +## Documentation + +- Use [semantic newlines] in [*reStructuredText*] files (files ending in `.rst`): + + ```rst + This is a sentence. + This is another sentence. + ``` + +- If you start a new section, add two blank lines before and one blank line after the header, except if two headers follow immediately after each other: + + ```rst + Last line of previous section. + + + Header of New Top Section + ------------------------- + + Header of New Section + ^^^^^^^^^^^^^^^^^^^^^ + + First line of new section. + ``` + +- If you add a new feature, demonstrate its awesomeness on the [examples page](https://github.com/python-attrs/attrs/blob/main/docs/examples.rst)! + + +### Changelog + +If your change is noteworthy, there needs to be a changelog entry so our users can learn about it! + +To avoid merge conflicts, we use the [*towncrier*](https://pypi.org/project/towncrier) package to manage our changelog. +*towncrier* uses independent files for each pull request -- so called *news fragments* -- instead of one monolithic changelog file. +On release, those news fragments are compiled into our [`CHANGELOG.rst`](https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst). + +You don't need to install *towncrier* yourself, you just have to abide by a few simple rules: + +- For each pull request, add a new file into `changelog.d` with a filename adhering to the `pr#.(change|deprecation|breaking).rst` schema: + For example, `changelog.d/42.change.rst` for a non-breaking change that is proposed in pull request #42. +- As with other docs, please use [semantic newlines] within news fragments. +- Wrap symbols like modules, functions, or classes into double backticks so they are rendered in a `monospace font`. +- Wrap arguments into asterisks like in docstrings: *these* or *attributes*. +- If you mention functions or other callables, add parentheses at the end of their names: `attr.func()` or `attr.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. +- 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()``. + The feature really *is* awesome. + ``` + +or: + + ```rst + ``attr.func()`` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. + The bug really *was* nasty. + ``` + +---- + +``tox -e changelog`` will render the current changelog to the terminal if you have any doubts. + + +## Local Development Environment + +You can (and should) run our test suite using [*tox*]. +However, you’ll probably want a more traditional environment as well. +We highly recommend to develop using the latest Python release because `attrs` tries to take advantage of modern features whenever possible. + +First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. +It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://github.com/direnv/direnv/wiki/Python), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). + +Next, get an up to date checkout of the ``attrs`` repository: + +```console +$ git clone git@github.com:python-attrs/attrs.git +``` + +or if you want to use git via ``https``: + +```console +$ git clone https://github.com/python-attrs/attrs.git +``` + +Change into the newly created directory and **after activating your virtual environment** install an editable version of `attrs` along with its tests and docs requirements: + +```console +$ cd attrs +$ pip install --upgrade pip setuptools # PLEASE don't skip this step +$ pip install -e '.[dev]' +``` + +At this point, + +```console +$ python -m pytest +``` + +should work and pass, as should: + +```console +$ cd docs +$ make html +``` + +The built documentation can then be found in `docs/_build/html/`. + +To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*][^dev] hooks: + +```console +$ pre-commit install +``` + +You can also run them anytime (as our tox does) using: + +```console +$ pre-commit run --all-files +``` + +[^dev]: *pre-commit* should have been installed into your virtualenv automatically when you ran `pip install -e '.[dev]'` above. If pre-commit is missing, it may be that you need to re-run `pip install -e '.[dev]'`. + + +Governance +---------- + +`attrs` is maintained by [team of volunteers](https://github.com/python-attrs) that is always open to new members that share our vision of a fast, lean, and magic-free library that empowers programmers to write better code with less effort. +If you'd like to join, just get a pull request merged and ask to be added in the very same pull request! + +**The simple rule is that everyone is welcome to review/merge pull requests of others but nobody is allowed to merge their own code.** + +[Hynek Schlawack] acts reluctantly as the [BDFL](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life) and has the final say over design decisions. + + +[CI]: https://github.com/python-attrs/attrs/actions?query=workflow%3ACI +[Hynek Schlawack]: https://hynek.me/about/ +[*pre-commit*]: https://pre-commit.com/ +[*tox*]: https://https://tox.wiki/ +[semantic newlines]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ +[*reStructuredText*]: https://www.sphinx-doc.org/en/stable/usage/restructuredtext/basics.html diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst deleted file mode 100644 index 6fe7946d2..000000000 --- a/.github/CONTRIBUTING.rst +++ /dev/null @@ -1,250 +0,0 @@ -How To Contribute -================= - -First off, thank you for considering contributing to ``attrs``! -It's people like *you* who make it such a great tool for everyone. - -This document intends to make contribution more accessible by codifying tribal knowledge and expectations. -Don't be afraid to open half-finished PRs, and ask questions if something is unclear! - - -Support -------- - -In case you'd like to help out but don't want to deal with GitHub, there's a great opportunity: -help your fellow developers on `Stack Overflow `_! - -The official tag is ``python-attrs`` and helping out in support frees us up to improve ``attrs`` instead! - - -Workflow --------- - -- No contribution is too small! - Please submit as many fixes for typos and grammar bloopers as you can! -- Try to limit each pull request to *one* change only. -- Since we squash on merge, it's up to you how you handle updates to the main branch. - Whether you prefer to rebase on main or merge main into your branch, do whatever is more comfortable for you. -- *Always* add tests and docs for your code. - 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. -- 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 `backward compatibility`_. - - -Code ----- - -- Obey `PEP 8`_ and `PEP 257`_. - We use the ``"""``\ -on-separate-lines style for docstrings: - - .. code-block:: python - - def func(x): - """ - Do something. - - :param str x: A very important parameter. - - :rtype: str - """ -- If you add or change public APIs, tag the docstring using ``.. versionadded:: 16.0.0 WHAT`` or ``.. versionchanged:: 16.2.0 WHAT``. -- We use isort_ to sort our imports, and we follow the Black_ code style with a line length of 79 characters. - As long as you run our full tox suite before committing, or install our pre-commit_ hooks (ideally you'll do both -- see below "Local Development Environment"), you won't have to spend any time on formatting your code at all. - If you don't, CI will catch it for you -- but that seems like a waste of your time! - - -Tests ------ - -- Write your asserts as ``expected == actual`` to line them up nicely: - - .. code-block:: python - - x = f() - - assert 42 == x.some_attribute - assert "foo" == x._a_private_attribute - -- To run the test suite, all you need is a recent tox_. - It will ensure the test suite runs with all dependencies against all Python versions just as it will in our CI. - If you lack some Python versions, you can can always limit the environments like ``tox -e py27,py35`` (in that case you may want to look into pyenv_, which makes it very easy to install many different Python versions in parallel). -- Write `good test docstrings`_. -- To ensure new features work well with the rest of the system, they should be also added to our `Hypothesis`_ testing strategy, which is found in ``tests/strategies.py``. -- If you've changed or added public APIs, please update our type stubs (files ending in ``.pyi``). - - -Documentation -------------- - -- Use `semantic newlines`_ in reStructuredText_ files (files ending in ``.rst``): - - .. code-block:: rst - - This is a sentence. - This is another sentence. - -- If you start a new section, add two blank lines before and one blank line after the header, except if two headers follow immediately after each other: - - .. code-block:: rst - - Last line of previous section. - - - Header of New Top Section - ------------------------- - - Header of New Section - ^^^^^^^^^^^^^^^^^^^^^ - - First line of new section. - -- If you add a new feature, demonstrate its awesomeness on the `examples page`_! - - -Changelog -^^^^^^^^^ - -If your change is noteworthy, there needs to be a changelog entry so our users can learn about it! - -To avoid merge conflicts, we use the towncrier_ package to manage our changelog. -``towncrier`` uses independent files for each pull request -- so called *news fragments* -- instead of one monolithic changelog file. -On release, those news fragments are compiled into our ``CHANGELOG.rst``. - -You don't need to install ``towncrier`` yourself, you just have to abide by a few simple rules: - -- For each pull request, add a new file into ``changelog.d`` with a filename adhering to the ``pr#.(change|deprecation|breaking).rst`` schema: - For example, ``changelog.d/42.change.rst`` for a non-breaking change that is proposed in pull request #42. -- As with other docs, please use `semantic newlines`_ within news fragments. -- Wrap symbols like modules, functions, or classes into double backticks so they are rendered in a ``monospace font``. -- Wrap arguments into asterisks like in docstrings: *these* or *attributes*. -- If you mention functions or other callables, add parentheses at the end of their names: ``attr.func()`` or ``attr.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. -- 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: - - .. code-block:: rst - - Added ``attr.validators.func()``. - The feature really *is* awesome. - -or: - - .. code-block:: rst - - ``attr.func()`` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. - The bug really *was* nasty. - ----- - -``tox -e changelog`` will render the current changelog to the terminal if you have any doubts. - - -Local Development Environment ------------------------------ - -You can (and should) run our test suite using tox_. -However, you’ll probably want a more traditional environment as well. -We highly recommend to develop using the latest Python 3 release because ``attrs`` tries to take advantage of modern features whenever possible. - -First create a `virtual environment `_. -It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like `pew `_, `virtualfish `_, and `virtualenvwrapper `_. - -Next, get an up to date checkout of the ``attrs`` repository: - -.. code-block:: bash - - $ git clone git@github.com:python-attrs/attrs.git - -or if you want to use git via ``https``: - -.. code-block:: bash - - $ git clone https://github.com/python-attrs/attrs.git - -Change into the newly created directory and **after activating your virtual environment** install an editable version of ``attrs`` along with its tests and docs requirements: - -.. code-block:: bash - - $ cd attrs - $ pip install -e '.[dev]' - -At this point, - -.. code-block:: bash - - $ python -m pytest - -should work and pass, as should: - -.. code-block:: bash - - $ cd docs - $ make html - -The built documentation can then be found in ``docs/_build/html/``. - -To avoid committing code that violates our style guide, we strongly advise you to install pre-commit_ [#f1]_ hooks: - -.. code-block:: bash - - $ pre-commit install - -You can also run them anytime (as our tox does) using: - -.. code-block:: bash - - $ pre-commit run --all-files - - -.. [#f1] pre-commit should have been installed into your virtualenv automatically when you ran ``pip install -e '.[dev]'`` above. If pre-commit is missing, it may be that you need to re-run ``pip install -e '.[dev]'``. - - -Governance ----------- - -``attrs`` is maintained by `team of volunteers`_ that is always open to new members that share our vision of a fast, lean, and magic-free library that empowers programmers to write better code with less effort. -If you'd like to join, just get a pull request merged and ask to be added in the very same pull request! - -**The simple rule is that everyone is welcome to review/merge pull requests of others but nobody is allowed to merge their own code.** - -`Hynek Schlawack`_ acts reluctantly as the BDFL_ and has the final say over design decisions. - - -**** - -Please note that this project is released with a Contributor `Code of Conduct`_. -By participating in this project you agree to abide by its terms. -Please report any harm to `Hynek Schlawack`_ in any way you find appropriate. - -Thank you for considering contributing to ``attrs``! - - -.. _`Hynek Schlawack`: https://hynek.me/about/ -.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ -.. _`PEP 257`: https://www.python.org/dev/peps/pep-0257/ -.. _`good test docstrings`: https://jml.io/pages/test-docstrings.html -.. _`Code of Conduct`: https://github.com/python-attrs/attrs/blob/main/.github/CODE_OF_CONDUCT.md -.. _changelog: https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst -.. _`backward compatibility`: https://www.attrs.org/en/latest/backward-compatibility.html -.. _tox: https://tox.readthedocs.io/ -.. _pyenv: https://github.com/pyenv/pyenv -.. _reStructuredText: https://www.sphinx-doc.org/en/stable/usage/restructuredtext/basics.html -.. _semantic newlines: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ -.. _examples page: https://github.com/python-attrs/attrs/blob/main/docs/examples.rst -.. _Hypothesis: https://hypothesis.readthedocs.io/ -.. _CI: https://github.com/python-attrs/attrs/actions?query=workflow%3ACI -.. _`team of volunteers`: https://github.com/python-attrs -.. _BDFL: https://en.wikipedia.org/wiki/Benevolent_dictator_for_life -.. _towncrier: https://pypi.org/project/towncrier -.. _black: https://github.com/psf/black -.. _pre-commit: https://pre-commit.com/ -.. _isort: https://github.com/PyCQA/isort diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dcb4823ca..9e635705f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,7 @@ Changes for the upcoming release can be found in the `"changelog.d" directory `_. Feel free to browse and add your own! -If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! +If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! ``attrs`` for Enterprise diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index 8fbb03c9e..000000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _contributing: - -.. include:: ../.github/CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst index 29082f234..8d816f808 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -91,7 +91,6 @@ Full Table of Contents license backward-compatibility python-2 - contributing changelog From d6771729c598b17200c55eb53adadada85df4044 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 09:38:19 +0100 Subject: [PATCH 068/139] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f2d32fe00..eea2bd607 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -191,7 +191,7 @@ $ make html The built documentation can then be found in `docs/_build/html/`. -To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*][^dev] hooks: +To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] [^dev] hooks: ```console $ pre-commit install @@ -203,7 +203,8 @@ You can also run them anytime (as our tox does) using: $ pre-commit run --all-files ``` -[^dev]: *pre-commit* should have been installed into your virtualenv automatically when you ran `pip install -e '.[dev]'` above. If pre-commit is missing, it may be that you need to re-run `pip install -e '.[dev]'`. +[^dev]: *pre-commit* should have been installed into your virtualenv automatically when you ran `pip install -e '.[dev]'` above. + If *pre-commit* is missing, your probably need to run `pip install -e '.[dev]'` again. Governance From e82e3404227de49ad6eb4ce0559c3ad837a3f33a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 09:39:58 +0100 Subject: [PATCH 069/139] Smarten some endashes Signed-off-by: Hynek Schlawack --- .github/CONTRIBUTING.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index eea2bd607..f58e9e6ff 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -51,8 +51,8 @@ The official tag is `python-attrs` and helping out in support frees us up to imp ``` - If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. - We use [*isort*](https://github.com/PyCQA/isort) to sort our imports, and we use [*Black*](https://github.com/psf/black) with line length of 79 characters to format our code. - As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both -- see [*Local Development Environment*](#local-development-environment) below), you won't have to spend any time on formatting your code at all. - If you don't, [CI] will catch it for you -- but that seems like a waste of your time! + As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both – see [*Local Development Environment*](#local-development-environment) below), you won't have to spend any time on formatting your code at all. + If you don't, [CI] will catch it for you – but that seems like a waste of your time! ## Tests @@ -108,7 +108,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp If your change is noteworthy, there needs to be a changelog entry so our users can learn about it! To avoid merge conflicts, we use the [*towncrier*](https://pypi.org/project/towncrier) package to manage our changelog. -*towncrier* uses independent files for each pull request -- so called *news fragments* -- instead of one monolithic changelog file. +*towncrier* uses independent files for each pull request – so called *news fragments* – instead of one monolithic changelog file. On release, those news fragments are compiled into our [`CHANGELOG.rst`](https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst). You don't need to install *towncrier* yourself, you just have to abide by a few simple rules: @@ -142,7 +142,7 @@ or: The bug really *was* nasty. ``` ----- +--- ``tox -e changelog`` will render the current changelog to the terminal if you have any doubts. @@ -207,8 +207,7 @@ $ pre-commit run --all-files If *pre-commit* is missing, your probably need to run `pip install -e '.[dev]'` again. -Governance ----------- +## Governance `attrs` is maintained by [team of volunteers](https://github.com/python-attrs) that is always open to new members that share our vision of a fast, lean, and magic-free library that empowers programmers to write better code with less effort. If you'd like to join, just get a pull request merged and ask to be added in the very same pull request! From c89d1f9d4137095b201fe613ef2d35a0a9aa1755 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 09:46:44 +0100 Subject: [PATCH 070/139] Fix contributing link --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 35f3b62a0..6e48ba2e6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,7 +5,7 @@ Please tell us what your pull request is about here. # Pull Request Check List -This is just a friendly reminder about the most common mistakes. Please make sure that you tick all boxes. But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles! +This is just a friendly reminder about the most common mistakes. Please make sure that you tick all boxes. But please read our [contribution guide](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles! If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do. If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing. From 9bacb3c74a2623080b141eb75e86ecf66bd222fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Thu, 25 Nov 2021 03:52:49 -0500 Subject: [PATCH 071/139] Extract PYTHON_LATEST in GHA (#873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gábor Lipták Co-authored-by: Hynek Schlawack --- .github/workflows/main.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be6fc6d5d..6dba41d86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,7 @@ on: env: FORCE_COLOR: "1" # Make tools pretty. TOX_TESTENV_PASSENV: "FORCE_COLOR" + PYTHON_LATEST: "3.10" jobs: @@ -55,7 +56,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: "3.10" # Use latest, so it understands all syntax. + python-version: ${{env.PYTHON_LATEST}} # Use latest, so it understands all syntax. - run: "python -m pip install --upgrade coverage[toml]" @@ -84,7 +85,7 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: - python-version: "3.10" + python-version: ${{env.PYTHON_LATEST}} - name: "Install build and lint tools" run: "python -m pip install build twine check-wheel-contents" @@ -106,7 +107,7 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: - python-version: "3.10" + python-version: ${{env.PYTHON_LATEST}} - name: "Install in dev mode" run: "python -m pip install -e .[dev]" - name: "Import package" From b403b91f484d518f064eaa7355d205fd777e7b80 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 10:00:10 +0100 Subject: [PATCH 072/139] Forgotten rst --- .github/CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f58e9e6ff..a80fbd553 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -68,7 +68,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp - To run the test suite, all you need is a recent [*tox*]. It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. - If you lack some Python versions, you can can always limit the environments like `tox -e py27,py38` or make it a non-failure using `tox --skip-missing-interpreters`. + If you lack some Python versions, you can can always limit the environments like `tox -e py27,py38`, or make it a non-failure using `tox --skip-missing-interpreters`. In that case you should look into [*asdf*](https://asdf-vm.com) or [*pyenv*](https://github.com/pyenv/pyenv), which make it very easy to install many different Python versions in parallel. - Write [good test docstrings](https://jml.io/pages/test-docstrings.html). @@ -156,13 +156,13 @@ We highly recommend to develop using the latest Python release because `attrs` t First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://github.com/direnv/direnv/wiki/Python), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). -Next, get an up to date checkout of the ``attrs`` repository: +Next, get an up to date checkout of the `attrs` repository: ```console $ git clone git@github.com:python-attrs/attrs.git ``` -or if you want to use git via ``https``: +or if you want to use git via `https`: ```console $ git clone https://github.com/python-attrs/attrs.git From 0b95a0c76ccd67fd6feae3d82c3f9eb9673fbc22 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 25 Nov 2021 17:06:12 +0100 Subject: [PATCH 073/139] Fold backward-compatibility into changelog Signed-off-by: Hynek Schlawack --- .github/CONTRIBUTING.md | 4 ++-- CHANGELOG.rst | 10 ++++++++++ docs/backward-compatibility.rst | 19 ------------------- docs/index.rst | 1 - 4 files changed, 12 insertions(+), 22 deletions(-) delete mode 100644 docs/backward-compatibility.rst diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a80fbd553..c84b9799d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,7 +31,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp - Make sure your changes pass our [CI]. You won't get any feedback until it's green unless you ask for it. - 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 [backward compatibility](https://www.attrs.org/en/latest/backward-compatibility.html). +- Don’t break backwards compatibility. ## Code @@ -151,7 +151,7 @@ or: You can (and should) run our test suite using [*tox*]. However, you’ll probably want a more traditional environment as well. -We highly recommend to develop using the latest Python release because `attrs` tries to take advantage of modern features whenever possible. +We highly recommend to develop using the latest Python release because we try to take advantage of modern features whenever possible. First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://github.com/direnv/direnv/wiki/Python), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e635705f..413be11d3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,16 @@ Changelog Versions follow `CalVer `_ with a strict backwards compatibility policy. The third digit is only for regressions. +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. + +.. warning:: + + The structure of the `attr.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. + Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. .. diff --git a/docs/backward-compatibility.rst b/docs/backward-compatibility.rst deleted file mode 100644 index c1165be14..000000000 --- a/docs/backward-compatibility.rst +++ /dev/null @@ -1,19 +0,0 @@ -Backward Compatibility -====================== - -.. currentmodule:: attr - -``attrs`` has a very strong backward compatibility policy that is inspired by the policy of the `Twisted framework `_. - -Put simply, you shouldn't ever be afraid to upgrade ``attrs`` if you're only using its public APIs. -If there will ever be a need to break compatibility, it will be announced in the `changelog` and raise a ``DeprecationWarning`` for a year (if possible) before it's finally really broken. - - -.. _exemption: - -.. warning:: - - The structure of the `attr.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/docs/index.rst b/docs/index.rst index 8d816f808..821280357 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -89,7 +89,6 @@ Full Table of Contents :maxdepth: 1 license - backward-compatibility python-2 changelog From ff65c6e64549066d3da65d9fc478d7df9540caf0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 26 Nov 2021 07:16:31 +0100 Subject: [PATCH 074/139] Link to more concrete blog post --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c84b9799d..818d24a54 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -154,7 +154,7 @@ However, you’ll probably want a more traditional environment as well. We highly recommend to develop using the latest Python release because we try to take advantage of modern features whenever possible. First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. -It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://github.com/direnv/direnv/wiki/Python), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). +It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://hynek.me/til/python-project-local-venvs/), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). Next, get an up to date checkout of the `attrs` repository: From ebe158cf8a302840e2d2275cef473b1f246e62dd Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 26 Nov 2021 07:19:16 +0100 Subject: [PATCH 075/139] Add more sensical example --- .github/CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 818d24a54..4229bc30a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -117,8 +117,10 @@ You don't need to install *towncrier* yourself, you just have to abide by a few For example, `changelog.d/42.change.rst` for a non-breaking change that is proposed in pull request #42. - As with other docs, please use [semantic newlines] within news fragments. - Wrap symbols like modules, functions, or classes into double backticks so they are rendered in a `monospace font`. -- Wrap arguments into asterisks like in docstrings: *these* or *attributes*. -- If you mention functions or other callables, add parentheses at the end of their names: `attr.func()` or `attr.Class.method()`. +- 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()`. This makes the changelog a lot more readable. - Prefer simple past tense or constructions with "now". For example: From 38b299d4a4fcf6f7c7c9f8ccc18b18bdee3ec9f1 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 28 Nov 2021 15:04:39 +0100 Subject: [PATCH 076/139] Use importlib.metadata to find the version in Sphinx docs --- docs/conf.py | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 42af10f84..aa42845b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,30 +1,4 @@ -import codecs -import os -import re - - -def read(*parts): - """ - Build an absolute path from *parts* and and return the contents of the - resulting file. Assume UTF-8 encoding. - """ - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: - return f.read() - - -def find_version(*file_paths): - """ - Build a path from *file_paths* and search for a ``__version__`` - string inside. - """ - version_file = read(*file_paths) - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M - ) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") +from importlib import metadata # -- General configuration ------------------------------------------------ @@ -75,11 +49,11 @@ def find_version(*file_paths): # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + +# The full version, including alpha/beta/rc tags. +release = metadata.version("attrs") # The short X.Y version. -release = find_version("../src/attr/__init__.py") version = release.rsplit(".", 1)[0] -# The full version, including alpha/beta/rc tags. # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From 45c263755f9435906d9f7913ea607e871e6b29b4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 29 Nov 2021 09:03:39 +0100 Subject: [PATCH 077/139] Move docs to 3.10 --- .readthedocs.yml | 2 +- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index d80f42170..add43471a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ formats: all python: # Keep version in sync with tox.ini (docs and gh-actions). - version: 3.8 + version: 3.10 install: - method: pip diff --git a/tox.ini b/tox.ini index 85d53773d..ddcbc4dbb 100644 --- a/tox.ini +++ b/tox.ini @@ -14,9 +14,9 @@ python = 3.5: py35 3.6: py36 3.7: py37 - 3.8: py38, changelog, docs + 3.8: py38, changelog 3.9: py39, pyright - 3.10: py310, manifest, typing + 3.10: py310, manifest, typing, docs pypy-2: pypy pypy-3: pypy3 @@ -28,7 +28,7 @@ isolated_build = True [testenv:docs] # Keep basepython in sync with gh-actions and .readthedocs.yml. -basepython = python3.8 +basepython = python3.10 extras = docs commands = sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html From 4bc7cf70a9f41bca005834cd87ca9d149a17c089 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 29 Nov 2021 09:09:25 +0100 Subject: [PATCH 078/139] YAML... --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index add43471a..8a5af32ff 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ formats: all python: # Keep version in sync with tox.ini (docs and gh-actions). - version: 3.10 + version: "3.10" install: - method: pip From ff39e86f2f269ce698ba3ce641975660e724057c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 29 Nov 2021 09:13:09 +0100 Subject: [PATCH 079/139] Use new RTD build config --- .readthedocs.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 8a5af32ff..58afb426d 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,10 +2,12 @@ version: 2 formats: all -python: - # Keep version in sync with tox.ini (docs and gh-actions). - version: "3.10" +build: + os: ubuntu-20.04 + tools: + python: "3.10" +python: install: - method: pip path: . From d56471ea94b8ea5b7bd2091f986bf6207e440931 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 29 Nov 2021 09:16:10 +0100 Subject: [PATCH 080/139] Re-add warning --- .readthedocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 58afb426d..d335c40d5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,6 +5,7 @@ formats: all build: os: ubuntu-20.04 tools: + # Keep version in sync with tox.ini (docs and gh-actions). python: "3.10" python: From 34c55613d2b26f45b2d3b7cf010e57188bc7bea5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 29 Nov 2021 09:23:45 +0100 Subject: [PATCH 081/139] Document the common question of derived attributes (#874) --- docs/init.rst | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/init.rst b/docs/init.rst index d4da16982..4b2697898 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -443,6 +443,47 @@ If present, the hooks are executed in the following order: Notably this means, that you can access all attributes from within your validators, but your converters have to deal with invalid values and have to return a valid value. +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. + +The simpler one is using ``__attrs_post_init__``:: + + @define + class APIClient: + token: str + client: WebClient = field(init=False) + + def __attrs_post_init__(self): + self.client = WebClient(self.token) + +The second one is using a decorator-based default:: + + @define + class APIClient: + token: str + client: WebClient = field() # needed! attr.ib works too + + @client.default + def _client_factory(self): + return WebClient(self.token) + +That said, and as pointed out in the beginning of the chapter, a better approach would be to have a factory class method:: + + @define + class APIClient: + client: WebClient + + @classmethod + def from_token(cls, token: str) -> SomeClass: + return cls(client=WebClient(token)) + +This makes the class more testable. + + .. _`Wiki page`: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs .. _`get confused`: https://github.com/python-attrs/attrs/issues/289 .. _`there is no such thing as a private argument`: https://github.com/hynek/characteristic/issues/6 From fe19f4b341dbba5cda14b11d6a184105891c67e5 Mon Sep 17 00:00:00 2001 From: wouter bolsterlee Date: Mon, 29 Nov 2021 19:35:59 +0000 Subject: [PATCH 082/139] Avoid whitelist/blacklist terminology (#878) The code already uses include() and exclude(), so simply tweak the docstrings. --- src/attr/filters.py | 8 ++++---- tests/test_filters.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/attr/filters.py b/src/attr/filters.py index dc47e8fa3..ae5248568 100644 --- a/src/attr/filters.py +++ b/src/attr/filters.py @@ -20,9 +20,9 @@ def _split_what(what): def include(*what): """ - Whitelist *what*. + Include *what*. - :param what: What to whitelist. + :param what: What to include. :type what: `list` of `type` or `attr.Attribute`\\ s :rtype: `callable` @@ -37,9 +37,9 @@ def include_(attribute, value): def exclude(*what): """ - Blacklist *what*. + Exclude *what*. - :param what: What to blacklist. + :param what: What to exclude. :type what: `list` of classes or `attr.Attribute`\\ s. :rtype: `callable` diff --git a/tests/test_filters.py b/tests/test_filters.py index 7a1a41895..c47cca47a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -49,7 +49,7 @@ class TestInclude(object): ) def test_allow(self, incl, value): """ - Return True if a class or attribute is whitelisted. + Return True if a class or attribute is included. """ i = include(*incl) assert i(fields(C).a, value) is True @@ -65,7 +65,7 @@ def test_allow(self, incl, value): ) def test_drop_class(self, incl, value): """ - Return False on non-whitelisted classes and attributes. + Return False on non-included classes and attributes. """ i = include(*incl) assert i(fields(C).a, value) is False @@ -87,7 +87,7 @@ class TestExclude(object): ) def test_allow(self, excl, value): """ - Return True if class or attribute is not blacklisted. + Return True if class or attribute is not excluded. """ e = exclude(*excl) assert e(fields(C).a, value) is True @@ -103,7 +103,7 @@ def test_allow(self, excl, value): ) def test_drop_class(self, excl, value): """ - Return True on non-blacklisted classes and attributes. + Return True on non-excluded classes and attributes. """ e = exclude(*excl) assert e(fields(C).a, value) is False From 1759260aa0bb0ef62fdfcab8636af9d4cfa91c14 Mon Sep 17 00:00:00 2001 From: wouter bolsterlee Date: Tue, 30 Nov 2021 05:32:24 +0000 Subject: [PATCH 083/139] Add support for re.Pattern to validators.matches_re() (#877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #875. This adds support for pre-compiled regular expressions to attr.validators.matches_re(). This cannot be combined with flags since the pre-compiled pattern already has those. Detailed changes: - attr.validators.matches_re() now accepts re.Pattern in addition to strings; update type annotations accordingly. - Convert percent-formatting into str.format() for an error message - Simplify (private) _MatchesReValidator helper class a bit: use the actual compiled pattern, and drop the unused .flags attribute. - Simplify control flow a bit; add pointer about fullmatch emulation. - Add tests - Tweak existing test to ensure that .fullmatch() actually works correctly by matching a pattern that also matches (but not ‘full-matches’) a shorter substring. Co-authored-by: Hynek Schlawack --- changelog.d/877.change.rst | 1 + src/attr/validators.py | 53 ++++++++++++++++++++++++-------------- src/attr/validators.pyi | 3 ++- tests/test_validators.py | 27 +++++++++++++++++-- tests/typing_example.py | 2 +- 5 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 changelog.d/877.change.rst diff --git a/changelog.d/877.change.rst b/changelog.d/877.change.rst new file mode 100644 index 000000000..b90209025 --- /dev/null +++ b/changelog.d/877.change.rst @@ -0,0 +1 @@ +``attr.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. diff --git a/src/attr/validators.py b/src/attr/validators.py index 1eb257225..3896d8346 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -14,6 +14,12 @@ from .exceptions import NotCallableError +try: + Pattern = re.Pattern +except AttributeError: # Python <3.7 lacks a Pattern type. + Pattern = type(re.compile("")) + + __all__ = [ "and_", "deep_iterable", @@ -129,8 +135,7 @@ def instance_of(type): @attrs(repr=False, frozen=True, slots=True) class _MatchesReValidator(object): - regex = attrib() - flags = attrib() + pattern = attrib() match_func = attrib() def __call__(self, inst, attr, value): @@ -139,18 +144,18 @@ def __call__(self, inst, attr, value): """ if not self.match_func(value): raise ValueError( - "'{name}' must match regex {regex!r}" + "'{name}' must match regex {pattern!r}" " ({value!r} doesn't)".format( - name=attr.name, regex=self.regex.pattern, value=value + name=attr.name, pattern=self.pattern.pattern, value=value ), attr, - self.regex, + self.pattern, value, ) def __repr__(self): - return "".format( - regex=self.regex + return "".format( + pattern=self.pattern ) @@ -159,7 +164,7 @@ def matches_re(regex, flags=0, func=None): A validator that raises `ValueError` if the initializer is called with a string that doesn't match *regex*. - :param str regex: a regex string to match against + :param regex: a regex string or precompiled pattern to match against :param int flags: flags that will be passed to the underlying re function (default 0) :param callable func: which underlying `re` function to call (options @@ -169,34 +174,44 @@ def matches_re(regex, flags=0, func=None): but on a pre-`re.compile`\ ed pattern. .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. """ fullmatch = getattr(re, "fullmatch", None) valid_funcs = (fullmatch, None, re.search, re.match) if func not in valid_funcs: raise ValueError( - "'func' must be one of %s." - % ( + "'func' must be one of {}.".format( ", ".join( sorted( e and e.__name__ or "None" for e in set(valid_funcs) ) - ), + ) ) ) - pattern = re.compile(regex, flags) + if isinstance(regex, Pattern): + if flags: + raise TypeError( + "'flags' can only be used with a string pattern; " + "pass flags to re.compile() instead" + ) + pattern = regex + else: + pattern = re.compile(regex, flags) + if func is re.match: match_func = pattern.match elif func is re.search: match_func = pattern.search - else: - if fullmatch: - match_func = pattern.fullmatch - else: - pattern = re.compile(r"(?:{})\Z".format(regex), flags) - match_func = pattern.match + elif fullmatch: + match_func = pattern.fullmatch + else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) + pattern = re.compile( + r"(?:{})\Z".format(pattern.pattern), pattern.flags + ) + match_func = pattern.match - return _MatchesReValidator(pattern, flags, match_func) + return _MatchesReValidator(pattern, match_func) @attrs(repr=False, slots=True, hash=True) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index a4393327c..5e00b8543 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -9,6 +9,7 @@ from typing import ( Mapping, Match, Optional, + Pattern, Tuple, Type, TypeVar, @@ -54,7 +55,7 @@ def optional( def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... def matches_re( - regex: AnyStr, + regex: Union[Pattern[AnyStr], AnyStr], flags: int = ..., func: Optional[ Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] diff --git a/tests/test_validators.py b/tests/test_validators.py index e1b83ed42..fb4382a9f 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -176,9 +176,9 @@ def test_match(self): @attr.s class ReTester(object): - str_match = attr.ib(validator=matches_re("a")) + str_match = attr.ib(validator=matches_re("a|ab")) - ReTester("a") # shouldn't raise exceptions + ReTester("ab") # shouldn't raise exceptions with pytest.raises(TypeError): ReTester(1) with pytest.raises(ValueError): @@ -197,6 +197,29 @@ class MatchTester(object): MatchTester("A1") # test flags and using re.match + def test_precompiled_pattern(self): + """ + Pre-compiled patterns are accepted. + """ + pattern = re.compile("a") + + @attr.s + class RePatternTester(object): + val = attr.ib(validator=matches_re(pattern)) + + RePatternTester("a") + + def test_precompiled_pattern_no_flags(self): + """ + A pre-compiled pattern cannot be combined with a 'flags' argument. + """ + pattern = re.compile("") + + with pytest.raises( + TypeError, match="can only be used with a string pattern" + ): + matches_re(pattern, flags=re.IGNORECASE) + def test_different_func(self): """ Changing the match functions works. diff --git a/tests/typing_example.py b/tests/typing_example.py index af124661a..75b41b89d 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -167,7 +167,7 @@ class Validated: attr.validators.instance_of(C), attr.validators.instance_of(D) ), ) - e: str = attr.ib(validator=attr.validators.matches_re(r"foo")) + e: str = attr.ib(validator=attr.validators.matches_re(re.compile(r"foo"))) f: str = attr.ib( validator=attr.validators.matches_re(r"foo", flags=42, func=re.search) ) From 659b59c64f865f43863601f5c7f4cd5d0beb4925 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 4 Dec 2021 08:31:34 +0100 Subject: [PATCH 084/139] Link David's blog post --- docs/names.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/names.rst b/docs/names.rst index 56d7d4458..abfdba480 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -71,7 +71,7 @@ A big change happened in May 2017 when Hynek sat down with `Guido van Rossum `_ 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 implementing 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. ----- From 7c9f5a8dd2b9cdeaf58685961f1bcdf9be00dc1e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 5 Dec 2021 06:15:08 +0100 Subject: [PATCH 085/139] Update main.yml --- .github/workflows/main.yml | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6dba41d86..e5aa1ee95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: python -VV python -m site python -m pip install --upgrade pip setuptools wheel - python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions + python -m pip install --upgrade virtualenv tox tox-gh-actions - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" @@ -56,25 +56,29 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: ${{env.PYTHON_LATEST}} # Use latest, so it understands all syntax. + # Use latest Python, so it understands all syntax. + python-version: ${{env.PYTHON_LATEST}} - - run: "python -m pip install --upgrade coverage[toml]" + - name: Install Coverage.py + run: python -m pip install --upgrade coverage[toml] - - name: "Download coverage data" + - name: Download coverage data uses: actions/download-artifact@v2 with: name: coverage-data - - run: python -m coverage combine - - run: python -m coverage html --skip-covered --skip-empty + - 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 coverage report" - uses: "actions/upload-artifact@v2" + - name: Upload HTML report for failed check + uses: actions/upload-artifact@v2 with: name: html-report path: htmlcov - - - run: python -m coverage report --fail-under=100 + if: ${{ failure() }} package: @@ -87,8 +91,7 @@ jobs: with: python-version: ${{env.PYTHON_LATEST}} - - name: "Install build and lint tools" - run: "python -m pip install build twine check-wheel-contents" + - 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" @@ -108,7 +111,6 @@ jobs: - uses: "actions/setup-python@v2" with: python-version: ${{env.PYTHON_LATEST}} - - name: "Install in dev mode" - run: "python -m pip install -e .[dev]" + - run: "python -m pip install -e .[dev]" - name: "Import package" run: "python -c 'import attr; print(attr.__version__)'" From 868c67e225bf5d4ecb982fc5cdaed040dc17d459 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:43:27 +0100 Subject: [PATCH 086/139] [pre-commit.ci] pre-commit autoupdate (#882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- src/attr/_compat.py | 1 - src/attr/_make.py | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 218b70497..a29c55487 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black exclude: tests/test_pattern_matching.py diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 9d03ac196..90026fec3 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -110,7 +110,6 @@ def just_warn(*args, **kw): # pragma: no cover consequences of not setting the cell on Python 2. """ - else: # Python 3 and later. from collections.abc import Mapping, Sequence # noqa diff --git a/src/attr/_make.py b/src/attr/_make.py index 89ae96c94..7f22b273b 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -629,7 +629,6 @@ def _frozen_setattrs(self, name, value): raise FrozenInstanceError() - else: def _frozen_setattrs(self, name, value): @@ -1628,7 +1627,6 @@ def _has_frozen_base_class(cls): and cls.__setattr__.__name__ == _frozen_setattrs.__name__ ) - else: def _has_frozen_base_class(cls): @@ -1929,7 +1927,6 @@ def _make_repr(attrs, ns, cls): "__repr__", "\n".join(lines), unique_filename, globs=globs ) - else: def _make_repr(attrs, ns, _): From dfa725bdfc6ea64935a80bffae4bac47badbc425 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 8 Dec 2021 07:10:11 +0100 Subject: [PATCH 087/139] Link to pyright's list of incompatibilities --- docs/types.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/types.rst b/docs/types.rst index f2dffca7d..19bb07930 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -90,9 +90,6 @@ Given the following definition, ``pyright`` will generate static type signatures .. warning:: - ``dataclass_transform``-based types are supported provisionally as of ``pyright`` 1.1.135 and ``attrs`` 21.1. - Both the ``pyright`` dataclass_transform_ specification and ``attrs`` implementation may changed in future versions. - The ``pyright`` inferred types are a subset of those supported by ``mypy``, including: - The generated ``__init__`` signature only includes the attribute type annotations. @@ -100,7 +97,10 @@ 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. + 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. .. _mypy: http://mypy-lang.org From 8ae0bd904d6147ce37750fa7ec336f951c14495c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 12 Dec 2021 10:25:50 +0100 Subject: [PATCH 088/139] Add _CompareWithType (#884) It got accidentally removed in https://github.com/python-attrs/attrs/pull/787/commits/6f5c5ce42f5c406e446dd3d4863c17f61c7254a7 --- src/attr/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 610eced79..a2b23dcc6 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -51,6 +51,7 @@ _OnSetAttrArgType = Union[ _FieldTransformer = Callable[ [type, List[Attribute[Any]]], List[Attribute[Any]] ] +_CompareWithType = Callable[[Any, Any], bool] # FIXME: in reality, if multiple validators are passed they must be in a list # or tuple, but those are invariant and so would prevent subtypes of # _ValidatorType from working when passed in a list or tuple. From 679e4b443d5558d5e64133ec9047e7a4e737426b Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 14 Dec 2021 15:50:37 +0100 Subject: [PATCH 089/139] NG: convert on setattr by default (#886) * NG: convert on setattr by default Not doing that from the get-go was an oversight. Fixes #835 * Add optimization for default on_setattr w/ no work to do Otherwise we'd end up with an explicit setattr every time. * Fix optimization for NG default & j/ convert * NG is actually 3.6+ * Add test for convert optimization for good measure --- changelog.d/835.breaking.rst | 4 +++ changelog.d/886.breaking.rst | 4 +++ docs/api.rst | 4 ++- src/attr/_make.py | 32 ++++++++++++++----- src/attr/_next_gen.py | 22 ++++++++++---- tests/test_functional.py | 59 ++++++++++++++++++++++++++++++++++-- tests/test_next_gen.py | 25 +++++++++++++++ 7 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 changelog.d/835.breaking.rst create mode 100644 changelog.d/886.breaking.rst diff --git a/changelog.d/835.breaking.rst b/changelog.d/835.breaking.rst new file mode 100644 index 000000000..8cdf0412d --- /dev/null +++ b/changelog.d/835.breaking.rst @@ -0,0 +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]``. + +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/886.breaking.rst b/changelog.d/886.breaking.rst new file mode 100644 index 000000000..8cdf0412d --- /dev/null +++ b/changelog.d/886.breaking.rst @@ -0,0 +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]``. + +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/docs/api.rst b/docs/api.rst index afbc87025..1dcaf978e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -707,13 +707,15 @@ The most notable differences are: - *auto_exc=True* - *auto_detect=True* - *eq=True*, but *order=False* -- Validators run when you set an attribute (*on_setattr=attr.setters.validate*). +- 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. diff --git a/src/attr/_make.py b/src/attr/_make.py index 7f22b273b..990786954 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -59,6 +59,8 @@ # Unique object for unequivocal getattr() defaults. _sentinel = object() +_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) + class _Nothing(object): """ @@ -722,13 +724,31 @@ def __init__( self._cls_dict["__delattr__"] = _frozen_delattrs self._wrote_own_setattr = True - elif on_setattr == setters.validate: + elif on_setattr in ( + _ng_default_on_setattr, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False for a in attrs: if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: break - else: - # If class-level on_setattr is set to validating, but there's - # no field to validate, pretend like there's no on_setattr. + if ( + ( + on_setattr == _ng_default_on_setattr + and not (has_validator or has_converter) + ) + or (on_setattr == setters.validate and not has_validator) + or (on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. self._on_setattr = None if getstate_setstate: @@ -2123,9 +2143,7 @@ def _make_init( raise ValueError("Frozen classes can't use on_setattr.") needs_cached_setattr = True - elif ( - has_cls_on_setattr and a.on_setattr is not setters.NO_OP - ) or _is_slot_attr(a.name, base_attr_map): + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: needs_cached_setattr = True unique_filename = _generate_unique_filename(cls, "init") diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 1d8acac36..843447173 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -8,7 +8,13 @@ from attr.exceptions import UnannotatedAttributeError from . import setters -from ._make import NOTHING, _frozen_setattrs, attrib, attrs +from ._make import ( + NOTHING, + _frozen_setattrs, + _ng_default_on_setattr, + attrib, + attrs, +) def define( @@ -35,8 +41,10 @@ def define( match_args=True, ): r""" - The only behavioral differences are the handling of the *auto_attribs* - option: + Define an ``attrs`` class. + + The behavioral differences to `attr.s` are the handling of the + *auto_attribs* option: :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: @@ -46,9 +54,11 @@ def define( 2. Otherwise it assumes *auto_attribs=False* and tries to collect `attr.ib`\ s. - and that mutable classes (``frozen=False``) validate on ``__setattr__``. + and that mutable classes (``frozen=False``) convert and validate on + ``__setattr__``. .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. """ def do_it(cls, auto_attribs): @@ -86,9 +96,9 @@ def wrap(cls): had_on_setattr = on_setattr not in (None, setters.NO_OP) - # By default, mutable classes validate on setattr. + # By default, mutable classes convert & validate on setattr. if frozen is False and on_setattr is None: - on_setattr = setters.validate + on_setattr = _ng_default_on_setattr # However, if we subclass a frozen class, we inherit the immutability # and disable on_setattr. diff --git a/tests/test_functional.py b/tests/test_functional.py index e9aa3c267..c616f8c1f 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -17,7 +17,7 @@ import attr -from attr._compat import PY2, TYPE +from attr._compat import PY2, PY36, TYPE from attr._make import NOTHING, Attribute from attr.exceptions import FrozenInstanceError @@ -692,8 +692,9 @@ class C(object): @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_if_validate_without_validators(self, slots): """ - If a class has on_setattr=attr.setters.validate (default in NG APIs) - but sets no validators, don't use the (slower) setattr in __init__. + If a class has on_setattr=attr.setters.validate (former default in NG + APIs) but sets no validators, don't use the (slower) setattr in + __init__. Regression test for #816. """ @@ -713,6 +714,58 @@ class D(C): assert "self.y = y" in src assert object.__setattr__ == D.__setattr__ + @pytest.mark.parametrize("slots", [True, False]) + def test_no_setattr_if_convert_without_converters(self, slots): + """ + If a class has on_setattr=attr.setters.convert but sets no validators, + don't use the (slower) setattr in __init__. + """ + + @attr.s(on_setattr=attr.setters.convert) + class C(object): + x = attr.ib() + + @attr.s(on_setattr=attr.setters.convert) + class D(C): + y = attr.ib() + + src = inspect.getsource(D.__init__) + + assert "setattr" not in src + assert "self.x = x" in src + assert "self.y = y" in src + assert object.__setattr__ == D.__setattr__ + + @pytest.mark.skipif(not PY36, reason="NG APIs are 3.6+") + @pytest.mark.parametrize("slots", [True, False]) + def test_no_setattr_with_ng_defaults(self, slots): + """ + If a class has the NG default on_setattr=[convert, validate] but sets + no validators or converters, don't use the (slower) setattr in + __init__. + """ + + @attr.define + class C(object): + x = attr.ib() + + src = inspect.getsource(C.__init__) + + assert "setattr" not in src + assert "self.x = x" in src + assert object.__setattr__ == C.__setattr__ + + @attr.define + class D(C): + y = attr.ib() + + src = inspect.getsource(D.__init__) + + assert "setattr" not in src + assert "self.x = x" in src + assert "self.y = y" in src + assert object.__setattr__ == D.__setattr__ + def test_on_setattr_detect_inherited_validators(self): """ _make_init detects the presence of a validator even if the field is diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index fce01ad45..fdc06b719 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -308,3 +308,28 @@ class MyException(Exception): assert "foo" == ei.value.x assert ei.value.__cause__ is None + + def test_converts_and_validates_by_default(self): + """ + If no on_setattr is set, assume setters.convert, setters.validate. + """ + + @attr.define + class C: + x: int = attr.field(converter=int) + + @x.validator + def _v(self, _, value): + if value < 10: + raise ValueError("must be >=10") + + inst = C(10) + + # Converts + inst.x = "11" + + assert 11 == inst.x + + # Validates + with pytest.raises(ValueError, match="must be >=10"): + inst.x = "9" From 9ef5ff77401116ad96864c480c7f4dbde345a5c3 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 14 Dec 2021 17:29:59 +0100 Subject: [PATCH 090/139] Add test to double check mro-collection by default in NG --- tests/test_make.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index 8b4a3f9e7..2b723058a 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -21,7 +21,7 @@ import attr from attr import _config -from attr._compat import PY2, PY310, ordered_dict +from attr._compat import PY2, PY36, PY310, ordered_dict from attr._make import ( Attribute, Factory, @@ -402,7 +402,7 @@ def test_mro(self): See #428 """ - @attr.s + @attr.s(collect_by_mro=True) class A(object): x = attr.ib(10) @@ -410,11 +410,11 @@ class A(object): def xx(self): return 10 - @attr.s + @attr.s(collect_by_mro=True) class B(A): y = attr.ib(20) - @attr.s + @attr.s(collect_by_mro=True) class C(A): x = attr.ib(50) @@ -429,6 +429,41 @@ class D(B, C): assert d.x == d.xx() + @pytest.mark.skipif(not PY36, reason="NG is 3.6+-only") + def test_mro_ng(self): + """ + Attributes and methods are looked up the same way in NG by default. + + See #428 + """ + + @attr.define + class A: + + x: int = 10 + + def xx(self): + return 10 + + @attr.define + class B(A): + y: int = 20 + + @attr.define + class C(A): + x: int = 50 + + def xx(self): + return 50 + + @attr.define + class D(B, C): + pass + + d = D() + + assert d.x == d.xx() + def test_inherited(self): """ Inherited Attributes have `.inherited` True, otherwise False. From e79b0a72efd24b8840d71dc079bf0bc7965d11f9 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 15 Dec 2021 07:11:04 +0100 Subject: [PATCH 091/139] Move 3.6-syntaxed test to test_next_gen.py --- tests/test_make.py | 37 +------------------------------------ tests/test_next_gen.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index 2b723058a..6f4888ace 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -21,7 +21,7 @@ import attr from attr import _config -from attr._compat import PY2, PY36, PY310, ordered_dict +from attr._compat import PY2, PY310, ordered_dict from attr._make import ( Attribute, Factory, @@ -429,41 +429,6 @@ class D(B, C): assert d.x == d.xx() - @pytest.mark.skipif(not PY36, reason="NG is 3.6+-only") - def test_mro_ng(self): - """ - Attributes and methods are looked up the same way in NG by default. - - See #428 - """ - - @attr.define - class A: - - x: int = 10 - - def xx(self): - return 10 - - @attr.define - class B(A): - y: int = 20 - - @attr.define - class C(A): - x: int = 50 - - def xx(self): - return 50 - - @attr.define - class D(B, C): - pass - - d = D() - - assert d.x == d.xx() - def test_inherited(self): """ Inherited Attributes have `.inherited` True, otherwise False. diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index fdc06b719..a2ed7fe67 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -333,3 +333,37 @@ def _v(self, _, value): # Validates with pytest.raises(ValueError, match="must be >=10"): inst.x = "9" + + def test_mro_ng(self): + """ + Attributes and methods are looked up the same way in NG by default. + + See #428 + """ + + @attr.define + class A: + + x: int = 10 + + def xx(self): + return 10 + + @attr.define + class B(A): + y: int = 20 + + @attr.define + class C(A): + x: int = 50 + + def xx(self): + return 50 + + @attr.define + class D(B, C): + pass + + d = D() + + assert d.x == d.xx() From 3833e4f49663bdecbde463535c6dca3461fa5209 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 15 Dec 2021 08:08:31 +0100 Subject: [PATCH 092/139] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 413be11d3..3773a973d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -130,7 +130,7 @@ Backward-incompatible Changes - ``attr.define()``, ``attr.frozen()``, ``attr.mutable()``, and ``attr.field()`` remain **provisional**. - This release does **not** change change anything about them and they are already used widely in production though. + This release does **not** change anything about them and they are already used widely in production though. If you wish to use them together with mypy, you can simply drop `this plugin `_ into your project. From bd0d0cc9fe633c57607f5124d5e4a979a7c6a16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Wed, 15 Dec 2021 13:57:16 +0100 Subject: [PATCH 093/139] Modernize docs some more (#885) * Modernize docs some more * Fix doctest * Tweak docs * Update docs/examples.rst Co-authored-by: Hynek Schlawack --- docs/examples.rst | 21 ++++++++++++--------- docs/types.rst | 7 +++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 03d7e38d3..075f4e0c6 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -199,7 +199,6 @@ For that, `attr.asdict` offers a callback that decides whether an attribute shou .. doctest:: - >>> from typing import List >>> from attr import asdict >>> @define @@ -209,7 +208,7 @@ For that, `attr.asdict` offers a callback that decides whether an attribute shou >>> @define ... class UserList: - ... users: List[User] + ... users: list[User] >>> asdict(UserList([User("jane@doe.invalid", "s33kred"), ... User("joe@doe.invalid", "p4ssw0rd")]), @@ -503,12 +502,12 @@ If you don't mind annotating *all* attributes, you can even drop the `field` and >>> @define ... class AutoC: ... cls_var: typing.ClassVar[int] = 5 # this one is ignored - ... l: typing.List[int] = Factory(list) + ... l: list[int] = Factory(list) ... x: int = 1 ... foo: str = "every attrib needs a type if auto_attribs=True" ... bar: typing.Any = None >>> fields(AutoC).l.type - typing.List[int] + list[int] >>> fields(AutoC).x.type >>> fields(AutoC).foo.type @@ -522,18 +521,17 @@ 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 strings (e.g. forward references), +If your annotations contain forward references, you can resolve these after all references have been defined by using :func:`attr.resolve_types`. This will replace the *type* attribute in the respective fields. .. doctest:: - >>> import typing >>> from attr import fields, resolve_types >>> @define ... class A: - ... a: typing.List['A'] + ... a: 'list[A]' ... b: 'B' ... >>> @define @@ -541,16 +539,21 @@ This will replace the *type* attribute in the respective fields. ... a: A ... >>> fields(A).a.type - typing.List[ForwardRef('A')] + 'list[A]' >>> fields(A).b.type 'B' >>> resolve_types(A, globals(), locals()) >>> fields(A).a.type - typing.List[A] + list[A] >>> fields(A).b.type +.. note:: + + If you find yourself using string type annotations to handle forward references, wrap the entire type annotation in quotes instead of only the type you need a forward reference to (so ``'list[A]'`` instead of ``list['A']``). + This is a limitation of the Python typing system. + .. warning:: ``attrs`` itself doesn't have any features that work on top of type metadata *yet*. diff --git a/docs/types.rst b/docs/types.rst index 19bb07930..5a71c393e 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -10,12 +10,11 @@ That means that on modern Python versions, the declaration part of the example f .. doctest:: >>> import attr - >>> import typing >>> @attr.s(auto_attribs=True) ... class SomeClass: ... a_number: int = 42 - ... list_of_numbers: typing.List[int] = attr.Factory(list) + ... list_of_numbers: list[int] = attr.Factory(list) >>> sc = SomeClass(1, [1, 2, 3]) >>> sc @@ -71,7 +70,7 @@ To mypy, this code is equivalent to the one above: @attr.s class SomeClass(object): a_number = attr.ib(default=42) # type: int - list_of_numbers = attr.ib(factory=list, type=typing.List[int]) + list_of_numbers = attr.ib(factory=list, type=list[int]) pyright @@ -86,7 +85,7 @@ Given the following definition, ``pyright`` will generate static type signatures @attr.define class SomeClass: a_number: int = 42 - list_of_numbers: typing.List[int] = attr.field(factory=list) + list_of_numbers: list[int] = attr.field(factory=list) .. warning:: From a955efeb415449a5ff90ac9b92c6d5f430b97430 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 24 Nov 2021 09:42:17 +0100 Subject: [PATCH 094/139] Explicitly call out DCs in README --- README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.rst b/README.rst index af4c37e56..6d04884ce 100644 --- a/README.rst +++ b/README.rst @@ -91,6 +91,17 @@ The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, an Please check out `On The Core API Names `_ for a more in-depth explanation. + +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. +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 `_. + + .. -getting-help- Getting Help From 02d61102ab8ec2291552103f1fb3bc93e8e36b54 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 08:14:31 +0100 Subject: [PATCH 095/139] Looks like pepy.tech isn't backfilling --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 6d04884ce..08e3952b2 100644 --- a/README.rst +++ b/README.rst @@ -15,9 +15,6 @@ - - Downloads per month -

.. teaser-begin From 430b12ef0c539f28392b7d818af8dd8351e6c72b Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 08:18:17 +0100 Subject: [PATCH 096/139] Clarify version scheme --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3773a973d..e5c603ce7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,10 @@ Changelog ========= Versions follow `CalVer `_ with a strict backwards compatibility policy. -The third digit is only for regressions. + +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). 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 de0f0138ae4694c2da3b1920e9552a2673583daf Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 08:23:50 +0100 Subject: [PATCH 097/139] Composite adjective --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e5c603ce7..2597ce9c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -Versions follow `CalVer `_ with a strict backwards compatibility policy. +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. From 17067930128e78a9c3c7c5280b5417b7bce82657 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 08:28:19 +0100 Subject: [PATCH 098/139] Mention coverage --- .github/PULL_REQUEST_TEMPLATE.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6e48ba2e6..565323f62 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,19 +5,25 @@ Please tell us what your pull request is about here. # Pull Request Check List -This is just a friendly reminder about the most common mistakes. Please make sure that you tick all boxes. But please read our [contribution guide](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles! +This is just a friendly reminder about the most common mistakes. +Please make sure that you tick all boxes. +But please read our [contribution guide](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles! -If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do. If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing. +If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do. +If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing. - [ ] Added **tests** for changed code. + Our CI fails if coverage is not 100%. - [ ] 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`. - [ ] 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. - - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file. + - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). + 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). -If you have *any* questions to *any* of the points above, just **submit and ask**! This checklist is here to *help* you, not to deter you from contributing! +If you have *any* questions to *any* of the points above, just **submit and ask**! +This checklist is here to *help* you, not to deter you from contributing! From 7b02220bd337754e38f8c64abfe10e073fd028de Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 09:01:51 +0100 Subject: [PATCH 099/139] Serialize keys as tuples in asdict (#888) * Add tuple_keys to asdict See #646 * Add typing example * Add newsfragments * Add missing test * Switch it on by default * Let's not make buggy behavior configurable --- changelog.d/646.change.rst | 1 + changelog.d/888.change.rst | 1 + src/attr/__init__.pyi | 1 + src/attr/_funcs.py | 81 ++++++++++++++++++++++++-------------- tests/test_funcs.py | 33 +++++++++++++++- tests/typing_example.py | 4 ++ 6 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 changelog.d/646.change.rst create mode 100644 changelog.d/888.change.rst diff --git a/changelog.d/646.change.rst b/changelog.d/646.change.rst new file mode 100644 index 000000000..aa3e3893d --- /dev/null +++ b/changelog.d/646.change.rst @@ -0,0 +1 @@ +``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. diff --git a/changelog.d/888.change.rst b/changelog.d/888.change.rst new file mode 100644 index 000000000..aa3e3893d --- /dev/null +++ b/changelog.d/888.change.rst @@ -0,0 +1 @@ +``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index a2b23dcc6..2af76b7a8 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -456,6 +456,7 @@ def asdict( value_serializer: Optional[ Callable[[type, Attribute[Any], Any], Any] ] = ..., + tuple_keys: Optional[bool] = ..., ) -> Dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 73271c5d5..6ea2de0a0 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -46,6 +46,8 @@ def asdict( .. versionadded:: 16.0.0 *dict_factory* .. versionadded:: 16.1.0 *retain_collection_types* .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 If a dict has a collection for a key, it is + serialized as a tuple. """ attrs = fields(inst.__class__) rv = dict_factory() @@ -61,11 +63,11 @@ def asdict( if has(v.__class__): rv[a.name] = asdict( v, - True, - filter, - dict_factory, - retain_collection_types, - value_serializer, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ) elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain_collection_types is True else list @@ -73,10 +75,11 @@ def asdict( [ _asdict_anything( i, - filter, - dict_factory, - retain_collection_types, - value_serializer, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ) for i in v ] @@ -87,17 +90,19 @@ def asdict( ( _asdict_anything( kk, - filter, - df, - retain_collection_types, - value_serializer, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ), _asdict_anything( vv, - filter, - df, - retain_collection_types, - value_serializer, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ), ) for kk, vv in iteritems(v) @@ -111,6 +116,7 @@ def asdict( def _asdict_anything( val, + is_key, filter, dict_factory, retain_collection_types, @@ -123,22 +129,29 @@ def _asdict_anything( # Attrs class. rv = asdict( val, - True, - filter, - dict_factory, - retain_collection_types, - value_serializer, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ) elif isinstance(val, (tuple, list, set, frozenset)): - cf = val.__class__ if retain_collection_types is True else list + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + rv = cf( [ _asdict_anything( i, - filter, - dict_factory, - retain_collection_types, - value_serializer, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ) for i in val ] @@ -148,10 +161,20 @@ def _asdict_anything( rv = df( ( _asdict_anything( - kk, filter, df, retain_collection_types, value_serializer + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ), _asdict_anything( - vv, filter, df, retain_collection_types, value_serializer + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, ), ) for kk, vv in iteritems(val) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index e957d0e38..79dfdffe2 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -26,7 +26,7 @@ @pytest.fixture(scope="session", name="C") -def fixture_C(): +def _C(): """ Return a simple but fully featured attrs class with an x and a y attribute. """ @@ -199,6 +199,37 @@ def test_asdict_preserve_order(self, cls): assert [a.name for a in fields(cls)] == list(dict_instance.keys()) + def test_retain_keys_are_tuples(self): + """ + retain_collect_types also retains keys. + """ + + @attr.s + class A(object): + a = attr.ib() + + instance = A({(1,): 1}) + + assert {"a": {(1,): 1}} == attr.asdict( + instance, retain_collection_types=True + ) + + def test_tuple_keys(self): + """ + If a key is collection type, retain_collection_types is False, + the key is serialized as a tuple. + + See #646 + """ + + @attr.s + class A(object): + a = attr.ib() + + instance = A({(1,): 1}) + + assert {"a": {(1,): 1}} == attr.asdict(instance) + class TestAsTuple(object): """ diff --git a/tests/typing_example.py b/tests/typing_example.py index 75b41b89d..3fced27ee 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -293,3 +293,7 @@ class FactoryTest: class MatchArgs: a: int = attr.ib() b: int = attr.ib() + + +attr.asdict(FactoryTest()) +attr.asdict(FactoryTest(), retain_collection_types=False) From d6f5ae957a264d865854b9d70248d653936608ae Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 16 Dec 2021 09:25:22 +0100 Subject: [PATCH 100/139] HTML-comment-out boilerplate --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 565323f62..d25e6ccfe 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,18 @@ # Summary -Please tell us what your pull request is about here. + # Pull Request Check List + - [ ] Added **tests** for changed code. Our CI fails if coverage is not 100%. @@ -25,5 +27,7 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele - [ ] 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). + From cb6627d5f818f8dadb84e6e0ca5231bc2b196d7e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Dec 2021 07:38:35 +0100 Subject: [PATCH 101/139] 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 102/139] 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 103/139] 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 104/139] 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 105/139] 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 106/139] 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 107/139] 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 108/139] 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 109/139] 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 110/139] 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 111/139] 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 112/139] 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 113/139] 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 114/139] 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 115/139] 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 116/139] 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 117/139] 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 118/139] 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 119/139] 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 120/139] 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 121/139] 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 122/139] 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 123/139] 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 124/139] 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 125/139] 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 126/139] 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 127/139] 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 128/139] 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 129/139] 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 130/139] 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 131/139] 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 132/139] 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 133/139] 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 134/139] 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 135/139] 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 136/139] 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 137/139] 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 138/139] 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 139/139] 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"