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 @@
-
-
-
+
+
+
.. 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 @@
-
+
-
-
-
-
-
+
+
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 @@
+
+
+
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 @@
-
+
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