From a18b3957d55c669de56799a9a72ea31febe93ce4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 24 Jun 2022 11:37:40 +0200 Subject: [PATCH 01/38] Add support for Python 3.11 (#969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Python 3.11 Co-authored-by: Tin Tvrtković * Tin/py311 (#973) * Fix test_slots::TestPickle::test_no_getstate_setstate_for_dict_classes * Fix annotations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tweak tests for 3.10 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tweak tests some more Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Tin Tvrtković Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- setup.py | 1 + src/attr/_compat.py | 28 +++++++++++++--------------- tests/test_annotations.py | 13 +++++++++---- tests/test_make.py | 8 ++++++-- tests/test_slots.py | 20 ++++++++++++-------- tox.ini | 3 ++- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f62e0a605..a4755b1c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] + python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta - 3.11", "pypy-3.7", "pypy-3.8"] steps: - uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index 59ebc6080..f96894440 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 435e447de..582649325 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -111,12 +111,10 @@ def force_x_to_be_a_cell(): # pragma: no cover # Convert this code object to a code object that sets the # function's first _freevar_ (not cellvar) to the argument. if sys.version_info >= (3, 8): - # CPython 3.8+ has an incompatible CodeType signature - # (added a posonlyargcount argument) but also added - # CodeType.replace() to do this without counting parameters. - set_first_freevar_code = co.replace( - co_cellvars=co.co_freevars, co_freevars=co.co_cellvars - ) + + def set_closure_cell(cell, value): + cell.cell_contents = value + else: args = [co.co_argcount] args.append(co.co_kwonlyargcount) @@ -140,15 +138,15 @@ def force_x_to_be_a_cell(): # pragma: no cover ) set_first_freevar_code = types.CodeType(*args) - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) + def set_closure_cell(cell, value): + # Create a function using the set_first_freevar_code, + # whose first closure cell is `cell`. Calling it will + # change the value of that cell. + setter = types.FunctionType( + set_first_freevar_code, {}, "setter", (), (cell,) + ) + # And call it to set the cell. + setter(value) # Make sure it works on this interpreter: def make_func_with_cell(): diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 49c9b0d7b..049f5e992 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -94,6 +94,10 @@ class C: assert 1 == len(attr.fields(C)) assert_init_annotations(C, x=typing.List[int]) + @pytest.mark.skipif( + sys.version_info[:2] < (3, 11), + reason="Incompatible behavior on older Pythons", + ) @pytest.mark.parametrize("slots", [True, False]) def test_auto_attribs(self, slots): """ @@ -149,7 +153,7 @@ class C: x=typing.List[int], y=int, z=int, - foo=typing.Optional[typing.Any], + foo=typing.Any, ) @pytest.mark.parametrize("slots", [True, False]) @@ -384,8 +388,9 @@ def noop(): assert attr.converters.optional(noop).__annotations__ == {} - @pytest.mark.xfail( - sys.version_info[:2] == (3, 6), reason="Does not work on 3.6." + @pytest.mark.skipif( + sys.version_info[:2] < (3, 11), + reason="Incompatible behavior on older Pythons", ) @pytest.mark.parametrize("slots", [True, False]) def test_annotations_strings(self, slots): @@ -417,7 +422,7 @@ class C: x=typing.List[int], y=int, z=int, - foo=typing.Optional[typing.Any], + foo=typing.Any, ) @pytest.mark.parametrize("slots", [True, False]) diff --git a/tests/test_make.py b/tests/test_make.py index d4d8640cd..96e07f333 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -2275,7 +2275,9 @@ class C: def __getstate__(self): return ("hi",) - assert None is getattr(C(), "__setstate__", None) + assert getattr(object, "__setstate__", None) is getattr( + C, "__setstate__", None + ) @attr.s(slots=slots, auto_detect=True) class C: @@ -2291,7 +2293,9 @@ def __setstate__(self, state): i.__setstate__(()) assert True is i.called - assert None is getattr(C(), "__getstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C, "__getstate__", None + ) @pytest.mark.skipif(PY310, reason="Pre-3.10 only.") def test_match_args_pre_310(self): diff --git a/tests/test_slots.py b/tests/test_slots.py index 5f08d4cd1..de4e90e0b 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -660,10 +660,12 @@ def test_no_getstate_setstate_for_dict_classes(self): As long as getstate_setstate is None, nothing is done to dict classes. """ - i = C1(1, 2) - - assert None is getattr(i, "__getstate__", None) - assert None is getattr(i, "__setstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C1, "__getstate__", None + ) + assert getattr(object, "__setstate__", None) is getattr( + C1, "__setstate__", None + ) def test_no_getstate_setstate_if_option_false(self): """ @@ -674,10 +676,12 @@ def test_no_getstate_setstate_if_option_false(self): class C: x = attr.ib() - i = C(42) - - assert None is getattr(i, "__getstate__", None) - assert None is getattr(i, "__setstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C, "__getstate__", None + ) + assert getattr(object, "__setstate__", None) is getattr( + C, "__setstate__", None + ) @pytest.mark.parametrize("cls", [C2(1), C2Slots(1)]) def test_getstate_set_state_force_true(self, cls): diff --git a/tox.ini b/tox.ini index 380a461d0..f93fa449a 100644 --- a/tox.ini +++ b/tox.ini @@ -16,11 +16,12 @@ python = 3.8: py38, changelog 3.9: py39, pyright 3.10: py310, manifest, typing, docs + 3.11: py311 pypy-3: pypy3 [tox] -envlist = typing,pre-commit,py35,py36,py37,py38,py39,py310,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report +envlist = typing,pre-commit,py35,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report isolated_build = True From e3f231090ab36355f9d28c5d2c98fe62efe1f35b Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 3 Jul 2022 14:52:29 +1000 Subject: [PATCH 02/38] docs: fix simple typo, coverter -> converter (#977) --- tests/test_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 049f5e992..18f0d21cf 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -255,7 +255,7 @@ class A: def test_nullary_converter(self): """ - A coverter with no arguments doesn't cause a crash. + A converter with no arguments doesn't cause a crash. """ def noop(): From 6785434ed81085bb1a04284d86dd299623a3e0f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 07:56:36 +0200 Subject: [PATCH 03/38] [pre-commit.ci] pre-commit autoupdate (#978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) - [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0) - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9dd5e8e00..5aba8a281 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,12 +4,12 @@ ci: repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py3-plus, --keep-percent-format] @@ -37,7 +37,7 @@ repos: language_version: python3.10 # needed for match - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 7091b1f89ac2436e8ba96f85a799b66c38a3c9f4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 7 Jul 2022 15:04:05 +0200 Subject: [PATCH 04/38] Update init.rst (#979) --- docs/init.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/init.rst b/docs/init.rst index 105fd1c93..487dbf2b2 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -372,8 +372,6 @@ Here's an example of a manual default value: .. doctest:: - >>> from typing import Optional - >>> @define ... class C: ... x: int From b0450650c16203be5692ffeece6109a9beb2cdcd Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 11 Jul 2022 16:56:16 +0200 Subject: [PATCH 05/38] Update SECURITY.md --- .github/SECURITY.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 5e565ec19..e34c45d47 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,2 +1,12 @@ +# Security Policy + +## Supported Versions + +We are following [CalVer](https://calver.org) with generous backwards-compatibility guarantees. +Therefore we only support the latest version. + + +## Reporting a Vulnerability + To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. From 17e1ba54c822ed61d609a19cd36e03037b0b1db4 Mon Sep 17 00:00:00 2001 From: Kevin M Granger Date: Fri, 15 Jul 2022 04:10:26 -0400 Subject: [PATCH 06/38] Fix typo in tox wiki link in CONTRIBUTING.md (#982) --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e081f9fcd..a4b10b9df 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -225,6 +225,6 @@ If you'd like to join, just get a pull request merged and ask to be added in the [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/ +[*tox*]: 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 From 36ed02046e864b0f530362bc0f48f5209434868f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 16 Jul 2022 18:29:23 +0100 Subject: [PATCH 07/38] Reorder docs, remove search link Search link makes no sense with our theme. Fixes #984 --- docs/index.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index de82a2d55..8abdf6462 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,8 @@ Day-to-Day Usage :start-after: -getting-help- :end-before: -project-information- +.. include:: ../README.rst + :start-after: -project-information- ---- @@ -81,10 +83,6 @@ Full Table of Contents names glossary - -.. include:: ../README.rst - :start-after: -project-information- - .. toctree:: :maxdepth: 1 @@ -92,8 +90,4 @@ Full Table of Contents changelog -Indices and tables -================== - -* `genindex` -* `search` +`Full Index ` From c98b6cb24f368c3fda5f08e78099d00adde3d8f8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:01:15 +0200 Subject: [PATCH 08/38] pre-commit autoupdate --- .pre-commit-config.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5aba8a281..439cfd55c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,9 @@ ci: autoupdate_schedule: monthly +default_language_version: + python: python3.10 # needed for match + repos: - repo: https://github.com/psf/black rev: 22.6.0 @@ -9,7 +12,7 @@ repos: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py3-plus, --keep-percent-format] @@ -21,7 +24,6 @@ repos: - id: isort additional_dependencies: [toml] files: \.py$ - language_version: python3.10 # needed for match - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 @@ -34,7 +36,7 @@ repos: hooks: - id: interrogate args: [tests] - language_version: python3.10 # needed for match + language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 @@ -42,6 +44,5 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - language_version: python3.10 # needed for match - id: check-toml - id: check-yaml From 0010bb55fe17f224b683398d0d7cd81a60fcfa1e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:11:24 +0200 Subject: [PATCH 09/38] Make project information easiert to read/scan --- README.rst | 28 ++++++++-------------------- docs/index.rst | 4 ---- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index b928cc000..b2b71a1ca 100644 --- a/README.rst +++ b/README.rst @@ -100,31 +100,19 @@ For instance it allows you to define `special handling of NumPy arrays for equal For more details, please refer to our `comparison page `_. - -.. -getting-help- - -Getting 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! - - .. -project-information- Project Information =================== -``attrs`` is released under the `MIT `_ license, -its documentation lives at `Read the Docs `_, -the code on `GitHub `_, -and the latest release on `PyPI `_. -It’s rigorously tested on Python 3.5+ and PyPy. -The last version with Python 2.7 support is `21.4.0 `_. - -We collect information on **third-party extensions** in our `wiki `_. -Feel free to browse and add your own! +- **License**: `MIT `_ +- **PyPI**: https://pypi.org/project/attrs/ +- **Source Code**: https://github.com/python-attrs/attrs +- **Documentation**: https://www.attrs.org/ +- **Changelog**: https://www.attrs.org/en/stable/changelog.html +- **Get Help**: please use the ``python-attrs`` tag on `StackOverflow `_ +- **Third-party Extensions**: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs +- **Supported Python Versions**: 3.5 and later (last 2.7-compatible release was `21.4.0 `_) If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! diff --git a/docs/index.rst b/docs/index.rst index 8abdf6462..e998ed9dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,10 +54,6 @@ Day-to-Day Usage `extending` will show you the affordances it offers and how to make it a building block of your own projects. -.. include:: ../README.rst - :start-after: -getting-help- - :end-before: -project-information- - .. include:: ../README.rst :start-after: -project-information- From 4e6ec65460db643471eae1c51fb9b5ed4ab7c8de Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:14:35 +0200 Subject: [PATCH 10/38] Polish changelog entries --- changelog.d/898.change.rst | 2 +- changelog.d/909.change.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/898.change.rst b/changelog.d/898.change.rst index bbd2d8bee..2b6c6d252 100644 --- a/changelog.d/898.change.rst +++ b/changelog.d/898.change.rst @@ -1 +1 @@ -Speedup instantiation of frozen slotted classes. +Instantiation of frozen slotted classes is now faster. diff --git a/changelog.d/909.change.rst b/changelog.d/909.change.rst index 359d1207b..baf9fe292 100644 --- a/changelog.d/909.change.rst +++ b/changelog.d/909.change.rst @@ -1 +1 @@ -If an eq key is defined, it is also used before hashing the attribute. +If an ``eq`` key is defined, it is also used before hashing the attribute. From 557e3fd8eacb0a4f5f85a19ea32d6d54497957e1 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:30:18 +0200 Subject: [PATCH 11/38] Fix test_yaml Looks like mypy added support for converter lambdas. --- tests/test_mypy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index fd09ca7e7..5dd03421a 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -827,9 +827,9 @@ ... @attr.s class C: - 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 + x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions, types and lambdas are currently supported + y: str = attr.ib(converter=lambda x: x) + z: str = attr.ib(converter=factory(8)) # E: Unsupported converter, only named functions, types and lambdas are currently supported reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> main.C" - case: testAttrsUsingConverterAndSubclass From f7b342609f7e1f68af099409c9640a8a0d276755 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:32:04 +0200 Subject: [PATCH 12/38] It still is! --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b2b71a1ca..95b8ed025 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,7 @@ Project Information - **Changelog**: https://www.attrs.org/en/stable/changelog.html - **Get Help**: please use the ``python-attrs`` tag on `StackOverflow `_ - **Third-party Extensions**: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs -- **Supported Python Versions**: 3.5 and later (last 2.7-compatible release was `21.4.0 `_) +- **Supported Python Versions**: 3.5 and later (last 2.7-compatible release is `21.4.0 `_) If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! From eefb7c8a049282c27def770865da943a17415c98 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:35:21 +0200 Subject: [PATCH 13/38] Fix broken links --- AUTHORS.rst | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index f14ef6c60..aa677e81d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -8,4 +8,4 @@ The development is kindly supported by `Variomedia AG `_. It’s the spiritual successor of `characteristic `_ and aspires to fix some of it clunkiness and unfortunate decisions. -Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? +Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? diff --git a/README.rst b/README.rst index 95b8ed025..bf936948a 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 bcef030781f6635a4f843e5f7397bd3128afc9e4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 27 Jul 2022 16:37:26 +0200 Subject: [PATCH 14/38] Add newsfragment for #969 --- changelog.d/969.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/969.change.rst diff --git a/changelog.d/969.change.rst b/changelog.d/969.change.rst new file mode 100644 index 000000000..a991347aa --- /dev/null +++ b/changelog.d/969.change.rst @@ -0,0 +1 @@ +Python 3.11 is now officially supported. From 4f6b894487b644e5b57870fce93d337eaa2ad8ec Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 06:12:27 +0200 Subject: [PATCH 15/38] Fix overview --- docs/overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview.rst b/docs/overview.rst index 2d7302c74..7df1f2476 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -6,7 +6,7 @@ In order to fulfill its ambitious goal of bringing back the joy to writing class .. include:: ../README.rst :start-after: -code-begin- - :end-before: -getting-help- + :end-before: -project-information- .. _philosophy: From a67c84f51e4e3df875961d287583abaef673eb48 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 06:38:19 +0200 Subject: [PATCH 16/38] Add more prominent callout about slots to API docs Fixes #971 --- src/attr/_next_gen.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 87bcef830..5a06a7438 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -49,12 +49,17 @@ def define( Differences to the classic `attr.s` that it uses underneath: - - Automatically detect whether or not *auto_attribs* should be `True` - (c.f. *auto_attribs* parameter). + - Automatically detect whether or not *auto_attribs* should be `True` (c.f. + *auto_attribs* parameter). - If *frozen* is `False`, run converters and validators when setting an attribute by default. - - *slots=True* (see :term:`slotted classes` for potentially surprising - behaviors) + - *slots=True* + + .. caution:: + + Usually this has only upsides and few visible effects in everyday + programming. But it *can* lead to some suprising behaviors, so please + make sure to read :term:`slotted classes`. - *auto_exc=True* - *auto_detect=True* - *order=False* From 1590917bf7c49e35bb429d117650f660aa45e0e4 Mon Sep 17 00:00:00 2001 From: Lan5880 <67932379+Lan5880@users.noreply.github.com> Date: Wed, 27 Jul 2022 22:45:43 -0700 Subject: [PATCH 17/38] Update readme.rst (#986) Fixed grammatical error for "After declaring you attributes attrs gives you" by adding a comma. Co-authored-by: Hynek Schlawack --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bf936948a..d10bd3ca4 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ For that, it gives you a class decorator and a way to declaratively define the a C(a='foo', b='bar') -After *declaring* your attributes ``attrs`` gives you: +After *declaring* your attributes, ``attrs`` gives you: - a concise and explicit overview of the class's attributes, - a nice human-readable ``__repr__``, From 5d84d9a5686e8210b1616447ac05021b6b211157 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 07:55:37 +0200 Subject: [PATCH 18/38] Move mypy config to pyproject.toml --- mypy.ini | 3 --- pyproject.toml | 5 +++++ setup.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 685c02599..000000000 --- a/mypy.ini +++ /dev/null @@ -1,3 +0,0 @@ -[mypy] -disallow_untyped_defs = True -check_untyped_defs = True diff --git a/pyproject.toml b/pyproject.toml index a0a3d30be..d100c75ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,3 +66,8 @@ profile = "attrs" directory = "change" name = "Changes" showcontent = true + + +[tool.mypy] +disallow_untyped_defs = true +check_untyped_defs = true diff --git a/setup.py b/setup.py index f96894440..392bc04de 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ and platform.python_implementation() != "PyPy" ): EXTRAS_REQUIRE["tests_no_zope"].extend( - ["mypy!=0.940", "pytest-mypy-plugins"] + ["mypy>=0.900,!=0.940", "pytest-mypy-plugins"] ) EXTRAS_REQUIRE["tests"] = EXTRAS_REQUIRE["tests_no_zope"] + ["zope.interface"] From 65c06831c1eee4d9895511c7db5caffba9d93c0e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 08:47:28 +0200 Subject: [PATCH 19/38] Use NG APIs in glossary --- docs/glossary.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index b252bc24d..c270a8cab 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -16,11 +16,11 @@ Glossary 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. - This is the type of class you get by default both with and without ``attrs`` (except with the next APIs `attr.define`, `attr.mutable`, and `attr.frozen`). + This is the type of class you get by default both with and without ``attrs`` (except with the next APIs `attrs.define()`, `attrs.mutable()`, and `attrs.frozen()`). slotted classes A class whose instances have no `object.__dict__` attribute and `define `_ their attributes in a `object.__slots__` attribute instead. - In ``attrs``, they are created by passing ``slots=True`` to ``@attr.s`` (and are on by default in `attr.define`/`attr.mutable`/`attr.frozen`). + In ``attrs``, they are created by passing ``slots=True`` to ``@attr.s`` (and are on by default in `attrs.define()`/`attrs.mutable()`/`attrs.frozen()`). Their main advantage is that they use less memory on CPython [#pypy]_ and are slightly faster. From 696fd786901a6b82b828dd71e3fd64bfae1014ca Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 09:23:44 +0200 Subject: [PATCH 20/38] Hyphenate compound adjectives --- CHANGELOG.rst | 2 +- README.rst | 2 +- docs/api.rst | 6 +++--- docs/names.rst | 2 +- tests/test_next_gen.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d519dadb..852c62d41 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -940,7 +940,7 @@ Deprecations: This will remove the confusing error message if you write your own ``__init__`` and forget to initialize some attribute. Instead you will get a straightforward ``AttributeError``. In other words: decorated classes will work more like plain Python classes which was always ``attrs``'s goal. -- The serious business aliases ``attr.attributes`` and ``attr.attr`` have been deprecated in favor of ``attr.attrs`` and ``attr.attrib`` which are much more consistent and frankly obvious in hindsight. +- The serious-business aliases ``attr.attributes`` and ``attr.attr`` have been deprecated in favor of ``attr.attrs`` and ``attr.attrib`` which are much more consistent and frankly obvious in hindsight. They will be purged from documentation immediately but there are no plans to actually remove them. diff --git a/README.rst b/README.rst index d10bd3ca4..9f197ea7a 100644 --- a/README.rst +++ b/README.rst @@ -86,7 +86,7 @@ Simply assign ``attrs.field()`` to the attributes instead of annotating them wit ---- This example uses ``attrs``'s modern APIs that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. -The classic APIs (``@attr.s``, ``attr.ib``, plus their serious business aliases) and the ``attr`` package import name will remain **indefinitely**. +The classic APIs (``@attr.s``, ``attr.ib``, plus their serious-business aliases) and the ``attr`` package import name will remain **indefinitely**. Please check out `On The Core API Names `_ for a more in-depth explanation. diff --git a/docs/api.rst b/docs/api.rst index 37392ee49..a273d19c2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -124,7 +124,7 @@ Classic .. note:: - ``attrs`` also comes with a serious business alias ``attr.attrs``. + ``attrs`` also comes with a serious-business alias ``attr.attrs``. For example: @@ -164,7 +164,7 @@ Classic .. note:: - ``attrs`` also comes with a serious business alias ``attr.attrib``. + ``attrs`` also comes with a serious-business alias ``attr.attrib``. The object returned by `attr.ib` also allows for setting the default and the validator using decorators: @@ -842,7 +842,7 @@ It behaves similarly to `sys.version_info` and is an instance of `VersionInfo`: ---- -The serious business aliases used to be called ``attr.attributes`` and ``attr.attr``. +The serious-business aliases used to be called ``attr.attributes`` and ``attr.attr``. There are no plans to remove them but they shouldn't be used in new code. .. autofunction:: assoc diff --git a/docs/names.rst b/docs/names.rst index 5aa00afa7..8fb59c306 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -23,7 +23,7 @@ They are only available in Python 3.6 and later. Sometimes they're referred to as *next-generation* or *NG* APIs. As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. -The traditional APIs `attr.s` / `attr.ib`, their serious business aliases ``attr.attrs`` / ``attr.attrib``, and the never-documented, but popular ``attr.dataclass`` easter egg will stay **forever**. +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. diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 8395f9c02..1f13de0aa 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT """ -Python 3-only integration tests for provisional next generation APIs. +Python 3-only integration tests for provisional next-generation APIs. """ import re From 899497f165b3c8291b66c9c3a01b64881b69cf39 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 09:33:00 +0200 Subject: [PATCH 21/38] Clarify (c) ownership --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7ae3df930..2bd6453d2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Hynek Schlawack +Copyright (c) 2015 Hynek Schlawack and the attrs contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From a3d7f20e54ba29d5ec73563c167d877351228cf2 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 09:46:31 +0200 Subject: [PATCH 22/38] Polish contributing guide --- .github/CONTRIBUTING.md | 126 ++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a4b10b9df..a9cc8662e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,7 +34,67 @@ The official tag is `python-attrs` and helping out in support frees us up to imp If you have problems to test something, open anyway and ask for advice. In some situations, we may agree to add an `# pragma: no cover`. - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. -- Don’t break backwards compatibility. +- Don’t break backwards-compatibility. + + +## 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 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://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: + +```console +$ git clone git@github.com:python-attrs/attrs.git +``` + +or if you prefer 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 +$ python -m pip install --upgrade pip setuptools # PLEASE don't skip this step +$ python -m 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, your probably need to run `pip install -e '.[dev]'` again. ## Code @@ -54,7 +114,7 @@ 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. + 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) above), 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! @@ -81,7 +141,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp ## Documentation -- Use [semantic newlines] in [*reStructuredText*] files (files ending in `.rst`): +- Use [semantic newlines] in [*reStructuredText*] and [*Markdown*](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) files (files ending in `.rst` and `.md`): ```rst This is a sentence. @@ -152,66 +212,6 @@ or: ``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 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://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: - -```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, your probably need to run `pip install -e '.[dev]'` again. - - ## 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. From 9f118b7dd5328ee5abd7f3880971ab9554047c3a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 11:00:37 +0200 Subject: [PATCH 23/38] Tune first steps --- docs/index.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e998ed9dd..b637e2bee 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,15 +22,13 @@ The recommended installation method is `pip `_-i $ python -m pip install attrs -The next three steps should bring you up and running in no time: +The next steps will get 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 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! +- `why` gives you a rundown of potential alternatives and why we think ``attrs`` is still worthwhile -- depending on *your* needs even superior. - If at any point you get confused by some terminology, please check out our `glossary`. @@ -52,6 +50,7 @@ Day-to-Day Usage - Once you're comfortable with the concepts, our `api` contains all information you need to use ``attrs`` to its fullest. - ``attrs`` is built for extension from the ground up. `extending` will show you the affordances it offers and how to make it a building block of your own projects. +- Finally, 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. .. include:: ../README.rst From c89abcd6e6a826b00898f93e851c96b78c80891f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 14:46:53 +0200 Subject: [PATCH 24/38] It totally is correct --- tests/test_mypy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 5dd03421a..6759dc1a8 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -418,7 +418,6 @@ a: int 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" From b3dfebe2e10b44437c4f97d788fb5220d790efd0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 15:03:02 +0200 Subject: [PATCH 25/38] Prepare 22.1.0 --- CHANGELOG.rst | 45 +++++++++++++++++++++++++++++++----- changelog.d/898.change.rst | 1 - changelog.d/909.change.rst | 1 - changelog.d/916.change.rst | 1 - changelog.d/925.change.rst | 1 - changelog.d/931.change.rst | 1 - changelog.d/936.breaking.rst | 6 ----- changelog.d/939.breaking.rst | 2 -- changelog.d/949.change.rst | 1 - changelog.d/951.change.rst | 1 - changelog.d/969.change.rst | 1 - src/attr/__init__.py | 2 +- 12 files changed, 40 insertions(+), 23 deletions(-) delete mode 100644 changelog.d/898.change.rst delete mode 100644 changelog.d/909.change.rst delete mode 100644 changelog.d/916.change.rst delete mode 100644 changelog.d/925.change.rst delete mode 100644 changelog.d/931.change.rst delete mode 100644 changelog.d/936.breaking.rst delete mode 100644 changelog.d/939.breaking.rst delete mode 100644 changelog.d/949.change.rst delete mode 100644 changelog.d/951.change.rst delete mode 100644 changelog.d/969.change.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 852c62d41..61ca55997 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,16 +17,49 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. -Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. +.. towncrier release notes start -.. - Do *NOT* add changelog entries here! +22.1.0 (2022-07-28) +------------------- - This changelog is managed by towncrier and is compiled at release time. +Backwards-incompatible Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. +- Python 2.7 is not supported anymore. + + Dealing with Python 2.7 tooling has become too difficult for a volunteer-run project. + + We have supported Python 2 more than 2 years after it was officially discontinued and feel that we have paid our dues. + All version up to 21.4.0 from December 2021 remain fully functional, of course. + `#936 `_ +- The deprecated ``cmp`` attribute of ``attrs.Attribute`` has been removed. + This does not affect the *cmp* argument to ``attr.s`` that can be used as a shortcut to set *eq* and *order* at the same time. + `#939 `_ + + +Changes +^^^^^^^ + +- Instantiation of frozen slotted classes is now faster. + `#898 `_ +- If an ``eq`` key is defined, it is also used before hashing the attribute. + `#909 `_ +- Added ``attrs.validators.min_len()``. + `#916 `_ +- ``attrs.validators.deep_iterable()``'s *member_validator* argument now also accepts a list of validators and wraps them in an ``attrs.validators.and_()``. + `#925 `_ +- Added missing type stub re-imports for ``attrs.converters`` and ``attrs.filters``. + `#931 `_ +- Added missing stub for ``attr(s).cmp_using()``. + `#949 `_ +- ``attrs.validators._in()``'s ``ValueError`` is not missing the attribute, expected options, and the value it got anymore. + `#951 `_ +- Python 3.11 is now officially supported. + `#969 `_ + + +---- -.. towncrier release notes start 21.4.0 (2021-12-29) ------------------- diff --git a/changelog.d/898.change.rst b/changelog.d/898.change.rst deleted file mode 100644 index 2b6c6d252..000000000 --- a/changelog.d/898.change.rst +++ /dev/null @@ -1 +0,0 @@ -Instantiation of frozen slotted classes is now faster. diff --git a/changelog.d/909.change.rst b/changelog.d/909.change.rst deleted file mode 100644 index baf9fe292..000000000 --- a/changelog.d/909.change.rst +++ /dev/null @@ -1 +0,0 @@ -If an ``eq`` key is defined, it is also used before hashing the attribute. diff --git a/changelog.d/916.change.rst b/changelog.d/916.change.rst deleted file mode 100644 index 4a5ccd22b..000000000 --- a/changelog.d/916.change.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``attrs.validators.min_len()``. diff --git a/changelog.d/925.change.rst b/changelog.d/925.change.rst deleted file mode 100644 index ddc795248..000000000 --- a/changelog.d/925.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attrs.validators.deep_iterable()``'s *member_validator* argument now also accepts a list of validators and wraps them in an ``attrs.validators.and_()``. diff --git a/changelog.d/931.change.rst b/changelog.d/931.change.rst deleted file mode 100644 index 73ec071d7..000000000 --- a/changelog.d/931.change.rst +++ /dev/null @@ -1 +0,0 @@ -Added missing type stub re-imports for ``attrs.converters`` and ``attrs.filters``. diff --git a/changelog.d/936.breaking.rst b/changelog.d/936.breaking.rst deleted file mode 100644 index 1a30d126f..000000000 --- a/changelog.d/936.breaking.rst +++ /dev/null @@ -1,6 +0,0 @@ -Python 2.7 is not supported anymore. - -Dealing with Python 2.7 tooling has become too difficult for a volunteer-run project. - -We have supported Python 2 more than 2 years after it was officially discontinued and feel that we have paid our dues. -All version up to 21.4.0 from December 2021 remain fully functional, of course. diff --git a/changelog.d/939.breaking.rst b/changelog.d/939.breaking.rst deleted file mode 100644 index 6782fa9cc..000000000 --- a/changelog.d/939.breaking.rst +++ /dev/null @@ -1,2 +0,0 @@ -The deprecated ``cmp`` attribute of ``attrs.Attribute`` has been removed. -This does not affect the *cmp* argument to ``attr.s`` that can be used as a shortcut to set *eq* and *order* at the same time. diff --git a/changelog.d/949.change.rst b/changelog.d/949.change.rst deleted file mode 100644 index 6e12e5355..000000000 --- a/changelog.d/949.change.rst +++ /dev/null @@ -1 +0,0 @@ -Added missing stub for ``attr(s).cmp_using()``. diff --git a/changelog.d/951.change.rst b/changelog.d/951.change.rst deleted file mode 100644 index 6ded5fe7b..000000000 --- a/changelog.d/951.change.rst +++ /dev/null @@ -1 +0,0 @@ -``attrs.validators._in()``'s ``ValueError`` is not missing the attribute, expected options, and the value it got anymore. diff --git a/changelog.d/969.change.rst b/changelog.d/969.change.rst deleted file mode 100644 index a991347aa..000000000 --- a/changelog.d/969.change.rst +++ /dev/null @@ -1 +0,0 @@ -Python 3.11 is now officially supported. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index b47591319..386305d62 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -23,7 +23,7 @@ from ._version_info import VersionInfo -__version__ = "22.1.0.dev0" +__version__ = "22.1.0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From a683bdb656e23433c5f9af94ef642a837b1faa8e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 15:22:12 +0200 Subject: [PATCH 26/38] Start new development cycle --- CHANGELOG.rst | 9 +++++++++ src/attr/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 61ca55997..c61aeb564 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,15 @@ Whenever there is a need to break compatibility, it is announced here in the cha However if you intend to build extensions on top of ``attrs`` you have to anticipate that. +Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. + +.. + Do *NOT* add changelog entries here! + + This changelog is managed by towncrier and is compiled at release time. + + See https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md#changelog for details. + .. towncrier release notes start 22.1.0 (2022-07-28) diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 386305d62..69b558600 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -23,7 +23,7 @@ from ._version_info import VersionInfo -__version__ = "22.1.0" +__version__ = "22.2.0.dev0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" From 430113c213c5213840abedbe369fe8bca8702f55 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 15:46:55 +0200 Subject: [PATCH 27/38] Add zenodo badge --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 9f197ea7a..ced233302 100644 --- a/README.rst +++ b/README.rst @@ -18,6 +18,7 @@ Downloads per month + DOI

.. teaser-begin From 0e9a101fc0414a53d72684a053818da96cbe4e6f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 28 Jul 2022 15:54:39 +0200 Subject: [PATCH 28/38] Add citation --- CITATION.cff | 9 +++++++++ MANIFEST.in | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..83718ad88 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,9 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it as below. +title: attrs +type: software +authors: + - given-names: Hynek + family-names: Schlawack + email: hs@ox.cx +doi: 10.5281/zenodo.6925130 diff --git a/MANIFEST.in b/MANIFEST.in index 3d68bf9c5..6dbe985e7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE *.rst *.toml *.yml *.yaml *.ini +include LICENSE *.rst *.toml *.yml *.yaml *.ini CITATION.cff graft .github # Stubs From c428229aca34833fc0e5db9d6fc21efca48356fd Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 29 Jul 2022 05:43:07 +0200 Subject: [PATCH 29/38] Refer to glossary explicitly so it's not in fixed font --- src/attr/_make.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 4d1afe3fc..977d93d39 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1207,8 +1207,7 @@ def attrs( match_args=True, ): r""" - A class decorator that adds `dunder - `_\ -methods according to the + A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. :param these: A dictionary of name to `attr.ib` mappings. This is @@ -1301,10 +1300,10 @@ def attrs( injected instead. This allows you to define a custom ``__init__`` method that can do pre-init work such as ``super().__init__()``, and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class ` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry `. + :param bool slots: Create a :term:`slotted class ` that's + more memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, so + we encourage you to read the :term:`glossary entry `. :param bool frozen: Make instances immutable after initialization. If someone attempts to modify a frozen instance, `attr.exceptions.FrozenInstanceError` is raised. From 95e0c423ca31123a9537405fd2de5a1ee9f01b7c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 30 Jul 2022 13:23:03 +0200 Subject: [PATCH 30/38] Update .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d054dc626..f5c97f1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,16 @@ *.pyc .cache .coverage* +.direnv +.envrc .hypothesis .mypy_cache .pytest_cache .tox +.vscode build dist docs/_build/ htmlcov pip-wheel-metadata +tmp From 06ce35459453e7c2964b0e0e25794a6af9aeb976 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 1 Aug 2022 10:07:49 +0200 Subject: [PATCH 31/38] pre-commit autoupdate, add yesqa --- .pre-commit-config.yaml | 9 ++++++--- setup.py | 2 +- tests/attr_import_star.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 439cfd55c..4f73100fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,18 +25,21 @@ repos: additional_dependencies: [toml] files: \.py$ + - repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa + - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.1 hooks: - id: flake8 - language_version: python3.10 - repo: https://github.com/econchick/interrogate rev: 1.5.0 hooks: - id: interrogate args: [tests] - language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 diff --git a/setup.py b/setup.py index 392bc04de..45a23985f 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def find_meta(meta): .. image:: https://www.attrs.org/en/stable/_static/attrs_logo.png :alt: attrs logo :align: center -""" # noqa +""" VERSION = find_meta("version") URL = find_meta("url") diff --git a/tests/attr_import_star.py b/tests/attr_import_star.py index 636545268..c2afd32d4 100644 --- a/tests/attr_import_star.py +++ b/tests/attr_import_star.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT -from attr import * # noqa: F401,F403 +from attr import * # noqa: F401, F403 # This is imported by test_import::test_from_attr_import_star; this must From 107367d437484006a7d01854e91e478401690087 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 05:51:38 +0200 Subject: [PATCH 32/38] [pre-commit.ci] pre-commit autoupdate (#990) --- .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 4f73100fb..ce0040ef7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - id: yesqa - repo: https://github.com/PyCQA/flake8 - rev: 5.0.1 + rev: 5.0.2 hooks: - id: flake8 From 08f8319efefca8dbaaeb7dc35dbd9a76d5a2400d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 5 Aug 2022 09:48:00 +0200 Subject: [PATCH 33/38] Drop Python 3.5 (#988) * Drop Python 3.5 Less than 0.5% of our downloads are 3.5 and it allows us to simplify A LOT of code. Fixes #965 Signed-off-by: Hynek Schlawack * Run 3.6 under coverage * Add newsfragment * Probably don't need 3.7 for coverage * Everything is ordered! * pre-commit autoupdate, add yesqa --- .github/workflows/main.yml | 2 +- .pre-commit-config.yaml | 2 +- README.rst | 2 +- changelog.d/988.breaking.rst | 1 + conftest.py | 12 +- docs/api.rst | 1 - docs/examples.rst | 2 +- docs/extending.rst | 2 +- docs/names.rst | 1 - setup.py | 15 +-- src/attr/__init__.py | 13 +- src/attr/_compat.py | 10 -- src/attr/_make.py | 229 +++++++++++------------------------ src/attr/_next_gen.py | 4 +- src/attr/converters.py | 2 +- src/attr/validators.py | 6 +- tests/test_annotations.py | 4 +- tests/test_funcs.py | 8 +- tests/test_functional.py | 8 +- tests/test_init_subclass.py | 2 - tests/test_make.py | 14 +-- tests/test_next_gen.py | 2 +- tests/test_pyright.py | 6 +- tox.ini | 12 +- 24 files changed, 110 insertions(+), 250 deletions(-) create mode 100644 changelog.d/988.breaking.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a4755b1c8..beaaffac5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta - 3.11", "pypy-3.7", "pypy-3.8"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta - 3.11", "pypy-3.7", "pypy-3.8"] steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce0040ef7..20c8761e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v2.37.3 hooks: - id: pyupgrade - args: [--py3-plus, --keep-percent-format] + args: [--py36-plus, --keep-percent-format] exclude: "tests/test_slots.py" - repo: https://github.com/PyCQA/isort diff --git a/README.rst b/README.rst index ced233302..9b5dcc6a6 100644 --- a/README.rst +++ b/README.rst @@ -113,7 +113,7 @@ Project Information - **Changelog**: https://www.attrs.org/en/stable/changelog.html - **Get Help**: please use the ``python-attrs`` tag on `StackOverflow `_ - **Third-party Extensions**: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs -- **Supported Python Versions**: 3.5 and later (last 2.7-compatible release is `21.4.0 `_) +- **Supported Python Versions**: 3.6 and later (last 2.7-compatible release is `21.4.0 `_) If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! diff --git a/changelog.d/988.breaking.rst b/changelog.d/988.breaking.rst new file mode 100644 index 000000000..58c1dbde3 --- /dev/null +++ b/changelog.d/988.breaking.rst @@ -0,0 +1 @@ +Python 3.5 is not supported anymore. diff --git a/conftest.py b/conftest.py index 33cc6a6cb..f6e5d8d6c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,9 +1,8 @@ # SPDX-License-Identifier: MIT - from hypothesis import HealthCheck, settings -from attr._compat import PY36, PY310 +from attr._compat import PY310 def pytest_configure(config): @@ -15,14 +14,5 @@ def pytest_configure(config): collect_ignore = [] -if not PY36: - collect_ignore.extend( - [ - "tests/test_annotations.py", - "tests/test_hooks.py", - "tests/test_init_subclass.py", - "tests/test_next_gen.py", - ] - ) if not PY310: collect_ignore.extend(["tests/test_pattern_matching.py"]) diff --git a/docs/api.rst b/docs/api.rst index a273d19c2..a609833c5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,7 +14,6 @@ As of version 21.3.0, ``attrs`` consists of **two** top-level package names: - The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` - The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). - Using this namespace requires Python 3.6 or later. The ``attrs`` namespace is built *on top of* ``attr`` which will *never* go away. diff --git a/docs/examples.rst b/docs/examples.rst index ae5ffa78e..ab9fe2d03 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -473,7 +473,7 @@ If you're the author of a third-party library with ``attrs`` integration, please Types ----- -``attrs`` also allows you to associate a type with an attribute using either the *type* argument to `attr.ib` or -- as of Python 3.6 -- using :pep:`526`-annotations: +``attrs`` also allows you to associate a type with an attribute using either the *type* argument to `attr.ib` or using :pep:`526`-annotations: .. doctest:: diff --git a/docs/extending.rst b/docs/extending.rst index d5775adcc..13d66edf3 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -124,7 +124,7 @@ Types ``attrs`` offers two ways of attaching type information to attributes: -- :pep:`526` annotations on Python 3.6 and later, +- :pep:`526` annotations, - and the *type* argument to `attr.ib`. This information is available to you: diff --git a/docs/names.rst b/docs/names.rst index 8fb59c306..6773db20c 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -19,7 +19,6 @@ We recommend our modern APIs for new code: - and `attrs.field()` to define an attribute. They have been added in ``attrs`` 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. -They are only available in Python 3.6 and later. Sometimes they're referred to as *next-generation* or *NG* APIs. As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. diff --git a/setup.py b/setup.py index 45a23985f..874fe757c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ import os import platform import re -import sys from setuptools import find_packages, setup @@ -33,7 +32,6 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -57,10 +55,7 @@ "pytest>=4.3.0", # 4.3.0 dropped last use of `convert` ], } -if ( - sys.version_info[:2] >= (3, 6) - and platform.python_implementation() != "PyPy" -): +if platform.python_implementation() != "PyPy": EXTRAS_REQUIRE["tests_no_zope"].extend( ["mypy>=0.900,!=0.940", "pytest-mypy-plugins"] ) @@ -92,11 +87,11 @@ def find_meta(meta): Extract __*meta*__ from META_FILE. """ meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M + rf"^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE, re.M ) if meta_match: return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + raise RuntimeError(f"Unable to find __{meta}__ string.") LOGO = """ @@ -119,7 +114,7 @@ def find_meta(meta): re.S, ).group(1) + "\n\n`Full changelog " - + "<{url}en/stable/changelog.html>`_.\n\n".format(url=URL) + + f"<{URL}en/stable/changelog.html>`_.\n\n" + read("AUTHORS.rst") ) @@ -141,7 +136,7 @@ def find_meta(meta): long_description_content_type="text/x-rst", packages=PACKAGES, package_dir={"": "src"}, - python_requires=">=3.5", + python_requires=">=3.6", zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 69b558600..92e8920b5 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -1,8 +1,5 @@ # SPDX-License-Identifier: MIT - -import sys - from functools import partial from . import converters, exceptions, filters, setters, validators @@ -20,6 +17,7 @@ make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo @@ -56,15 +54,19 @@ "attrs", "cmp_using", "converters", + "define", "evolve", "exceptions", + "field", "fields", "fields_dict", "filters", + "frozen", "get_run_validators", "has", "ib", "make_class", + "mutable", "resolve_types", "s", "set_run_validators", @@ -72,8 +74,3 @@ "validate", "validators", ] - -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - - __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 582649325..cc98c88b5 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -12,19 +12,9 @@ PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 PY310 = sys.version_info[:2] >= (3, 10) -if PYPY or PY36: - ordered_dict = dict -else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - def just_warn(*args, **kw): warnings.warn( "Running interpreter doesn't sufficiently support code object " diff --git a/src/attr/_make.py b/src/attr/_make.py index 977d93d39..f39258264 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -11,14 +11,7 @@ # 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, - PY310, - PYPY, - _AnnotationExtractor, - ordered_dict, - set_closure_cell, -) +from ._compat import PY310, PYPY, _AnnotationExtractor, set_closure_cell from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, @@ -201,9 +194,9 @@ def attrib( value is converted before being passed to the validator, if any. :param metadata: An arbitrary mapping, to be used by third-party components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see :pep:`526`). + + :param type: The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on ``Attribute.type``. @@ -323,7 +316,7 @@ def _make_method(name, script, filename, globs): if old_val == linecache_tuple: break else: - filename = "{}-{}>".format(base_filename[:-1], count) + filename = f"{base_filename[:-1]}-{count}>" count += 1 _compile_and_eval(script, globs, locs, filename) @@ -341,9 +334,9 @@ class MyClassAttributes(tuple): __slots__ = () x = property(itemgetter(0)) """ - attr_class_name = "{}Attributes".format(cls_name) + attr_class_name = f"{cls_name}Attributes" attr_class_template = [ - "class {}(tuple):".format(attr_class_name), + f"class {attr_class_name}(tuple):", " __slots__ = ()", ] if attr_names: @@ -418,13 +411,6 @@ def _get_annotations(cls): return {} -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - def _collect_base_attrs(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. @@ -502,9 +488,6 @@ def _transform_attrs( if these is not None: ca_list = [(name, ca) for name, ca in these.items()] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) elif auto_attribs is True: ca_names = { name @@ -735,7 +718,7 @@ def __init__( ) = self._make_getstate_setstate() def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + return f"<_ClassBuilder(cls={self._cls.__name__})>" def build_class(self): """ @@ -1218,10 +1201,7 @@ def attrs( If *these* is not ``None``, ``attrs`` will *not* search the class body for attributes and will *not* remove any attributes from it. - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. + The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` @@ -1329,7 +1309,7 @@ def attrs( :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated - attributes (Python 3.6 and later only) from the class body. + attributes from the class body. In this case, you **must** annotate every field. If ``attrs`` encounters a field that is set to an `attr.ib` but lacks a type @@ -1833,126 +1813,61 @@ def _add_eq(cls, attrs=None): return cls -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - 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 +def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + 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["_compat"] = _compat + 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)' ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - 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) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - 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 + 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) -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full 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 - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - class_name = real_cls.__qualname__.rsplit(">.", 1)[-1] - else: - class_name = ns + "." + real_cls.__name__ + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__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. - already_repring.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: - already_repring.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 __repr__ + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) def _add_repr(cls, ns=None, attrs=None): @@ -1988,9 +1903,7 @@ def fields(cls): raise TypeError("Passed object must be a class.") attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") return attrs @@ -2005,10 +1918,7 @@ def fields_dict(cls): :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` class. - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. + :rtype: dict .. versionadded:: 18.1.0 """ @@ -2016,10 +1926,8 @@ def fields_dict(cls): raise TypeError("Passed object must be a class.") attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict((a.name, a) for a in attrs) + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") + return {a.name: a for a in attrs} def validate(inst): @@ -2579,7 +2487,7 @@ def from_counting_attr(cls, name, ca, type=None): type=type, cmp=None, inherited=False, - **inst_dict + **inst_dict, ) # Don't use attr.evolve since fields(Attribute) doesn't work @@ -2865,10 +2773,9 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): :param attrs: A list of names or a dictionary of mappings of names to attributes. - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. + The order is deduced from the order of the names or attributes inside + *attrs*. Otherwise the order of the definition of the attributes is + used. :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 5a06a7438..260519f1c 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT """ -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. """ diff --git a/src/attr/converters.py b/src/attr/converters.py index a73626c26..4cada106b 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -141,4 +141,4 @@ def to_bool(val): except TypeError: # Raised when "val" is not hashable (e.g., lists) pass - raise ValueError("Cannot convert value to bool: {}".format(val)) + raise ValueError(f"Cannot convert value to bool: {val}") diff --git a/src/attr/validators.py b/src/attr/validators.py index eece517da..f27049b3d 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -391,7 +391,7 @@ def __repr__(self): iterable_identifier = ( "" if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) + else f" {self.iterable_validator!r}" ) return ( "".format(max=self.max_length) + return f"" def max_len(length): @@ -579,7 +579,7 @@ def __call__(self, inst, attr, value): ) def __repr__(self): - return "".format(min=self.min_length) + return f"" def min_len(length): diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 18f0d21cf..fb996a6a9 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -2,8 +2,6 @@ """ Tests for PEP-526 type annotations. - -Python 3.6+ only. """ import sys @@ -397,7 +395,7 @@ def test_annotations_strings(self, slots): """ String annotations are passed into __init__ as is. - It fails on 3.6 due to a bug in Python. + The strings keep changing between releases. """ import typing as t diff --git a/tests/test_funcs.py b/tests/test_funcs.py index d73d94c51..f77bfd4ab 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -15,7 +15,7 @@ import attr from attr import asdict, assoc, astuple, evolve, fields, has -from attr._compat import Mapping, Sequence, ordered_dict +from attr._compat import Mapping, Sequence from attr.exceptions import AttrsAttributeNotFoundError from attr.validators import instance_of @@ -196,7 +196,7 @@ def test_asdict_preserve_order(self, cls): Field order should be preserved when dumping to an ordered_dict. """ instance = cls() - dict_instance = asdict(instance, dict_factory=ordered_dict) + dict_instance = asdict(instance, dict_factory=dict) assert [a.name for a in fields(cls)] == list(dict_instance.keys()) @@ -483,9 +483,7 @@ def test_unknown(self, C): ) as e, pytest.deprecated_call(): assoc(C(), aaaa=2) - assert ( - "aaaa is not an attrs attribute on {cls!r}.".format(cls=C), - ) == e.value.args + assert (f"aaaa is not an attrs attribute on {C!r}.",) == e.value.args def test_frozen(self): """ diff --git a/tests/test_functional.py b/tests/test_functional.py index 09f504802..741068012 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -17,7 +17,6 @@ import attr -from attr._compat import PY36 from attr._make import NOTHING, Attribute from attr.exceptions import FrozenInstanceError @@ -224,9 +223,9 @@ def test_subclassing_with_extra_attrs(self, cls): assert i.x is i.meth() is obj assert i.y == 2 if cls is Sub: - assert "Sub(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"Sub(x={obj}, y=2)" == repr(i) else: - assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"SubSlots(x={obj}, y=2)" == repr(i) @pytest.mark.parametrize("base", [Base, BaseSlots]) def test_subclass_without_extra_attrs(self, base): @@ -241,7 +240,7 @@ class Sub2(base): obj = object() i = Sub2(x=obj) assert i.x is i.meth() is obj - assert "Sub2(x={obj})".format(obj=obj) == repr(i) + assert f"Sub2(x={obj})" == repr(i) @pytest.mark.parametrize( "frozen_class", @@ -701,7 +700,6 @@ class D(C): assert "self.y = y" in src assert object.__setattr__ == D.__setattr__ - @pytest.mark.skipif(not PY36, reason="NG APIs are 3.6+") @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_with_ng_defaults(self, slots): """ diff --git a/tests/test_init_subclass.py b/tests/test_init_subclass.py index 863e79437..c686e414e 100644 --- a/tests/test_init_subclass.py +++ b/tests/test_init_subclass.py @@ -2,8 +2,6 @@ """ Tests for `__init_subclass__` related tests. - -Python 3.6+ only. """ import pytest diff --git a/tests/test_make.py b/tests/test_make.py index 96e07f333..fe8c5e613 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -22,7 +22,7 @@ import attr from attr import _config -from attr._compat import PY310, ordered_dict +from attr._compat import PY310 from attr._make import ( Attribute, Factory, @@ -297,7 +297,7 @@ def test_these_ordered(self): b = attr.ib(default=2) a = attr.ib(default=1) - @attr.s(these=ordered_dict([("a", a), ("b", b)])) + @attr.s(these=dict([("a", a), ("b", b)])) class C: pass @@ -1071,7 +1071,7 @@ def test_make_class_ordered(self): b = attr.ib(default=2) a = attr.ib(default=1) - C = attr.make_class("C", ordered_dict([("a", a), ("b", b)])) + C = attr.make_class("C", dict([("a", a), ("b", b)])) assert "C(a=1, b=2)" == repr(C()) @@ -1114,7 +1114,7 @@ def test_handler_non_attrs_class(self): fields(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1156,7 +1156,7 @@ def test_handler_non_attrs_class(self): fields_dict(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1166,7 +1166,7 @@ def test_fields_dict(self, C): """ d = fields_dict(C) - assert isinstance(d, ordered_dict) + assert isinstance(d, dict) assert list(fields(C)) == list(d.values()) assert [a.name for a in fields(C)] == [field_name for field_name in d] @@ -1214,7 +1214,7 @@ def test_converter_factory_property(self, val, init): """ C = make_class( "C", - ordered_dict( + dict( [ ("y", attr.ib()), ( diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 1f13de0aa..78fd0e52d 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT """ -Python 3-only integration tests for provisional next-generation APIs. +Integration tests for next-generation APIs. """ import re diff --git a/tests/test_pyright.py b/tests/test_pyright.py index e055ebb8c..eddb31ae9 100644 --- a/tests/test_pyright.py +++ b/tests/test_pyright.py @@ -4,17 +4,13 @@ import os.path import shutil import subprocess -import sys import pytest import attr -if sys.version_info < (3, 6): - _found_pyright = False -else: - _found_pyright = shutil.which("pyright") +_found_pyright = shutil.which("pyright") @attr.s(frozen=True) diff --git a/tox.ini b/tox.ini index f93fa449a..1fd8d925d 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ filterwarnings = # Keep docs in sync with docs env and .readthedocs.yml. [gh-actions] python = - 3.5: py35 3.6: py36 3.7: py37 3.8: py38, changelog @@ -21,7 +20,7 @@ python = [tox] -envlist = typing,pre-commit,py35,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report +envlist = typing,pre-commit,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report isolated_build = True @@ -40,12 +39,7 @@ extras = tests commands = python -m pytest {posargs} -[testenv:py35] -extras = tests -commands = coverage run -m pytest {posargs} - - -[testenv:py37] +[testenv:py36] extras = tests commands = coverage run -m pytest {posargs} @@ -63,7 +57,7 @@ commands = coverage run -m pytest {posargs} [testenv:coverage-report] basepython = python3.10 -depends = py35,py37,py310 +depends = py36,py310 skip_install = true deps = coverage[toml]>=5.4 commands = From daa654c62ac6e31fe58dbdf1c22d82dfa27632c5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 6 Aug 2022 06:53:37 +0200 Subject: [PATCH 34/38] Expand supporters --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index aa677e81d..c9d56a777 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -3,7 +3,7 @@ Credits ``attrs`` is written and maintained by `Hynek Schlawack `_. -The development is kindly supported by `Variomedia AG `_. +The development is kindly supported by `Variomedia AG `_, the ``attrs`` `Tidelift subscribers `_, and all my amazing `GitHub Sponsors `_. A full list of contributors can be found in `GitHub's overview `_. From a2a741097c43ef5a36cc5e3c2e855a0a5c27b8b6 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 6 Aug 2022 06:55:49 +0200 Subject: [PATCH 35/38] Don't need this word --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index c9d56a777..d0ac45924 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -3,7 +3,7 @@ Credits ``attrs`` is written and maintained by `Hynek Schlawack `_. -The development is kindly supported by `Variomedia AG `_, the ``attrs`` `Tidelift subscribers `_, and all my amazing `GitHub Sponsors `_. +The development is kindly supported by `Variomedia AG `_, ``attrs`` `Tidelift subscribers `_, and all my amazing `GitHub Sponsors `_. A full list of contributors can be found in `GitHub's overview `_. From 983c2c42935ccfc2fce77d6f20dac7821bc720e8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 7 Aug 2022 02:13:02 +0200 Subject: [PATCH 36/38] Re-import AttrsInstance in attrs ns (#994) --- changelog.d/987.change.rst | 1 + src/attrs/__init__.pyi | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/987.change.rst diff --git a/changelog.d/987.change.rst b/changelog.d/987.change.rst new file mode 100644 index 000000000..3ae9699f6 --- /dev/null +++ b/changelog.d/987.change.rst @@ -0,0 +1 @@ +Added missing re-import of ``attr.AttrsInstance`` to the ``attrs`` namespace. diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi index fc44de46a..4ea64d8ea 100644 --- a/src/attrs/__init__.pyi +++ b/src/attrs/__init__.pyi @@ -23,6 +23,7 @@ from attr import __version_info__ as __version_info__ from attr import _FilterType from attr import assoc as assoc from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance from attr import cmp_using as cmp_using from attr import converters as converters from attr import define as define From a8191556c0aad84125d2c3435c5a2352b2f8a9ba Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 7 Aug 2022 09:52:28 +0200 Subject: [PATCH 37/38] Speedup `_setattr` usage and fix slight performance regressions (#991) * Speedup `_setattr` usage and fix performance regressions * Add changelog file Co-authored-by: Hynek Schlawack --- changelog.d/991.change.rst | 1 + docs/how-does-it-work.rst | 4 ++-- src/attr/_make.py | 24 +++++++++++++++++------- tests/test_functional.py | 5 +++-- 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 changelog.d/991.change.rst diff --git a/changelog.d/991.change.rst b/changelog.d/991.change.rst new file mode 100644 index 000000000..bc9487d1d --- /dev/null +++ b/changelog.d/991.change.rst @@ -0,0 +1 @@ +Fix slight performance regression in classes with custom ``__setattr__`` and speedup even more. diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index c7b408341..12528ab17 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -94,9 +94,9 @@ This is (still) slower than a plain assignment: -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \ "C(1, 2, 3)" ......................................... - Mean +- std dev: 450 ns +- 26 ns + Mean +- std dev: 425 ns +- 16 ns -So on a laptop computer the difference is about 230 nanoseconds (1 second is 1,000,000,000 nanoseconds). +So on a laptop computer the difference is about 200 nanoseconds (1 second is 1,000,000,000 nanoseconds). It's certainly something you'll feel in a hot loop but shouldn't matter in normal code. Pick what's more important to you. diff --git a/src/attr/_make.py b/src/attr/_make.py index f39258264..0c2da5edd 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -915,7 +915,7 @@ def slots_setstate(self, state): """ Automatically created by attrs. """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) + __bound_setattr = _obj_setattr.__get__(self) for name, value in zip(state_attr_names, state): __bound_setattr(name, value) @@ -2007,6 +2007,7 @@ def _make_init( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ) @@ -2019,7 +2020,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_setattr"] = _obj_setattr + globs["_cached_setattr_get"] = _obj_setattr.__get__ init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2036,7 +2037,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr(self, '%s', %s)" % (attr_name, value_var) + return "_setattr('%s', %s)" % (attr_name, value_var) def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2044,7 +2045,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr): Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """ - return "_setattr(self, '%s', %s(%s))" % ( + return "_setattr('%s', %s(%s))" % ( attr_name, _init_converter_pat % (attr_name,), value_var, @@ -2086,6 +2087,7 @@ def _attrs_to_init_script( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ): @@ -2101,6 +2103,14 @@ def _attrs_to_init_script( if pre_init: lines.append("self.__attrs_pre_init__()") + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr_get(self)" + ) + if frozen is True: if slots is True: fmt_setter = _setattr @@ -2315,7 +2325,7 @@ def fmt_setter_with_converter( if frozen: if slots: # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr(self, '%s', %s)" + init_hash_cache = "_setattr('%s', %s)" else: # if frozen and not slots, then _inst_dict defined above init_hash_cache = "_inst_dict['%s'] = %s" @@ -2428,7 +2438,7 @@ def __init__( ) # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. @@ -2525,7 +2535,7 @@ def __setstate__(self, state): self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) diff --git a/tests/test_functional.py b/tests/test_functional.py index 741068012..cdbbd5235 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -745,6 +745,7 @@ class D(C): src = inspect.getsource(D.__init__) - assert "_setattr(self, 'x', x)" in src - assert "_setattr(self, 'y', y)" in src + assert "_setattr = _cached_setattr_get(self)" in src + assert "_setattr('x', x)" in src + assert "_setattr('y', y)" in src assert object.__setattr__ != D.__setattr__ From 61516832448ea9ba9cf47281caec840a09e32a40 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 10 Aug 2022 12:41:46 +0200 Subject: [PATCH 38/38] Eliminate most str.format() and %-formatting (#995) * Eliminate most str.format() and %-formatting * Add newsfragment --- changelog.d/995.change.rst | 3 ++ docs/why.rst | 2 +- src/attr/_cmp.py | 6 +-- src/attr/_funcs.py | 4 +- src/attr/_make.py | 107 +++++++++++++++---------------------- tests/test_make.py | 4 +- 6 files changed, 53 insertions(+), 73 deletions(-) create mode 100644 changelog.d/995.change.rst diff --git a/changelog.d/995.change.rst b/changelog.d/995.change.rst new file mode 100644 index 000000000..15ddb9f27 --- /dev/null +++ b/changelog.d/995.change.rst @@ -0,0 +1,3 @@ +Class-creation performance improvements by switching performance-sensitive templating operations to f-strings. + +You can expect an improvement of about 5% even for very simple classes. diff --git a/docs/why.rst b/docs/why.rst index 9edae27a2..be57ce6da 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -218,7 +218,7 @@ is roughly ... self.b = b ... ... def __repr__(self): - ... return "ArtisanalClass(a={}, b={})".format(self.a, self.b) + ... return f"ArtisanalClass(a={self.a}, b={self.b})" ... ... def __eq__(self, other): ... if other.__class__ is self.__class__: diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py index 81b99e4c3..ad1e18c75 100644 --- a/src/attr/_cmp.py +++ b/src/attr/_cmp.py @@ -130,9 +130,9 @@ def method(self, other): return result - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." ) return method diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index a982d7cb5..49f241d02 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -331,9 +331,7 @@ def assoc(inst, **changes): a = getattr(attrs, k, NOTHING) if a is NOTHING: raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) + f"{k} is not an attrs attribute on {new.__class__}." ) _obj_setattr(new, k, v) return new diff --git a/src/attr/_make.py b/src/attr/_make.py index 0c2da5edd..730ed60cc 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -23,10 +23,7 @@ # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ _init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) +_init_factory_pat = "__attr_factory_%s" _classvar_prefixes = ( "typing.ClassVar", "t.ClassVar", @@ -342,7 +339,7 @@ class MyClassAttributes(tuple): if attr_names: for i, attr_name in enumerate(attr_names): attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) + f" {attr_name} = _attrs_property(_attrs_itemgetter({i}))" ) else: attr_class_template.append(" pass") @@ -559,7 +556,7 @@ def _transform_attrs( if had_default is True and a.default is NOTHING: raise ValueError( "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) + f"default value or factory. Attribute in question: {a!r}" ) if had_default is False and a.default is not NOTHING: @@ -1062,8 +1059,9 @@ def _add_method_dunders(self, method): pass try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, + method.__doc__ = ( + "Method generated by attrs for class " + f"{self._cls.__qualname__}." ) except AttributeError: pass @@ -1583,12 +1581,10 @@ def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), + return ( + f"" ) - return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1630,34 +1626,34 @@ def append_hash_computation_lines(prefix, indent): method_lines.extend( [ indent + prefix + hash_func, - indent + " %d," % (type_hash,), + indent + f" {type_hash},", ] ) for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" globs[cmp_name] = a.eq_key method_lines.append( - indent + " %s(self.%s)," % (cmp_name, a.name) + indent + f" {cmp_name}(self.{a.name})," ) else: - method_lines.append(indent + " self.%s," % a.name) + method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + method_lines.append(tab + f"if self.{_hash_cache_field} is None:") if frozen: append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 + f"self.{_hash_cache_field} = ", tab * 2 ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) + method_lines.append(tab + f"return self.{_hash_cache_field}") else: append_hash_computation_lines("return ", tab) @@ -1713,27 +1709,15 @@ def _make_eq(cls, attrs): others = [" ) == ("] for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) + lines.append(f" {cmp_name}(self.{a.name}),") + others.append(f" {cmp_name}(other.{a.name}),") else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) + lines.append(f" self.{a.name},") + others.append(f" other.{a.name},") lines += others + [" )"] else: @@ -1860,7 +1844,7 @@ def _make_repr(attrs, ns, cls): " else:", " already_repring.add(id(self))", " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + f" return f'{cls_name_fragment}({repr_fragment})'", " finally:", " already_repring.remove(id(self))", ] @@ -2037,7 +2021,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr('%s', %s)" % (attr_name, value_var) + return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2060,7 +2044,7 @@ def _assign(attr_name, value, has_on_setattr): if has_on_setattr: return _setattr(attr_name, value, True) - return "self.%s = %s" % (attr_name, value) + return f"self.{attr_name} = {value}" def _assign_with_converter(attr_name, value_var, has_on_setattr): @@ -2126,7 +2110,7 @@ def fmt_setter(attr_name, value_var, has_on_setattr): if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) - return "_inst_dict['%s'] = %s" % (attr_name, value_var) + return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name, value_var, has_on_setattr @@ -2174,12 +2158,12 @@ def fmt_setter_with_converter( if a.init is False: if has_factory: - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2189,7 +2173,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2199,7 +2183,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter_with_converter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) @@ -2209,12 +2193,12 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: @@ -2233,14 +2217,14 @@ def fmt_setter_with_converter( lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: - arg = "%s=NOTHING" % (arg_name,) + arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) + lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( " " @@ -2307,9 +2291,7 @@ def fmt_setter_with_converter( for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a @@ -2336,9 +2318,9 @@ def fmt_setter_with_converter( # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) - lines.append("BaseException.__init__(self, %s)" % (vals,)) + lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) if kw_only_args: @@ -2346,14 +2328,13 @@ def fmt_setter_with_converter( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) + return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", + "def %s(self, %s):\n %s\n" + % ( + ("__attrs_init__" if attrs_init else "__init__"), + args, + "\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, diff --git a/tests/test_make.py b/tests/test_make.py index fe8c5e613..a68ae1db9 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1910,9 +1910,7 @@ class A: if hasattr(A, "__qualname__"): method = getattr(A, meth_name) - expected = "Method generated by attrs for class {}.".format( - A.__qualname__ - ) + expected = f"Method generated by attrs for class {A.__qualname__}." assert expected == method.__doc__