diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e081f9fcd..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. @@ -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 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. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f62e0a605..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", "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/.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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9dd5e8e00..20c8761e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,20 @@ ci: autoupdate_schedule: monthly +default_language_version: + python: python3.10 # needed for match + 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.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 @@ -21,27 +24,28 @@ repos: - id: isort additional_dependencies: [toml] files: \.py$ - language_version: python3.10 # needed for match + + - 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.2 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 # 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 - id: debug-statements - language_version: python3.10 # needed for match - id: check-toml - id: check-yaml diff --git a/AUTHORS.rst b/AUTHORS.rst index f14ef6c60..d0ac45924 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -3,9 +3,9 @@ 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 `_, ``attrs`` `Tidelift subscribers `_, and all my amazing `GitHub Sponsors `_. A full list of contributors can be found in `GitHub's overview `_. 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/CHANGELOG.rst b/CHANGELOG.rst index 2d519dadb..c61aeb564 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,48 @@ Changes for the upcoming release can be found in the `"changelog.d" directory `_ +- 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 `_ + + +---- + + 21.4.0 (2021-12-29) ------------------- @@ -940,7 +982,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/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/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 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 diff --git a/README.rst b/README.rst index b928cc000..9b5dcc6a6 100644 --- a/README.rst +++ b/README.rst @@ -18,12 +18,13 @@ Downloads per month + DOI

.. 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. @@ -68,7 +69,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__``, @@ -86,7 +87,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. @@ -100,31 +101,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.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/898.change.rst b/changelog.d/898.change.rst deleted file mode 100644 index bbd2d8bee..000000000 --- a/changelog.d/898.change.rst +++ /dev/null @@ -1 +0,0 @@ -Speedup instantiation of frozen slotted classes. diff --git a/changelog.d/909.change.rst b/changelog.d/909.change.rst deleted file mode 100644 index 359d1207b..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/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/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/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/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/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 ecaee7e68..379facc9f 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. @@ -124,7 +123,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 +163,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 +841,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/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 74bec9e83..38b8fa048 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/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. 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/docs/index.rst b/docs/index.rst index de82a2d55..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,12 +50,11 @@ 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 - :start-after: -getting-help- - :end-before: -project-information- - + :start-after: -project-information- ---- @@ -81,10 +78,6 @@ Full Table of Contents names glossary - -.. include:: ../README.rst - :start-after: -project-information- - .. toctree:: :maxdepth: 1 @@ -92,8 +85,4 @@ Full Table of Contents changelog -Indices and tables -================== - -* `genindex` -* `search` +`Full Index ` diff --git a/docs/init.rst b/docs/init.rst index 330642dab..551702232 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -373,8 +373,6 @@ Here's an example of a manual default value: .. doctest:: - >>> from typing import Optional - >>> @define ... class C: ... x: int diff --git a/docs/names.rst b/docs/names.rst index 5aa00afa7..6773db20c 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -19,11 +19,10 @@ 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. -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/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: 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/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 59ebc6080..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,12 +32,12 @@ "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", "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", @@ -56,12 +55,9 @@ "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.940", "pytest-mypy-plugins"] + ["mypy>=0.900,!=0.940", "pytest-mypy-plugins"] ) EXTRAS_REQUIRE["tests"] = EXTRAS_REQUIRE["tests_no_zope"] + ["zope.interface"] @@ -91,18 +87,18 @@ 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 = """ .. 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") @@ -118,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") ) @@ -140,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 b47591319..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,10 +17,11 @@ make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo -__version__ = "22.1.0.dev0" +__version__ = "22.2.0.dev0" __version_info__ = VersionInfo._from_version_string(__version__) __title__ = "attrs" @@ -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/_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/_compat.py b/src/attr/_compat.py index 435e447de..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 " @@ -111,12 +101,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 +128,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/src/attr/_funcs.py b/src/attr/_funcs.py index 9b0a5b232..1f573c110 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 cdf8db810..d4c2dfc69 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, @@ -30,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", @@ -202,9 +192,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``. @@ -329,7 +319,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) @@ -347,15 +337,15 @@ 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: 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") @@ -424,13 +414,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*. @@ -508,9 +491,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 @@ -582,7 +562,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: @@ -749,7 +729,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): """ @@ -946,7 +926,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) @@ -1093,8 +1073,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 @@ -1221,8 +1202,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 @@ -1233,10 +1213,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` @@ -1315,10 +1292,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. @@ -1344,7 +1321,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 @@ -1618,12 +1595,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): @@ -1665,34 +1640,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) @@ -1748,27 +1723,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: @@ -1848,126 +1811,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 - ) - 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 +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)' ) - -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 + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) - def __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:", + f" return f'{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): @@ -2003,9 +1901,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 @@ -2020,10 +1916,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 """ @@ -2031,10 +1924,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): @@ -2114,6 +2005,7 @@ def _make_init( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ) @@ -2126,7 +2018,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__", @@ -2143,7 +2035,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 f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2151,7 +2043,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, @@ -2166,7 +2058,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): @@ -2193,6 +2085,7 @@ def _attrs_to_init_script( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ): @@ -2208,6 +2101,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 @@ -2223,7 +2124,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 @@ -2274,12 +2175,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, ) ) @@ -2289,7 +2190,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, ) ) @@ -2299,7 +2200,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, ) ) @@ -2309,12 +2210,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: @@ -2333,14 +2234,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( " " @@ -2407,9 +2308,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 @@ -2425,7 +2324,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" @@ -2436,9 +2335,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: @@ -2446,14 +2345,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, @@ -2553,7 +2451,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. @@ -2613,7 +2511,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 @@ -2651,7 +2549,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) @@ -2906,10 +2804,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 c46c29ac2..79e8a44dc 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. """ @@ -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* 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/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 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 diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 49c9b0d7b..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 @@ -94,6 +92,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 +151,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]) @@ -251,7 +253,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(): @@ -384,15 +386,16 @@ 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): """ 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 @@ -417,7 +420,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_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 4a0b6b3b8..92fab29cc 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 @@ -228,9 +227,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): @@ -245,7 +244,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", @@ -705,7 +704,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): """ @@ -751,6 +749,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__ 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 3f8a39b61..a8bae2571 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()), ( @@ -2011,9 +2011,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__ @@ -2376,7 +2374,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: @@ -2392,7 +2392,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_mypy.yml b/tests/test_mypy.yml index fd09ca7e7..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" @@ -827,9 +826,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 diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 8395f9c02..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/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..1fd8d925d 100644 --- a/tox.ini +++ b/tox.ini @@ -10,17 +10,17 @@ 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 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,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report isolated_build = True @@ -39,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} @@ -62,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 =