New Upstream Release - python-attrs
Ready changes
Summary
Merged new upstream version: 23.1.0 (was: 22.2.0).
Diff
diff --git a/.git_archival.txt b/.git_archival.txt
new file mode 100644
index 0000000..07e72d2
--- /dev/null
+++ b/.git_archival.txt
@@ -0,0 +1,4 @@
+node: 1e2f6f9cac5cc60f0adab051c14adf09ffe39155
+node-date: 2023-04-16T12:22:15+02:00
+describe-name: 23.1.0
+ref-names: tag: 23.1.0
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ec8c333
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Force LF line endings for text files
+* text=auto eol=lf
+
+# Needed for setuptools-scm-git-archive
+.git_archival.txt export-subst
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 2b22ab6..da2124f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -42,27 +42,42 @@ The official tag is `python-attrs` and helping out in support frees us up to imp
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.
+Also, running [*pre-commit*] later on will require the latest Python version.
-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/).
+First [fork](https://github.com/python-attrs/attrs/fork) the repository on GitHub.
-Next, get an up-to-date checkout of the *attrs* repository:
+Clone the fork to your computer:
```console
-$ git clone git@github.com:python-attrs/attrs.git
+$ git clone git@github.com:<your-username>/attrs.git
```
-or if you prefer to use *Git* via `https`:
+Or if you prefer to use Git via HTTPS:
```console
-$ git clone https://github.com/python-attrs/attrs.git
+$ git clone https://github.com/<your-username>/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:
+Then add the *attrs* repository as *upstream* remote:
+
+```console
+$ git remote add -t main -m main --tags upstream https://github.com/python-attrs/attrs.git
+```
+
+The next step is to sync your local copy with the upstream repository:
+
+```console
+$ git fetch upstream
+```
+
+This is important to obtain eventually missing tags, which are needed to install the development version later on.
+See [#1104](https://github.com/python-attrs/attrs/issues/1104) for more information.
+
+Change into the newly created directory and after activating a virtual environment install an editable version of *attrs* along with its tests and docs requirements:
```console
$ cd attrs
-$ python -m pip install --upgrade pip wheel setuptools # PLEASE don't skip this step
+$ python -m pip install --upgrade pip wheel # PLEASE don't skip this step
$ python -m pip install -e '.[dev]'
```
@@ -84,6 +99,34 @@ $ make html
The built documentation can then be found in `docs/_build/html/`.
+To file a pull request, create a new branch on top of the upstream repository's `main` branch:
+
+```console
+$ git fetch upstream
+$ git checkout -b my_topical_branch upstream/main
+```
+
+Make your changes, push them to your fork (the remote *origin*):
+
+```console
+$ git push -u origin
+```
+
+and publish the PR in GitHub's web interface!
+
+After your pull request is merged and the branch is no longer needed, delete it:
+
+```console
+$ git checkout main
+$ git push --delete origin my_topical_branch && git branch -D my_topical_branch
+```
+
+Before starting to work on your next pull request, run the following command to sync your local repository with the remote *upstream*:
+
+```console
+$ git fetch upstream -u main:main
+```
+
---
To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] and its hooks:
@@ -168,7 +211,7 @@ But it's way more comfortable to run it locally and *git* catching avoidable err
First line of new section.
```
-- If you add a new feature, demonstrate its awesomeness on the [examples page](https://github.com/python-attrs/attrs/blob/main/docs/examples.rst)!
+- If you add a new feature, demonstrate its awesomeness on the [examples page](https://github.com/python-attrs/attrs/blob/main/docs/examples.md)!
### Changelog
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index ef4f212..7c250da 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,5 +1,3 @@
---
-
github: hynek
-ko_fi: the_hynek
tidelift: "pypi/attrs"
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4133e06..d23ac58 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -24,7 +24,10 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele
- [ ] New functions/classes have to be added to `docs/api.rst` by hand.
- [ ] Changes to the signature of `@attr.s()` have to be added by hand too.
- [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).
- Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file.
+ The next version is the second number in the current release + 1.
+ The first number represents the current year.
+ So if the current version on PyPI is 22.2.0, the next version is gonna be 22.3.0.
+ If the next version is the first in the new year, it'll be 23.1.0.
- [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).
- [ ] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you.
diff --git a/.github/workflows/build-docset.yml b/.github/workflows/build-docset.yml
index 21009ff..3910dcf 100644
--- a/.github/workflows/build-docset.yml
+++ b/.github/workflows/build-docset.yml
@@ -10,24 +10,19 @@ env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
PIP_NO_PYTHON_VERSION_WARNING: 1
-permissions: # added using https://github.com/step-security/secure-workflows
+permissions:
contents: read
jobs:
docset:
runs-on: ubuntu-latest
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
-
- uses: actions/checkout@v3
with:
- fetch-depth: 0 # get correct version once we switch to hatch-vcs
+ fetch-depth: 0
- uses: actions/setup-python@v4
with:
- python-version: "3.11"
+ python-version: "3.x"
- run: pip install tox
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 433af99..6dd7b85 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,21 +14,23 @@ env:
TOX_TESTENV_PASSENV: FORCE_COLOR
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_PYTHON_VERSION_WARNING: "1"
+ # Keep in sync with .pre-commit-config.yaml/default_language_version/python.
PYTHON_LATEST: "3.11"
+ # Use oldest version used in doctests / examples.
+ SETUPTOOLS_SCM_PRETEND_VERSION: "19.2.0"
permissions:
contents: read
jobs:
tests:
- name: tox on ${{ matrix.python-version }}
- runs-on: ubuntu-20.04
+ name: Tests on ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- - "3.6"
- "3.7"
- "3.8"
- "3.9"
@@ -42,28 +44,29 @@ jobs:
${{ contains(matrix.python-version, '~') && true || false }}
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- api.github.com:443
- files.pythonhosted.org:443
- github.com:443
- objects.githubusercontent.com:443
- pypi.org:443
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
+ - run: python -Im pip install --upgrade wheel tox
- - name: Install dependencies
+ - name: Determine Python version for tox
run: |
- python -VV
- python -m site
- python -m pip install --upgrade wheel tox tox-gh-actions
+ V=${{ matrix.python-version }}
+ if [[ "$V" = ~* ]]; then
+ # Extract version from a '~3.XX.0-0' specifier.
+ V=${V:1:4}
+ fi
+
+ if [[ "$V" = pypy-* ]]; then
+ V=pypy3
+ else
+ V=py$(echo $V | tr -d .)
+ fi
- - run: python -m tox
+ echo TOX_PYTHON=$V >>$GITHUB_ENV
+
+ - run: python -Im tox run -f ${{ env.TOX_PYTHON }}
- name: Upload coverage data
uses: actions/upload-artifact@v3
@@ -77,23 +80,13 @@ jobs:
needs: tests
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
- api.github.com:443
-
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
# Use latest Python, so it understands all syntax.
python-version: ${{env.PYTHON_LATEST}}
- - run: python -m pip install --upgrade coverage[toml]
+ - run: python -Im pip install --upgrade coverage[toml]
- name: Download coverage data
uses: actions/download-artifact@v3
@@ -102,9 +95,9 @@ jobs:
- name: Combine coverage and fail if it's <100%.
run: |
- python -m coverage combine
- python -m coverage html --skip-covered --skip-empty
- python -m coverage report --fail-under=100
+ python -Im coverage combine
+ python -Im coverage html --skip-covered --skip-empty
+ python -Im coverage report --fail-under=100
- name: Upload HTML report if check failed.
uses: actions/upload-artifact@v3
@@ -117,64 +110,48 @@ jobs:
name: Build docs & run doctests
runs-on: ubuntu-latest
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- docs.python.org:443
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
-
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
- python-version: "3.10"
+ # Keep in sync with tox/docs and .readthedocs.yaml.
+ python-version: "3.11"
- - run: python -m pip install --upgrade wheel tox
- - run: python -m tox -e docs
+ - run: python -Im pip install --upgrade wheel tox
+ - run: python -Im tox -e docs,changelog
- pyright:
- name: Check types using pyright
+ mypy:
+ name: Mypy on ${{ matrix.python-version }}
runs-on: ubuntu-latest
- steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- nodejs.org:443
- pypi.org:443
- registry.npmjs.org:443
- api.github.com:443
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - "3.7"
+ - "3.8"
+ - "3.9"
+ - "3.10"
+ - "3.11"
+ steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
- python-version: ${{env.PYTHON_LATEST}}
+ python-version: ${{ matrix.python-version }}
- - run: python -m pip install --upgrade wheel tox
- - run: python -m tox -e pyright
+ - run: python -Im pip install --upgrade wheel tox
+ - run: python -Im tox run -e mypy
- package:
- name: Build & verify package
+ pyright:
+ name: Check types using pyright
runs-on: ubuntu-latest
-
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
with:
- egress-policy: block
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
+ python-version: ${{env.PYTHON_LATEST}}
- - uses: actions/checkout@v3
- - uses: hynek/build-and-inspect-python-package@v1
+ - run: python -Im pip install --upgrade wheel tox
+ - run: python -Im tox run -e pyright
install-dev:
name: Verify dev env
@@ -184,21 +161,13 @@ jobs:
os: [ubuntu-latest, windows-latest]
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
- api.github.com:443
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{env.PYTHON_LATEST}}
- - run: python -m pip install -e .[dev]
- - run: python -c 'import attr; print(attr.__version__)'
+ - run: python -Im pip install -e .[dev]
+ - run: python -Ic 'import attr; print(attr.__version__)'
+ - run: python -Ic 'import attrs; print(attrs.__version__)'
# Ensure everything required is passing for branch protection.
required-checks-pass:
@@ -208,18 +177,14 @@ jobs:
- coverage
- docs
- install-dev
- - package
- - pyright
+ - mypy
+ # Pyright is currently flaky
+ # XXX: https://github.com/ekalinin/nodeenv/issues/324
+ # - pyright
runs-on: ubuntu-latest
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- api.github.com:443
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 8b736e3..bd8b486 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -27,18 +27,6 @@ jobs:
language: ["python"]
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
- with:
- egress-policy: block
- allowed-endpoints: >
- api.github.com:443
- bootstrap.pypa.io:443
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
- uploads.github.com:443
-
- name: Checkout repository
uses: actions/checkout@v3
diff --git a/.github/workflows/pypi-package.yml b/.github/workflows/pypi-package.yml
index acdcc06..8a613ff 100644
--- a/.github/workflows/pypi-package.yml
+++ b/.github/workflows/pypi-package.yml
@@ -1,78 +1,70 @@
---
-name: Deploy to (Test-) PyPI
+name: Build & maybe upload PyPI package
on:
push:
+ branches: [main]
tags: ["*"]
+ pull_request:
+ branches: [main]
release:
types:
- published
+ workflow_dispatch:
permissions:
contents: read
jobs:
- release-test-pypi:
- # Upload to Test PyPI on every pushed tag.
- environment: release-test-pypi
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+ # Always build & lint package.
+ build-package:
+ name: Build & verify package
runs-on: ubuntu-latest
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
+ - uses: actions/checkout@v3
with:
- egress-policy: block
- # Need the real pypi.org for installations.
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
- test.pypi.org:443
+ fetch-depth: 0
- - uses: actions/checkout@v3
- # with:
- # fetch-depth: 0
+ - uses: hynek/build-and-inspect-python-package@v1
- - name: Build package
- run: |
- python -m pip install -U build twine wheel
- python -m build
- twine check --strict dist/*
+ # Upload to Test PyPI on every commit on main.
+ release-test-pypi:
+ name: Publish in-dev package to test.pypi.org
+ environment: release-test-pypi
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ needs: build-package
- - name: Publish package to Test PyPI
+ steps:
+ - name: Download packages built by build-and-inspect-python-package
+ uses: actions/download-artifact@v3
+ with:
+ name: Packages
+ path: dist
+
+ - name: Upload package to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
- repository_url: https://test.pypi.org/legacy/
+ repository-url: https://test.pypi.org/legacy/
+ # Upload to real PyPI on GitHub Releases.
release-pypi:
- # Upload to real PyPI on GitHub Releases.
+ name: Publish released package to pypi.org
environment: release-pypi
if: github.event.action == 'published'
runs-on: ubuntu-latest
+ needs: build-package
steps:
- - name: Harden Runner
- uses: step-security/harden-runner@v2
+ - name: Download packages built by build-and-inspect-python-package
+ uses: actions/download-artifact@v3
with:
- egress-policy: block
- allowed-endpoints: >
- files.pythonhosted.org:443
- github.com:443
- pypi.org:443
-
- - uses: actions/checkout@v3
- # with:
- # fetch-depth: 0
-
- - name: Build package
- run: |
- python -m pip install -U build twine wheel
- python -m build
- twine check --strict dist/*
+ name: Packages
+ path: dist
- - name: Publish package to PyPI
+ - name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 937048e..b3e3cc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
.pytest_cache
.tox
.vscode
+.venv*
build
dist
docs/_build
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fc6b89b..1ac5c76 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,26 +3,26 @@ ci:
autoupdate_schedule: monthly
default_language_version:
- python: python3.10 # needed for match
+ # Keep in sync with ci.yml/PYTHON_LATEST
+ python: python3.11
repos:
- repo: https://github.com/psf/black
- rev: 22.10.0
+ rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.0
+ rev: v3.3.1
hooks:
- id: pyupgrade
- args: [--py36-plus, --keep-percent-format]
+ args: [--py37-plus, --keep-percent-format]
exclude: "tests/test_slots.py"
- repo: https://github.com/PyCQA/isort
- rev: 5.10.1
+ rev: 5.12.0
hooks:
- id: isort
- additional_dependencies: [toml]
files: \.py$
- repo: https://github.com/asottile/yesqa
diff --git a/readthedocs.yml b/.readthedocs.yaml
similarity index 92%
rename from readthedocs.yml
rename to .readthedocs.yaml
index 39aa7d6..121f885 100644
--- a/readthedocs.yml
+++ b/.readthedocs.yaml
@@ -6,7 +6,7 @@ build:
os: ubuntu-20.04
tools:
# Keep version in sync with tox.ini/docs and ci.yml/docs.
- python: "3.10"
+ python: "3.11"
python:
install:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3d1feb..fecbf56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,56 @@ Changes for the upcoming release can be found in the ["changelog.d" directory](h
<!-- towncrier release notes start -->
+## [23.1.0](https://github.com/python-attrs/attrs/tree/23.1.0) - 2023-04-16
+
+### Backwards-incompatible Changes
+
+- Python 3.6 has been dropped and packaging switched to static package data using [Hatch](https://hatch.pypa.io/latest/).
+ [#993](https://github.com/python-attrs/attrs/issues/993)
+
+
+### Deprecations
+
+- The support for *zope-interface* via the `attrs.validators.provides` validator is now deprecated and will be removed in, or after, April 2024.
+
+ The presence of a C-based package in our developement dependencies has caused headaches and we're not under the impression it's used a lot.
+
+ Let us know if you're using it and we might publish it as a separate package.
+ [#1120](https://github.com/python-attrs/attrs/issues/1120)
+
+
+### Changes
+
+- `attrs.filters.exclude()` and `attrs.filters.include()` now support the passing of attribute names as strings.
+ [#1068](https://github.com/python-attrs/attrs/issues/1068)
+- `attrs.has()` and `attrs.fields()` now handle generic classes correctly.
+ [#1079](https://github.com/python-attrs/attrs/issues/1079)
+- Fix frozen exception classes when raised within e.g. `contextlib.contextmanager`, which mutates their `__traceback__` attributes.
+ [#1081](https://github.com/python-attrs/attrs/issues/1081)
+- `@frozen` now works with type checkers that implement [PEP-681](https://peps.python.org/pep-0681/) (ex. [pyright](https://github.com/microsoft/pyright/)).
+ [#1084](https://github.com/python-attrs/attrs/issues/1084)
+- Restored ability to unpickle instances pickled before 22.2.0.
+ [#1085](https://github.com/python-attrs/attrs/issues/1085)
+- `attrs.asdict()`'s and `attrs.astuple()`'s type stubs now accept the `attrs.AttrsInstance` protocol.
+ [#1090](https://github.com/python-attrs/attrs/issues/1090)
+- Fix slots class cellvar updating closure in CPython 3.8+ even when `__code__` introspection is unavailable.
+ [#1092](https://github.com/python-attrs/attrs/issues/1092)
+- `attrs.resolve_types()` can now pass `include_extras` to `typing.get_type_hints()` on Python 3.9+, and does so by default.
+ [#1099](https://github.com/python-attrs/attrs/issues/1099)
+- Added instructions for pull request workflow to `CONTRIBUTING.md`.
+ [#1105](https://github.com/python-attrs/attrs/issues/1105)
+- Added *type* parameter to `attrs.field()` function for use with `attrs.make_class()`.
+
+ Please note that type checkers ignore type metadata passed into `make_class()`, but it can be useful if you're wrapping _attrs_.
+ [#1107](https://github.com/python-attrs/attrs/issues/1107)
+- It is now possible for `attrs.evolve()` (and `attr.evolve()`) to change fields named `inst` if the instance is passed as a positional argument.
+
+ Passing the instance using the `inst` keyword argument is now deprecated and will be removed in, or after, April 2024.
+ [#1117](https://github.com/python-attrs/attrs/issues/1117)
+- `attrs.validators.optional()` now also accepts a tuple of validators (in addition to lists of validators).
+ [#1122](https://github.com/python-attrs/attrs/issues/1122)
+
+
## [22.2.0](https://github.com/python-attrs/attrs/tree/22.2.0) - 2022-12-21
### Backwards-incompatible Changes
@@ -277,7 +327,7 @@ Changes for the upcoming release can be found in the ["changelog.d" directory](h
See the [new docs on comparison](https://www.attrs.org/en/stable/comparison.html) for more details.
[#787](https://github.com/python-attrs/attrs/issues/787)
-- Added **provisional** support for static typing in `pyright` via the [dataclass_transforms specification](https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md).
+- Added **provisional** support for static typing in `pyright` via [PEP 681](https://peps.python.org/pep-0681/).
Both the `pyright` specification and `attrs` implementation may change in future versions of both projects.
Your constructive feedback is welcome in both [attrs#795](https://github.com/python-attrs/attrs/issues/795) and [pyright#1782](https://github.com/microsoft/pyright/discussions/1782).
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 5424fea..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,25 +0,0 @@
-include LICENSE *.md *.toml *.yml *.yaml *.ini CITATION.cff
-graft .github
-
-# Stubs
-recursive-include src *.pyi
-recursive-include src py.typed
-
-# Tests
-include tox.ini conftest.py
-recursive-include tests *.py
-recursive-include tests *.yml
-
-# Documentation
-include docs/Makefile docs/docutils.conf
-recursive-include docs *.png
-recursive-include docs *.svg
-recursive-include docs *.py
-recursive-include docs *.rst
-recursive-include docs *.md
-prune docs/_build
-
-# Just to keep check-manifest happy; on releases those files are gone.
-# Last rule wins!
-exclude changelog.d/*.md
-include changelog.d/towncrier_template.md.jinja
diff --git a/README.md b/README.md
index 1c4837a..2f5b223 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,6 @@
</a>
</p>
-<!-- logo-end -->
-
<p align="center">
<a href="https://www.attrs.org/en/stable/">
<img src="https://img.shields.io/badge/Docs-RTD-black" alt="Documentation" />
@@ -145,7 +143,7 @@ For more details, please refer to our [comparison page](https://www.attrs.org/en
- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs)
- **License**: [MIT](https://www.attrs.org/en/latest/license.html)
- **Get Help**: please use the `python-attrs` tag on [StackOverflow](https://stackoverflow.com/questions/tagged/python-attrs)
-- **Supported Python Versions**: 3.6 and later
+- **Supported Python Versions**: 3.7 and later
### *attrs* for Enterprise
diff --git a/debian/changelog b/debian/changelog
index 16ca91c..6405850 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-attrs (23.1.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 24 Jun 2023 15:28:53 -0000
+
python-attrs (22.2.0-1) unstable; urgency=medium
* Team upload.
diff --git a/debian/patches/0001-Point-intersphinx_mapping-at-local-Python-docs.patch b/debian/patches/0001-Point-intersphinx_mapping-at-local-Python-docs.patch
index 0d01867..8e1e142 100644
--- a/debian/patches/0001-Point-intersphinx_mapping-at-local-Python-docs.patch
+++ b/debian/patches/0001-Point-intersphinx_mapping-at-local-Python-docs.patch
@@ -6,11 +6,11 @@ Subject: Point intersphinx_mapping at local Python docs.
docs/conf.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
-diff --git a/docs/conf.py b/docs/conf.py
-index ef5c419..83a5993 100644
---- a/docs/conf.py
-+++ b/docs/conf.py
-@@ -163,7 +163,7 @@ texinfo_documents = [
+Index: python-attrs.git/docs/conf.py
+===================================================================
+--- python-attrs.git.orig/docs/conf.py
++++ python-attrs.git/docs/conf.py
+@@ -160,7 +160,7 @@ texinfo_documents = [
epub_description = "Python Clases Without Boilerplate"
intersphinx_mapping = {
diff --git a/debian/patches/0002-Link-to-local-copy-of-logo.patch b/debian/patches/0002-Link-to-local-copy-of-logo.patch
index 4b31f4a..3615705 100644
--- a/debian/patches/0002-Link-to-local-copy-of-logo.patch
+++ b/debian/patches/0002-Link-to-local-copy-of-logo.patch
@@ -6,11 +6,11 @@ Subject: Link to local copy of logo
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
-diff --git a/CHANGELOG.md b/CHANGELOG.md
-index e3d1feb..cb0934e 100644
---- a/CHANGELOG.md
-+++ b/CHANGELOG.md
-@@ -853,7 +853,7 @@ To encourage more participation, the project has also been moved into a [dedicat
+Index: python-attrs.git/CHANGELOG.md
+===================================================================
+--- python-attrs.git.orig/CHANGELOG.md
++++ python-attrs.git/CHANGELOG.md
+@@ -903,7 +903,7 @@ To encourage more participation, the pro
`attrs` also has a logo now!
diff --git a/debian/patches/0003-Revert-theme.patch b/debian/patches/0003-Revert-theme.patch
index fa8bb73..678e4b6 100644
--- a/debian/patches/0003-Revert-theme.patch
+++ b/debian/patches/0003-Revert-theme.patch
@@ -6,11 +6,11 @@ Subject: Revert theme.
docs/conf.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
-diff --git a/docs/conf.py b/docs/conf.py
-index 83a5993..69e6dd2 100644
---- a/docs/conf.py
-+++ b/docs/conf.py
-@@ -92,7 +92,7 @@ add_function_parentheses = True
+Index: python-attrs.git/docs/conf.py
+===================================================================
+--- python-attrs.git.orig/docs/conf.py
++++ python-attrs.git/docs/conf.py
+@@ -89,7 +89,7 @@ add_function_parentheses = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
diff --git a/debian/patches/0004-Drop-unpackaged-sphinx-extensions.patch b/debian/patches/0004-Drop-unpackaged-sphinx-extensions.patch
index 8a7afc1..873ec89 100644
--- a/debian/patches/0004-Drop-unpackaged-sphinx-extensions.patch
+++ b/debian/patches/0004-Drop-unpackaged-sphinx-extensions.patch
@@ -6,11 +6,11 @@ Subject: Drop unpackaged sphinx extensions
docs/conf.py | 2 --
1 file changed, 2 deletions(-)
-diff --git a/docs/conf.py b/docs/conf.py
-index 69e6dd2..0f4c1ad 100644
---- a/docs/conf.py
-+++ b/docs/conf.py
-@@ -43,8 +43,6 @@ extensions = [
+Index: python-attrs.git/docs/conf.py
+===================================================================
+--- python-attrs.git.orig/docs/conf.py
++++ python-attrs.git/docs/conf.py
+@@ -40,8 +40,6 @@ extensions = [
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
diff --git a/debian/patches/0005-Fix-privacy-breaches.patch b/debian/patches/0005-Fix-privacy-breaches.patch
index 89b876f..c7392b3 100644
--- a/debian/patches/0005-Fix-privacy-breaches.patch
+++ b/debian/patches/0005-Fix-privacy-breaches.patch
@@ -6,11 +6,11 @@ Subject: Fix privacy breaches
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
-diff --git a/README.md b/README.md
-index 1c4837a..0f08a84 100644
---- a/README.md
-+++ b/README.md
-@@ -41,19 +41,19 @@ Especially those generously supporting us at the *The Organization* tier and hig
+Index: python-attrs.git/README.md
+===================================================================
+--- python-attrs.git.orig/README.md
++++ python-attrs.git/README.md
+@@ -39,19 +39,19 @@ Especially those generously supporting u
<p align="center">
<a href="https://www.variomedia.de/">
diff --git a/docs/_static/docset-icon.png b/docs/_static/docset-icon.png
index 36d1034..d9886f1 100644
Binary files a/docs/_static/docset-icon.png and b/docs/_static/docset-icon.png differ
diff --git a/docs/_static/docset-icon@2x.png b/docs/_static/docset-icon@2x.png
index ae336fe..5551b42 100644
Binary files a/docs/_static/docset-icon@2x.png and b/docs/_static/docset-icon@2x.png differ
diff --git a/docs/api-attr.rst b/docs/api-attr.rst
new file mode 100644
index 0000000..1c1c3ed
--- /dev/null
+++ b/docs/api-attr.rst
@@ -0,0 +1,242 @@
+API Reference for the ``attr`` Namespace
+========================================
+
+.. module:: attr
+
+
+Core
+----
+
+.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True, unsafe_hash=None)
+
+ .. note::
+
+ *attrs* also comes with a serious-business alias ``attr.attrs``.
+
+ For example:
+
+ .. doctest::
+
+ >>> import attr
+ >>> @attr.s
+ ... class C:
+ ... _private = attr.ib()
+ >>> C(private=42)
+ C(_private=42)
+ >>> class D:
+ ... def __init__(self, x):
+ ... self.x = x
+ >>> D(1)
+ <D object at ...>
+ >>> D = attr.s(these={"x": attr.ib()}, init=False)(D)
+ >>> D(1)
+ D(x=1)
+ >>> @attr.s(auto_exc=True)
+ ... class Error(Exception):
+ ... x = attr.ib()
+ ... y = attr.ib(default=42, init=False)
+ >>> Error("foo")
+ Error(x='foo', y=42)
+ >>> raise Error("foo")
+ Traceback (most recent call last):
+ ...
+ Error: ('foo', 42)
+ >>> raise ValueError("foo", 42) # for comparison
+ Traceback (most recent call last):
+ ...
+ ValueError: ('foo', 42)
+
+
+.. autofunction:: attr.ib
+
+ .. note::
+
+ *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:
+
+ .. doctest::
+
+ >>> @attr.s
+ ... class C:
+ ... x = attr.ib()
+ ... y = attr.ib()
+ ... @x.validator
+ ... def _any_name_except_a_name_of_an_attribute(self, attribute, value):
+ ... if value < 0:
+ ... raise ValueError("x must be positive")
+ ... @y.default
+ ... def _any_name_except_a_name_of_an_attribute(self):
+ ... return self.x + 1
+ >>> C(1)
+ C(x=1, y=2)
+ >>> C(-1)
+ Traceback (most recent call last):
+ ...
+ ValueError: x must be positive
+
+
+.. function:: define
+
+ Same as `attrs.define`.
+
+.. function:: mutable
+
+ Same as `attrs.mutable`.
+
+.. function:: frozen
+
+ Same as `attrs.frozen`.
+
+.. function:: field
+
+ Same as `attrs.field`.
+
+.. class:: Attribute
+
+ Same as `attrs.Attribute`.
+
+.. function:: make_class
+
+ Same as `attrs.make_class`.
+
+.. autoclass:: Factory
+ :noindex:
+
+ Same as `attrs.Factory`.
+
+
+.. data:: NOTHING
+
+ Same as `attrs.NOTHING`.
+
+
+Exceptions
+----------
+
+.. module:: attr.exceptions
+
+All exceptions are available from both ``attr.exceptions`` and `attrs.exceptions` (it's the same module in a different namespace).
+
+Please refer to `attrs.exceptions` for details.
+
+
+Helpers
+-------
+
+.. currentmodule:: attr
+
+.. function:: cmp_using
+
+ Same as `attrs.cmp_using`.
+
+.. function:: fields
+
+ Same as `attrs.fields`.
+
+.. function:: fields_dict
+
+ Same as `attr.fields_dict`.
+
+.. function:: has
+
+ Same as `attrs.has`.
+
+.. function:: resolve_types
+
+ Same as `attrs.resolve_types`.
+
+.. autofunction:: asdict
+.. autofunction:: astuple
+
+.. module:: attr.filters
+
+.. function:: include
+
+ Same as `attrs.filters.include`.
+
+.. function:: exclude
+
+ Same as `attrs.filters.exclude`.
+
+See :func:`attrs.asdict` for examples.
+
+All objects from `attrs.filters` are also available in ``attr.filters``.
+
+----
+
+.. currentmodule:: attr
+
+.. function:: evolve
+
+ Same as `attrs.evolve`.
+
+.. function:: validate
+
+ Same as `attrs.validate`.
+
+
+Validators
+----------
+
+.. module:: attr.validators
+
+All objects from `attrs.validators` are also available in ``attr.validators``.
+Please refer to the former for details.
+
+
+Converters
+----------
+
+.. module:: attr.converters
+
+All objects from `attrs.converters` are also available from ``attr.converters``.
+Please refer to the former for details.
+
+
+Setters
+-------
+
+.. module:: attr.setters
+
+All objects from `attrs.setters` are also available in ``attr.setters``.
+Please refer to the former for details.
+
+
+Deprecated APIs
+---------------
+
+.. currentmodule:: attr
+
+To help you write backward compatible code that doesn't throw warnings on modern releases, the ``attr`` module has an ``__version_info__`` attribute as of version 19.2.0.
+It behaves similarly to `sys.version_info` and is an instance of `attr.VersionInfo`:
+
+.. autoclass:: VersionInfo
+
+ With its help you can write code like this:
+
+ >>> if getattr(attr, "__version_info__", (0,)) >= (19, 2):
+ ... cmp_off = {"eq": False}
+ ... else:
+ ... cmp_off = {"cmp": False}
+ >>> cmp_off == {"eq": False}
+ True
+ >>> @attr.s(**cmp_off)
+ ... class C:
+ ... pass
+
+
+----
+
+.. autofunction:: assoc
+
+Before *attrs* got `attrs.validators.set_disabled` and `attrs.validators.set_disabled`, it had the following APIs to globally enable and disable validators.
+They won't be removed, but are discouraged to use:
+
+.. autofunction:: set_run_validators
+.. autofunction:: get_run_validators
+
+----
+
+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.
diff --git a/docs/api.rst b/docs/api.rst
index 2ab620d..1f87e01 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1,83 +1,62 @@
API Reference
=============
-.. currentmodule:: attr
+.. module:: attrs
-*attrs* works by decorating a class using `attrs.define` or `attr.s` and then optionally defining attributes on the class using `attrs.field`, `attr.ib`, or a type annotation.
+*attrs* works by decorating a class using `attrs.define` or `attr.s` and then defining attributes on the class using `attrs.field`, `attr.ib`, or type annotations.
-If you're confused by the many names, please check out `names` for clarification.
+What follows is the API explanation, if you'd like a more hands-on tutorial, have a look at `examples`.
-What follows is the API explanation, if you'd like a more hands-on introduction, have a look at `examples`.
+If you're confused by the many names, please check out `names` for clarification, but the `TL;DR <https://en.wikipedia.org/wiki/TL;DR>`_ is that as of version 21.3.0, *attrs* consists of **two** top-level package names:
-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.
+- The classic ``attr`` that powers the venerable `attr.s` and `attr.ib`.
+- The newer ``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`).
-The ``attrs`` namespace is built *on top of* ``attr`` which will *never* go away.
+The ``attrs`` namespace is built *on top of* ``attr`` -- which will *never* go away -- and is just as stable, since it doesn't constitute a rewrite.
+To keep repetition low and this document at a reasonable size, the ``attr`` namespace is `documented on a separate page <api-attr>`, though.
Core
----
-.. note::
-
- Please note that the ``attrs`` namespace has been added in version 21.3.0.
- Most of the objects are simply re-imported from ``attr``.
- Therefore if a class, method, or function claims that it has been added in an older version, it is only available in the ``attr`` namespace.
-
.. autodata:: attrs.NOTHING
:no-value:
.. autofunction:: attrs.define
-.. function:: attrs.mutable(same_as_define)
+.. function:: mutable(same_as_define)
- Alias for `attrs.define`.
+ Same as `attrs.define`.
.. versionadded:: 20.1.0
-.. function:: attrs.frozen(same_as_define)
+.. function:: frozen(same_as_define)
Behaves the same as `attrs.define` but sets *frozen=True* and *on_setattr=None*.
.. versionadded:: 20.1.0
-.. autofunction:: attrs.field
-
-.. function:: define
-
- Old import path for `attrs.define`.
-
-.. function:: mutable
-
- Old import path for `attrs.mutable`.
+.. autofunction:: field
-.. function:: frozen
-
- Old import path for `attrs.frozen`.
-
-.. function:: field
-
- Old import path for `attrs.field`.
-
-.. autoclass:: attrs.Attribute
+.. autoclass:: Attribute
:members: evolve
For example:
.. doctest::
- >>> import attr
- >>> @attr.s
+ >>> import attrs
+ >>> from attrs import define, field
+
+ >>> @define
... class C:
- ... x = attr.ib()
- >>> attr.fields(C).x
+ ... x = field()
+ >>> attrs.fields(C).x
Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x')
-.. autofunction:: attrs.make_class
+.. autofunction:: make_class
This is handy if you want to programmatically create classes.
@@ -85,25 +64,27 @@ Core
.. doctest::
- >>> C1 = attr.make_class("C1", ["x", "y"])
+ >>> C1 = attrs.make_class("C1", ["x", "y"])
>>> C1(1, 2)
C1(x=1, y=2)
- >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42),
- ... "y": attr.ib(default=attr.Factory(list))})
+ >>> C2 = attrs.make_class("C2", {
+ ... "x": field(default=42),
+ ... "y": field(factory=list)
+ ... })
>>> C2()
C2(x=42, y=[])
-.. autoclass:: attrs.Factory
+.. autoclass:: Factory
For example:
.. doctest::
- >>> @attr.s
+ >>> @define
... class C:
- ... x = attr.ib(default=attr.Factory(list))
- ... y = attr.ib(default=attr.Factory(
+ ... x = field(default=attrs.Factory(list))
+ ... y = field(default=attrs.Factory(
... lambda self: set(self.x),
... takes_self=True)
... )
@@ -113,86 +94,11 @@ Core
C(x=[1, 2, 3], y={1, 2, 3})
-Classic
-~~~~~~~
-
-.. data:: attr.NOTHING
-
- Same as `attrs.NOTHING`.
-
-.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True, unsafe_hash=None)
-
- .. note::
-
- *attrs* also comes with a serious-business alias ``attr.attrs``.
-
- For example:
-
- .. doctest::
-
- >>> import attr
- >>> @attr.s
- ... class C:
- ... _private = attr.ib()
- >>> C(private=42)
- C(_private=42)
- >>> class D:
- ... def __init__(self, x):
- ... self.x = x
- >>> D(1)
- <D object at ...>
- >>> D = attr.s(these={"x": attr.ib()}, init=False)(D)
- >>> D(1)
- D(x=1)
- >>> @attr.s(auto_exc=True)
- ... class Error(Exception):
- ... x = attr.ib()
- ... y = attr.ib(default=42, init=False)
- >>> Error("foo")
- Error(x='foo', y=42)
- >>> raise Error("foo")
- Traceback (most recent call last):
- ...
- Error: ('foo', 42)
- >>> raise ValueError("foo", 42) # for comparison
- Traceback (most recent call last):
- ...
- ValueError: ('foo', 42)
-
-
-.. autofunction:: attr.ib
-
- .. note::
-
- *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:
-
- .. doctest::
-
- >>> @attr.s
- ... class C:
- ... x = attr.ib()
- ... y = attr.ib()
- ... @x.validator
- ... def _any_name_except_a_name_of_an_attribute(self, attribute, value):
- ... if value < 0:
- ... raise ValueError("x must be positive")
- ... @y.default
- ... def _any_name_except_a_name_of_an_attribute(self):
- ... return self.x + 1
- >>> C(1)
- C(x=1, y=2)
- >>> C(-1)
- Traceback (most recent call last):
- ...
- ValueError: x must be positive
-
-
-
Exceptions
----------
+.. module:: attrs.exceptions
+
All exceptions are available from both ``attr.exceptions`` and ``attrs.exceptions`` and are the same thing.
That means that it doesn't matter from from which namespace they've been raised and/or caught:
@@ -205,15 +111,15 @@ That means that it doesn't matter from from which namespace they've been raised
... print("this works!")
this works!
-.. autoexception:: attrs.exceptions.PythonTooOldError
-.. autoexception:: attrs.exceptions.FrozenError
-.. autoexception:: attrs.exceptions.FrozenInstanceError
-.. autoexception:: attrs.exceptions.FrozenAttributeError
-.. autoexception:: attrs.exceptions.AttrsAttributeNotFoundError
-.. autoexception:: attrs.exceptions.NotAnAttrsClassError
-.. autoexception:: attrs.exceptions.DefaultAlreadySetError
-.. autoexception:: attrs.exceptions.UnannotatedAttributeError
-.. autoexception:: attrs.exceptions.NotCallableError
+.. autoexception:: PythonTooOldError
+.. autoexception:: FrozenError
+.. autoexception:: FrozenInstanceError
+.. autoexception:: FrozenAttributeError
+.. autoexception:: AttrsAttributeNotFoundError
+.. autoexception:: NotAnAttrsClassError
+.. autoexception:: DefaultAlreadySetError
+.. autoexception:: NotCallableError
+.. autoexception:: UnannotatedAttributeError
For example::
@@ -230,10 +136,9 @@ Helpers
*attrs* comes with a bunch of helper methods that make working with it easier:
-.. autofunction:: attrs.cmp_using
-.. function:: attr.cmp_using
+.. currentmodule:: attrs
- Same as `attrs.cmp_using`.
+.. autofunction:: attrs.cmp_using
.. autofunction:: attrs.fields
@@ -241,10 +146,10 @@ Helpers
.. doctest::
- >>> @attr.s
+ >>> @define
... class C:
- ... x = attr.ib()
- ... y = attr.ib()
+ ... x = field()
+ ... y = field()
>>> attrs.fields(C)
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y'))
>>> attrs.fields(C)[1]
@@ -252,10 +157,6 @@ Helpers
>>> attrs.fields(C).y is attrs.fields(C)[1]
True
-.. function:: attr.fields
-
- Same as `attrs.fields`.
-
.. autofunction:: attrs.fields_dict
For example:
@@ -273,10 +174,6 @@ Helpers
>>> attrs.fields_dict(C)['y'] is attrs.fields(C).y
True
-.. function:: attr.fields_dict
-
- Same as `attrs.fields_dict`.
-
.. autofunction:: attrs.has
For example:
@@ -291,10 +188,6 @@ Helpers
>>> attr.has(object)
False
-.. function:: attr.has
-
- Same as `attrs.has`.
-
.. autofunction:: attrs.resolve_types
For example:
@@ -302,12 +195,12 @@ Helpers
.. doctest::
>>> import typing
- >>> @attrs.define
+ >>> @define
... class A:
... a: typing.List['A']
... b: 'B'
...
- >>> @attrs.define
+ >>> @define
... class B:
... a: A
...
@@ -322,68 +215,55 @@ Helpers
>>> attrs.fields(A).b.type
<class 'B'>
-.. function:: attr.resolve_types
-
- Same as `attrs.resolve_types`.
-
.. autofunction:: attrs.asdict
For example:
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
... x: int
... y: int
>>> attrs.asdict(C(1, C(2, 3)))
{'x': 1, 'y': {'x': 2, 'y': 3}}
-.. autofunction:: attr.asdict
-
.. autofunction:: attrs.astuple
For example:
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attr.field()
- ... y = attr.field()
+ ... x = field()
+ ... y = field()
>>> attrs.astuple(C(1,2))
(1, 2)
-.. autofunction:: attr.astuple
-
-
-*attrs* includes some handy helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`:
-
-.. autofunction:: attrs.filters.include
-
-.. autofunction:: attrs.filters.exclude
+.. module:: attrs.filters
-.. function:: attr.filters.include
+*attrs* includes helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`:
- Same as `attrs.filters.include`.
+.. autofunction:: include
-.. function:: attr.filters.exclude
-
- Same as `attrs.filters.exclude`.
+.. autofunction:: exclude
See :func:`attrs.asdict` for examples.
-All objects from ``attrs.filters`` are also available from ``attr.filters``.
+All objects from ``attrs.filters`` are also available from ``attr.filters`` (it's the same module in a different namespace).
----
+.. currentmodule:: attrs
+
.. autofunction:: attrs.evolve
For example:
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
... x: int
... y: int
@@ -403,19 +283,15 @@ All objects from ``attrs.filters`` are also available from ``attr.filters``.
* attributes with ``init=False`` can't be set with ``evolve``.
* the usual ``__init__`` validators will validate the new values.
-.. function:: attr.evolve
-
- Same as `attrs.evolve`.
-
.. autofunction:: attrs.validate
For example:
.. doctest::
- >>> @attrs.define(on_setattr=attrs.setters.NO_OP)
+ >>> @define(on_setattr=attrs.setters.NO_OP)
... class C:
- ... x = attrs.field(validator=attrs.validators.instance_of(int))
+ ... x = field(validator=attrs.validators.instance_of(int))
>>> i = C(1)
>>> i.x = "1"
>>> attrs.validate(i)
@@ -423,26 +299,16 @@ All objects from ``attrs.filters`` are also available from ``attr.filters``.
...
TypeError: ("'x' must be <class 'int'> (got '1' that is a <class 'str'>).", ...)
-.. function:: attr.validate
-
- Same as `attrs.validate`.
-
-
-Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact:
-
-.. autofunction:: set_run_validators
-
-.. autofunction:: get_run_validators
-
.. _api-validators:
Validators
----------
-*attrs* comes with some common validators in the ``attrs.validators`` module.
-All objects from ``attrs.validators`` are also available from ``attr.validators``.
+.. module:: attrs.validators
+*attrs* comes with some common validators in the ``attrs.validators`` module.
+All objects from ``attrs.validators`` are also available from ``attr.validators`` (it's the same module in a different namespace).
.. autofunction:: attrs.validators.lt
@@ -450,9 +316,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.lt(42))
+ ... x = field(validator=attrs.validators.lt(42))
>>> C(41)
C(x=41)
>>> C(42)
@@ -466,9 +332,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attr.validators.le(42))
+ ... x = field(validator=attrs.validators.le(42))
>>> C(42)
C(x=42)
>>> C(43)
@@ -482,7 +348,7 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
... x = attrs.field(validator=attrs.validators.ge(42))
>>> C(42)
@@ -498,9 +364,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attr.field(validator=attrs.validators.gt(42))
+ ... x = field(validator=attrs.validators.gt(42))
>>> C(43)
C(x=43)
>>> C(42)
@@ -514,9 +380,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.max_len(4))
+ ... x = field(validator=attrs.validators.max_len(4))
>>> C("spam")
C(x='spam')
>>> C("bacon")
@@ -530,9 +396,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.min_len(1))
+ ... x = field(validator=attrs.validators.min_len(1))
>>> C("bacon")
C(x='bacon')
>>> C("")
@@ -546,9 +412,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.instance_of(int))
+ ... x = field(validator=attrs.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
@@ -570,10 +436,10 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
>>> class State(enum.Enum):
... ON = "on"
... OFF = "off"
- >>> @attrs.define
+ >>> @define
... class C:
- ... state = attrs.field(validator=attrs.validators.in_(State))
- ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3]))
+ ... state = field(validator=attrs.validators.in_(State))
+ ... val = field(validator=attrs.validators.in_([1, 2, 3]))
>>> C(State.ON, 1)
C(state=<State.ON: 'on'>, val=1)
>>> C("on", 1)
@@ -593,8 +459,8 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
Thus the following two statements are equivalent::
- x = attrs.field(validator=attrs.validators.and_(v1, v2, v3))
- x = attrs.field(validator=[v1, v2, v3])
+ x = field(validator=attrs.validators.and_(v1, v2, v3))
+ x = field(validator=[v1, v2, v3])
.. autofunction:: attrs.validators.not_
@@ -603,9 +469,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
>>> reserved_names = {"id", "time", "source"}
- >>> @attrs.define
+ >>> @define
... class Measurement:
- ... tags = attrs.field(
+ ... tags = field(
... validator=attrs.validators.deep_mapping(
... key_validator=attrs.validators.not_(
... attrs.validators.in_(reserved_names),
@@ -628,9 +494,12 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.optional(attr.validators.instance_of(int)))
+ ... x = field(
+ ... validator=attrs.validators.optional(
+ ... attrs.validators.instance_of(int)
+ ... ))
>>> C(42)
C(x=42)
>>> C("42")
@@ -647,9 +516,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.is_callable())
+ ... x = field(validator=attrs.validators.is_callable())
>>> C(isinstance)
C(x=<built-in function isinstance>)
>>> C("not a callable")
@@ -664,9 +533,9 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class User:
- ... email = attrs.field(validator=attrs.validators.matches_re(
+ ... email = field(validator=attrs.validators.matches_re(
... "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"))
>>> User(email="user@example.com")
User(email='user@example.com')
@@ -682,11 +551,11 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.deep_iterable(
- ... member_validator=attrs.validators.instance_of(int),
- ... iterable_validator=attrs.validators.instance_of(list)
+ ... x = field(validator=attrs.validators.deep_iterable(
+ ... member_validator=attrs.validators.instance_of(int),
+ ... iterable_validator=attrs.validators.instance_of(list)
... ))
>>> C(x=[1, 2, 3])
C(x=[1, 2, 3])
@@ -706,12 +575,12 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
.. doctest::
- >>> @attrs.define
+ >>> @define
... class C:
- ... x = attrs.field(validator=attrs.validators.deep_mapping(
- ... key_validator=attrs.validators.instance_of(str),
- ... value_validator=attrs.validators.instance_of(int),
- ... mapping_validator=attrs.validators.instance_of(dict)
+ ... x = field(validator=attrs.validators.deep_mapping(
+ ... key_validator=attrs.validators.instance_of(str),
+ ... value_validator=attrs.validators.instance_of(int),
+ ... mapping_validator=attrs.validators.instance_of(dict)
... ))
>>> C(x={"a": 1, "b": 2})
C(x={'a': 1, 'b': 2})
@@ -740,16 +609,18 @@ Validators can be both globally and locally disabled:
Converters
----------
-All objects from ``attrs.converters`` are also available from ``attr.converters``.
+.. module:: attrs.converters
+
+All objects from ``attrs.converters`` are also available from ``attr.converters`` (it's the same module in a different namespace).
.. autofunction:: attrs.converters.pipe
- For convenience, it's also possible to pass a list to `attr.ib`'s converter argument.
+ For convenience, it's also possible to pass a list to `attrs.field` / `attr.ib`'s converter arguments.
Thus the following two statements are equivalent::
- x = attr.ib(converter=attr.converter.pipe(c1, c2, c3))
- x = attr.ib(converter=[c1, c2, c3])
+ x = attrs.field(converter=attrs.converter.pipe(c1, c2, c3))
+ x = attrs.field(converter=[c1, c2, c3])
.. autofunction:: attrs.converters.optional
@@ -757,9 +628,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters`
.. doctest::
- >>> @attr.s
+ >>> @define
... class C:
- ... x = attr.ib(converter=attr.converters.optional(int))
+ ... x = field(converter=attrs.converters.optional(int))
>>> C(None)
C(x=None)
>>> C(42)
@@ -772,10 +643,10 @@ All objects from ``attrs.converters`` are also available from ``attr.converters`
.. doctest::
- >>> @attr.s
+ >>> @define
... class C:
- ... x = attr.ib(
- ... converter=attr.converters.default_if_none("")
+ ... x = field(
+ ... converter=attrs.converters.default_if_none("")
... )
>>> C(None)
C(x='')
@@ -787,10 +658,10 @@ All objects from ``attrs.converters`` are also available from ``attr.converters`
.. doctest::
- >>> @attr.s
+ >>> @define
... class C:
- ... x = attr.ib(
- ... converter=attr.converters.to_bool
+ ... x = field(
+ ... converter=attrs.converters.to_bool
... )
>>> C("yes")
C(x=True)
@@ -808,14 +679,17 @@ All objects from ``attrs.converters`` are also available from ``attr.converters`
Setters
-------
+.. module:: attrs.setters
+
These are helpers that you can use together with `attrs.define`'s and `attrs.fields`'s ``on_setattr`` arguments.
-All setters in ``attrs.setters`` are also available from ``attr.setters``.
+All setters in ``attrs.setters`` are also available from ``attr.setters`` (it's the same module in a different namespace).
+
+.. autofunction:: frozen
+.. autofunction:: validate
+.. autofunction:: convert
+.. autofunction:: pipe
-.. autofunction:: attrs.setters.frozen
-.. autofunction:: attrs.setters.validate
-.. autofunction:: attrs.setters.convert
-.. autofunction:: attrs.setters.pipe
-.. data:: attrs.setters.NO_OP
+.. data:: NO_OP
Sentinel for disabling class-wide *on_setattr* hooks for certain attributes.
@@ -827,10 +701,10 @@ All setters in ``attrs.setters`` are also available from ``attr.setters``.
.. doctest::
- >>> @attrs.define(on_setattr=attr.setters.frozen)
+ >>> @define(on_setattr=attr.setters.frozen)
... class C:
- ... x = attr.field()
- ... y = attr.field(on_setattr=attr.setters.NO_OP)
+ ... x = field()
+ ... y = field(on_setattr=attr.setters.NO_OP)
>>> c = C(1, 2)
>>> c.y = 3
>>> c.y
@@ -841,34 +715,3 @@ All setters in ``attrs.setters`` are also available from ``attr.setters``.
attrs.exceptions.FrozenAttributeError: ()
N.B. Please use `attrs.define`'s *frozen* argument (or `attrs.frozen`) to freeze whole classes; it is more efficient.
-
-
-Deprecated APIs
----------------
-
-.. _version-info:
-
-To help you write backward compatible code that doesn't throw warnings on modern releases, the ``attr`` module has an ``__version_info__`` attribute as of version 19.2.0.
-It behaves similarly to `sys.version_info` and is an instance of `VersionInfo`:
-
-.. autoclass:: VersionInfo
-
- With its help you can write code like this:
-
- >>> if getattr(attr, "__version_info__", (0,)) >= (19, 2):
- ... cmp_off = {"eq": False}
- ... else:
- ... cmp_off = {"cmp": False}
- >>> cmp_off == {"eq": False}
- True
- >>> @attr.s(**cmp_off)
- ... class C:
- ... pass
-
-
-----
-
-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/conf.py b/docs/conf.py
index ef5c419..b02f07e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,9 +20,6 @@ linkcheck_ignore = [
r"https://github.com/.*/(issues|pull)/\d+",
# Rate limits and the latest tag is missing anyways on release.
"https://github.com/python-attrs/attrs/tree/.*",
- # It never finds the anchor even though it's there.
- "https://github.com/microsoft/pyright/blob/main/specs/"
- "dataclass_transforms.md#attrs",
]
# In nitpick mode (-n), still ignore any of the following "broken" references
diff --git a/docs/examples.md b/docs/examples.md
index 465dc39..242720c 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -215,7 +215,7 @@ For that, {func}`attrs.asdict` offers a callback that decides whether an attribu
{'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]}
```
-For the common case where you want to [`include`](attrs.filters.include) or [`exclude`](attrs.filters.exclude) certain types or attributes, *attrs* ships with a few helpers:
+For the common case where you want to [`include`](attrs.filters.include) or [`exclude`](attrs.filters.exclude) certain types, string name or attributes, *attrs* ships with a few helpers:
```{doctest}
>>> from attrs import asdict, filters, fields
@@ -224,11 +224,12 @@ For the common case where you want to [`include`](attrs.filters.include) or [`ex
... class User:
... login: str
... password: str
+... email: str
... id: int
>>> asdict(
-... User("jane", "s33kred", 42),
-... filter=filters.exclude(fields(User).password, int))
+... User("jane", "s33kred", "jane@example.com", 42),
+... filter=filters.exclude(fields(User).password, "email", int))
{'login': 'jane'}
>>> @define
@@ -240,7 +241,33 @@ For the common case where you want to [`include`](attrs.filters.include) or [`ex
>>> asdict(C("foo", "2", 3),
... filter=filters.include(int, fields(C).x))
{'x': 'foo', 'z': 3}
+
+>>> asdict(C("foo", "2", 3),
+... filter=filters.include(fields(C).x, "z"))
+{'x': 'foo', 'z': 3}
+```
+
+:::{note}
+Though using string names directly is convenient, mistyping attribute names will silently do the wrong thing and neither Python nor your type checker can help you.
+{func}`attrs.fields()` will raise an `AttributeError` when the field doesn't exist while literal string names won't.
+Using {func}`attrs.fields()` to get attributes is worth being recommended in most cases.
+
+```{doctest}
+>>> asdict(
+... User("jane", "s33kred", "jane@example.com", 42),
+... filter=filters.exclude("passwd")
+... )
+{'login': 'jane', 'password': 's33kred', 'email': 'jane@example.com', 'id': 42}
+
+>>> asdict(
+... User("jane", "s33kred", "jane@example.com", 42),
+... filter=fields(User).passwd
+... )
+Traceback (most recent call last):
+...
+AttributeError: 'UserAttributes' object has no attribute 'passwd'. Did you mean: 'password'?
```
+:::
Other times, all you want is a tuple and *attrs* won't let you down:
@@ -471,7 +498,7 @@ If you're the author of a third-party library with *attrs* integration, please s
## Types
-*attrs* also allows you to associate a type with an attribute using either the *type* argument to {func}`attr.ib` or using {pep}`526`-annotations:
+*attrs* also allows you to associate a type with an attribute using either the *type* argument to {func}`attr.ib` and {func}`attr.field` or using {pep}`526`-annotations:
```{doctest}
>>> @define
@@ -626,11 +653,13 @@ Sometimes you may want to create a class programmatically.
>>> from attrs import make_class
>>> @define
... class C1:
-... x = field()
+... x = field(type=int)
... y = field()
->>> C2 = make_class("C2", ["x", "y"])
+>>> C2 = make_class("C2", {"x": field(type=int), "y": field()})
>>> fields(C1) == fields(C2)
True
+>>> fields(C1).x.type
+<class 'int'>
```
You can still have power over the attributes if you pass a dictionary of name: {func}`~attrs.field` mappings and can pass arguments to `@attr.s`:
diff --git a/docs/extending.md b/docs/extending.md
index 84bd9bf..b2f96fb 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -94,7 +94,7 @@ You can only use this trick to tell *Mypy* that a class is actually an *attrs* c
### Pyright
-Generic decorator wrapping is supported in [*Pyright*](https://github.com/microsoft/pyright) via their [`dataclass_transform`] specification.
+Generic decorator wrapping is supported in [*Pyright*](https://github.com/microsoft/pyright) via `dataclass_transform` / {pep}`689`.
For a custom wrapping of the form:
@@ -118,11 +118,6 @@ def __dataclass_transform__(
def custom_define(f): ...
```
-:::{warning}
-`dataclass_transform` is supported **provisionally** as of `pyright` 1.1.135.
-
-Both the *Pyright* [`dataclass_transform`] specification and *attrs* implementation may change in future versions.
-:::
## Types
@@ -332,5 +327,3 @@ It has the signature
>>> json.dumps(data)
'{"dt": "2020-05-04T13:37:00"}'
```
-
-[`dataclass_transform`]: https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md
diff --git a/docs/index.md b/docs/index.md
index 113e6ab..d0a473f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -67,6 +67,7 @@ init
comparison
hashing
api
+api-attr
extending
how-does-it-work
names
diff --git a/docs/init.md b/docs/init.md
index 1ddeac8..2320294 100644
--- a/docs/init.md
+++ b/docs/init.md
@@ -213,10 +213,12 @@ Traceback (most recent call last):
ValueError: 'x' has to be smaller than 'y'!
```
-This example also shows of some syntactic sugar for using the {obj}`attrs.validators.and_` validator: if you pass a list, all validators have to pass.
+This example demonstrates a convenience shortcut:
+Passing a list of validators directly is equivalent to passing them wrapped in the {obj}`attrs.validators.and_` validator and all validators must pass.
*attrs* won't intercept your changes to those attributes but you can always call {func}`attrs.validate` on any instance to verify that it's still valid:
-When using {func}`attrs.define` or [`attrs.frozen`](attrs.frozen), *attrs* will run the validators even when setting the attribute.
+
+When using {func}`attrs.define` or [`attrs.frozen`](attrs.frozen), however, *attrs* will run the validators even when setting the attribute.
```{doctest}
>>> i = C(4, 5)
@@ -241,7 +243,7 @@ TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attri
```
Of course you can mix and match the two approaches at your convenience.
-If you define validators both ways for an attribute, they are both ran:
+If you use both ways to define validators for an attribute, they are both ran:
```{doctest}
>>> @define
@@ -263,7 +265,7 @@ Traceback (most recent call last):
ValueError: value out of bounds
```
-And finally you can disable validators globally:
+Finally, validators can be globally disabled:
```{doctest}
>>> attrs.validators.set_disabled(True)
@@ -276,7 +278,7 @@ Traceback (most recent call last):
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')
```
-You can achieve the same by using the context manager:
+... or within a context manager:
```{doctest}
>>> with attrs.validators.disabled():
@@ -292,8 +294,7 @@ TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", At
## Converters
-Finally, sometimes you may want to normalize the values coming in.
-For that *attrs* comes with converters.
+Sometimes, it is necessary to normalize the values coming in, therefore *attrs* comes with converters.
Attributes can have a `converter` function specified, which will be called with the attribute's passed-in value to get a new value to use.
This can be useful for doing type-conversions on values that you don't want to force your callers to do.
@@ -350,10 +351,10 @@ A converter will override an explicit type annotation or `type` argument.
## Hooking Yourself Into Initialization
-Generally speaking, the moment you think that you need finer control over how your class is instantiated than what *attrs* offers, it's usually best to use a {obj}`classmethod` factory or to apply the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern).
+Generally speaking, the moment you realize the need of finer control – than what *attrs* offers – over how a class is instantiated, it's usually best to use a {obj}`classmethod` factory or to apply the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern).
However, sometimes you need to do that one quick thing before or after your class is initialized.
-And for that *attrs* offers three means:
+For that purpose, *attrs* offers the following options:
- `__attrs_pre_init__` is automatically detected and run *before* *attrs* starts initializing.
This is useful if you need to inject a call to `super().__init__()`.
diff --git a/docs/types.md b/docs/types.md
index b1c14db..494693b 100644
--- a/docs/types.md
+++ b/docs/types.md
@@ -73,7 +73,7 @@ class SomeClass:
## Pyright
-*attrs* provides support for [*Pyright*] though the [`dataclass_transform`] specification.
+*attrs* provides support for [*Pyright*] through the `dataclass_transform` / {pep}`681` specification.
This provides static type inference for a subset of *attrs* equivalent to standard-library {mod}`dataclasses`,
and requires explicit type annotations using the {func}`attrs.define` or `@attr.s(auto_attribs=True)` API.
@@ -94,13 +94,10 @@ The *Pyright* inferred types are a tiny subset of those supported by *Mypy*, inc
- The `attrs.frozen` decorator is not typed with frozen attributes, which are properly typed via `attrs.define(frozen=True)`.
- A [full list](https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md#attrs) of limitations and incompatibilities can be found in *Pyright*'s repository.
-
Your constructive feedback is welcome in both [attrs#795](https://github.com/python-attrs/attrs/issues/795) and [pyright#1782](https://github.com/microsoft/pyright/discussions/1782).
Generally speaking, the decision on improving *attrs* support in *Pyright* is entirely Microsoft's prerogative, though.
:::
-[`dataclass_transform`]: https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md
[*Mypy*]: http://mypy-lang.org
[*Pyright*]: https://github.com/microsoft/pyright
[*pytype*]: https://google.github.io/pytype/
diff --git a/docs/why.md b/docs/why.md
index ed896f1..6e4fde8 100644
--- a/docs/why.md
+++ b/docs/why.md
@@ -269,7 +269,7 @@ is roughly
ArtisanalClass(a=1, b=2)
```
-which is quite a mouthful and it doesn't even use any of *attrs*'s more advanced features like validators or defaults values.
+which is quite a mouthful and it doesn't even use any of *attrs*'s more advanced features like validators or default values.
Also: no tests whatsoever.
And who will guarantee you, that you don't accidentally flip the `<` in your tenth implementation of `__gt__`?
@@ -293,5 +293,5 @@ If you don't care and like typing, we're not gonna stop you.
However it takes a lot of bias and determined rationalization to claim that *attrs* raises the mental burden on a project given how difficult it is to find the important bits in a hand-written class and how annoying it is to ensure you've copy-pasted your code correctly over all your classes.
-In any case, if you ever get sick of the repetitiveness and drowning important code in a sea of boilerplate, *attrs* will be waiting for you.
+In any case, if you ever get sick of the repetitiveness and the drowning of important code in a sea of boilerplate, *attrs* will be waiting for you.
:::
diff --git a/pyproject.toml b/pyproject.toml
index 98fa365..fb8fae3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,124 @@
# SPDX-License-Identifier: MIT
[build-system]
-requires = ["setuptools>=40.6.0", "wheel"]
-build-backend = "setuptools.build_meta"
+requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"]
+build-backend = "hatchling.build"
+
+
+[project]
+name = "attrs"
+authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }]
+license = "MIT"
+requires-python = ">=3.7"
+description = "Classes Without Boilerplate"
+keywords = ["class", "attribute", "boilerplate"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "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",
+ "Typing :: Typed",
+]
+dependencies = ["importlib_metadata;python_version<'3.8'"]
+dynamic = ["version", "readme"]
+
+[project.optional-dependencies]
+tests-no-zope = [
+ # For regression test to ensure cloudpickle compat doesn't break.
+ 'cloudpickle; python_implementation == "CPython"',
+ "hypothesis",
+ "pympler",
+ # 4.3.0 dropped last use of `convert`
+ "pytest>=4.3.0",
+ "pytest-xdist[psutil]",
+ # Since the mypy error messages keep changing, we have to keep updating this
+ # pin.
+ 'mypy>=1.1.1; python_implementation == "CPython"',
+ 'pytest-mypy-plugins; python_implementation == "CPython" and python_version<"3.11"',
+]
+tests = ["attrs[tests-no-zope]", "zope.interface"]
+cov = [
+ "attrs[tests]",
+ # Ensure coverage is new enough for `source_pkgs`.
+ "coverage[toml]>=5.3",
+]
+docs = [
+ "furo",
+ "myst-parser",
+ "sphinx",
+ "zope.interface",
+ "sphinx-notfound-page",
+ "sphinxcontrib-towncrier",
+ "towncrier",
+]
+dev = ["attrs[tests,docs]", "pre-commit"]
+
+[project.urls]
+Documentation = "https://www.attrs.org/"
+Changelog = "https://www.attrs.org/en/stable/changelog.html"
+"Bug Tracker" = "https://github.com/python-attrs/attrs/issues"
+"Source Code" = "https://github.com/python-attrs/attrs"
+Funding = "https://github.com/sponsors/hynek"
+Tidelift = "https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi"
+
+
+[tool.hatch.version]
+source = "vcs"
+raw-options = { local_scheme = "no-local-version" }
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/attr", "src/attrs"]
+
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/markdown"
+
+# PyPI doesn't support the <picture> tag.
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+text = """<p align="center">
+ <a href="https://www.attrs.org/">
+ <img src="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo.svg" width="35%" alt="attrs" />
+ </a>
+</p>
+"""
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+path = "README.md"
+start-after = "<!-- teaser-begin -->"
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+text = """
+
+## Release Information
+
+"""
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+path = "CHANGELOG.md"
+pattern = "\n(###.+?\n)## "
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+text = """
+
+---
+
+[Full changelog](https://www.attrs.org/en/stable/changelog.html)
+"""
+
+
+# Make coverage play nicely with pytest-xdist.
+[tool.hatch.build.targets.wheel.hooks.autorun]
+dependencies = ["hatch-autorun"]
+code = """
+import coverage
+coverage.process_startup()
+"""
+enable-by-default = false
[tool.pytest.ini_options]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0278215..0000000
--- a/setup.py
+++ /dev/null
@@ -1,163 +0,0 @@
-# SPDX-License-Identifier: MIT
-
-import codecs
-import os
-import re
-
-from setuptools import find_packages, setup
-
-
-###############################################################################
-
-NAME = "attrs"
-PACKAGES = find_packages(where="src")
-META_PATH = os.path.join("src", "attr", "__init__.py")
-KEYWORDS = ["class", "attribute", "boilerplate", "dataclass"]
-PROJECT_URLS = {
- "Documentation": "https://www.attrs.org/",
- "Changelog": "https://www.attrs.org/en/stable/changelog.html",
- "Bug Tracker": "https://github.com/python-attrs/attrs/issues",
- "Source Code": "https://github.com/python-attrs/attrs",
- "Funding": "https://github.com/sponsors/hynek",
- "Tidelift": "https://tidelift.com/subscription/pkg/pypi-attrs?"
- "utm_source=pypi-attrs&utm_medium=pypi",
- "Ko-fi": "https://ko-fi.com/the_hynek",
-}
-CLASSIFIERS = [
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: Developers",
- "Natural Language :: English",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3",
- "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",
-]
-INSTALL_REQUIRES = []
-EXTRAS_REQUIRE = {
- "docs": [
- "furo",
- "sphinx",
- "myst-parser",
- "zope.interface",
- "sphinx-notfound-page",
- "sphinxcontrib-towncrier",
- "towncrier",
- ],
- "tests-no-zope": [
- # For regression test to ensure cloudpickle compat doesn't break.
- 'cloudpickle; python_implementation == "CPython"',
- "hypothesis",
- "pympler",
- # 4.3.0 dropped last use of `convert`
- "pytest>=4.3.0",
- # psutil extra is needed for correct core count detection.
- "pytest-xdist[psutil]",
- # Since the mypy error messages keep changing, we have to keep updating
- # this pin.
- "mypy>=0.971,<0.990; python_implementation == 'CPython'",
- "pytest-mypy-plugins; python_implementation == 'CPython' and "
- "python_version<'3.11'",
- ],
- "tests": [
- "attrs[tests-no-zope]",
- "zope.interface",
- ],
- "cov": [
- "attrs[tests]",
- "coverage-enable-subprocess",
- # Ensure coverage is new enough for `source_pkgs`.
- "coverage[toml]>=5.3",
- ],
- "dev": ["attrs[tests,docs]"],
-}
-# Don't break Paul unnecessarily just yet. C.f. #685
-EXTRAS_REQUIRE["tests_no_zope"] = EXTRAS_REQUIRE["tests-no-zope"]
-
-
-###############################################################################
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-
-def read(*parts):
- """
- Build an absolute path from *parts* and return the contents of the
- resulting file. Assume UTF-8 encoding.
- """
- with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
- return f.read()
-
-
-META_FILE = read(META_PATH)
-
-
-def find_meta(meta):
- """
- Extract __*meta*__ from META_FILE.
- """
- meta_match = re.search(
- rf"^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE, re.M
- )
- if meta_match:
- return meta_match.group(1)
- raise RuntimeError(f"Unable to find __{meta}__ string.")
-
-
-VERSION = find_meta("version")
-URL = find_meta("url")
-
-# PyPI doesn't support the <picture> tag.
-LOGO = """<p align="center">
- <a href="https://www.attrs.org/">
- <img src="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo.svg" width="35%" alt="attrs" />
- </a>
-</p>
-""" # noqa
-
-LONG = (
- LOGO
- + read("README.md").split("<!-- logo-end -->", 1)[1]
- + "\n\n## Changes in This Release\n"
- + read("CHANGELOG.md")
- .split("towncrier release notes start -->", 1)[1]
- .strip()
- .split("\n## ", 1)[0]
- .strip()
- .split("\n", 1)[1]
- + "\n\n---\n\n[Full changelog]"
- "(https://www.attrs.org/en/stable/changelog.html)\n"
-)
-
-if __name__ == "__main__":
- setup(
- name=NAME,
- description=find_meta("description"),
- license=find_meta("license"),
- url=URL,
- project_urls=PROJECT_URLS,
- version=VERSION,
- author=find_meta("author"),
- author_email=find_meta("email"),
- maintainer=find_meta("author"),
- maintainer_email=find_meta("email"),
- keywords=KEYWORDS,
- long_description=LONG,
- long_description_content_type="text/markdown",
- packages=PACKAGES,
- package_dir={"": "src"},
- python_requires=">=3.6",
- zip_safe=False,
- classifiers=CLASSIFIERS,
- install_requires=INSTALL_REQUIRES,
- extras_require=EXTRAS_REQUIRE,
- include_package_data=True,
- )
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index 0424378..7cfa792 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -1,9 +1,11 @@
# SPDX-License-Identifier: MIT
-import sys
-import warnings
+"""
+Classes Without Boilerplate
+"""
from functools import partial
+from typing import Callable
from . import converters, exceptions, filters, setters, validators
from ._cmp import cmp_using
@@ -24,30 +26,6 @@ from ._next_gen import define, field, frozen, mutable
from ._version_info import VersionInfo
-if sys.version_info < (3, 7): # pragma: no cover
- warnings.warn(
- "Running attrs on Python 3.6 is deprecated & we intend to drop "
- "support soon. If that's a problem for you, please let us know why & "
- "we MAY re-evaluate: <https://github.com/python-attrs/attrs/pull/993>",
- DeprecationWarning,
- )
-
-__version__ = "22.2.0"
-__version_info__ = VersionInfo._from_version_string(__version__)
-
-__title__ = "attrs"
-__description__ = "Classes Without Boilerplate"
-__url__ = "https://www.attrs.org/"
-__uri__ = __url__
-__doc__ = __description__ + " <" + __uri__ + ">"
-
-__author__ = "Hynek Schlawack"
-__email__ = "hs@ox.cx"
-
-__license__ = "MIT"
-__copyright__ = "Copyright (c) 2015 Hynek Schlawack"
-
-
s = attributes = attrs
ib = attr = attrib
dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)
@@ -91,3 +69,64 @@ __all__ = [
"validate",
"validators",
]
+
+
+def _make_getattr(mod_name: str) -> Callable:
+ """
+ Create a metadata proxy for packaging information that uses *mod_name* in
+ its warnings and errors.
+ """
+
+ def __getattr__(name: str) -> str:
+ dunder_to_metadata = {
+ "__title__": "Name",
+ "__copyright__": "",
+ "__version__": "version",
+ "__version_info__": "version",
+ "__description__": "summary",
+ "__uri__": "",
+ "__url__": "",
+ "__author__": "",
+ "__email__": "",
+ "__license__": "license",
+ }
+ if name not in dunder_to_metadata.keys():
+ raise AttributeError(f"module {mod_name} has no attribute {name}")
+
+ import sys
+ import warnings
+
+ if sys.version_info < (3, 8):
+ from importlib_metadata import metadata
+ else:
+ from importlib.metadata import metadata
+
+ if name != "__version_info__":
+ warnings.warn(
+ f"Accessing {mod_name}.{name} is deprecated and will be "
+ "removed in a future release. Use importlib.metadata directly "
+ "to query for attrs's packaging metadata.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ meta = metadata("attrs")
+ if name == "__license__":
+ return "MIT"
+ elif name == "__copyright__":
+ return "Copyright (c) 2015 Hynek Schlawack"
+ elif name in ("__uri__", "__url__"):
+ return meta["Project-URL"].split(" ", 1)[-1]
+ elif name == "__version_info__":
+ return VersionInfo._from_version_string(meta["version"])
+ elif name == "__author__":
+ return meta["Author-email"].rsplit(" ", 1)[0]
+ elif name == "__email__":
+ return meta["Author-email"].rsplit("<", 1)[1][:-1]
+
+ return meta[dunder_to_metadata[name]]
+
+ return __getattr__
+
+
+__getattr__ = _make_getattr(__name__)
diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index 42a2ee2..ced5a3f 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -69,6 +69,7 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]
class AttrsInstance(AttrsInstance_, Protocol):
pass
+_A = TypeVar("_A", bound=AttrsInstance)
# _make --
class _Nothing(enum.Enum):
@@ -116,6 +117,7 @@ def __dataclass_transform__(
eq_default: bool = True,
order_default: bool = False,
kw_only_default: bool = False,
+ frozen_default: bool = False,
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
) -> Callable[[_T], _T]: ...
@@ -257,6 +259,7 @@ def field(
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
+ type: Optional[type] = ...,
) -> Any: ...
# This form catches an explicit None or no default and infers the type from the
@@ -277,6 +280,7 @@ def field(
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
+ type: Optional[type] = ...,
) -> _T: ...
# This form catches an explicit default argument.
@@ -296,6 +300,7 @@ def field(
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
+ type: Optional[type] = ...,
) -> _T: ...
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -315,6 +320,7 @@ def field(
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
+ type: Optional[type] = ...,
) -> Any: ...
@overload
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
@@ -426,17 +432,73 @@ def define(
) -> Callable[[_C], _C]: ...
mutable = define
-frozen = define # they differ only in their defaults
+@overload
+@__dataclass_transform__(
+ frozen_default=True, field_descriptors=(attrib, field)
+)
+def frozen(
+ maybe_cls: _C,
+ *,
+ these: Optional[Dict[str, Any]] = ...,
+ repr: bool = ...,
+ unsafe_hash: Optional[bool] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ slots: bool = ...,
+ frozen: bool = ...,
+ weakref_slot: bool = ...,
+ str: bool = ...,
+ auto_attribs: bool = ...,
+ kw_only: bool = ...,
+ cache_hash: bool = ...,
+ auto_exc: bool = ...,
+ eq: Optional[bool] = ...,
+ order: Optional[bool] = ...,
+ auto_detect: bool = ...,
+ getstate_setstate: Optional[bool] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+ field_transformer: Optional[_FieldTransformer] = ...,
+ match_args: bool = ...,
+) -> _C: ...
+@overload
+@__dataclass_transform__(
+ frozen_default=True, field_descriptors=(attrib, field)
+)
+def frozen(
+ maybe_cls: None = ...,
+ *,
+ these: Optional[Dict[str, Any]] = ...,
+ repr: bool = ...,
+ unsafe_hash: Optional[bool] = ...,
+ hash: Optional[bool] = ...,
+ init: bool = ...,
+ slots: bool = ...,
+ frozen: bool = ...,
+ weakref_slot: bool = ...,
+ str: bool = ...,
+ auto_attribs: bool = ...,
+ kw_only: bool = ...,
+ cache_hash: bool = ...,
+ auto_exc: bool = ...,
+ eq: Optional[bool] = ...,
+ order: Optional[bool] = ...,
+ auto_detect: bool = ...,
+ getstate_setstate: Optional[bool] = ...,
+ on_setattr: Optional[_OnSetAttrArgType] = ...,
+ field_transformer: Optional[_FieldTransformer] = ...,
+ match_args: bool = ...,
+) -> Callable[[_C], _C]: ...
def fields(cls: Type[AttrsInstance]) -> Any: ...
def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ...
def resolve_types(
- cls: _C,
+ cls: _A,
globalns: Optional[Dict[str, Any]] = ...,
localns: Optional[Dict[str, Any]] = ...,
attribs: Optional[List[Attribute[Any]]] = ...,
-) -> _C: ...
+ include_extras: bool = ...,
+) -> _A: ...
# TODO: add support for returning a proper attrs class from the mypy plugin
# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',
diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py
index ad1e18c..d9cbe22 100644
--- a/src/attr/_cmp.py
+++ b/src/attr/_cmp.py
@@ -20,22 +20,22 @@ def cmp_using(
class_name="Comparable",
):
"""
- Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and
- ``cmp`` arguments to customize field comparison.
-
- The resulting class will have a full set of ordering methods if
- at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided.
-
- :param Optional[callable] eq: `callable` used to evaluate equality
- of two objects.
- :param Optional[callable] lt: `callable` used to evaluate whether
- one object is less than another object.
- :param Optional[callable] le: `callable` used to evaluate whether
- one object is less than or equal to another object.
- :param Optional[callable] gt: `callable` used to evaluate whether
- one object is greater than another object.
- :param Optional[callable] ge: `callable` used to evaluate whether
- one object is greater than or equal to another object.
+ Create a class that can be passed into `attrs.field`'s ``eq``, ``order``,
+ and ``cmp`` arguments to customize field comparison.
+
+ The resulting class will have a full set of ordering methods if at least
+ one of ``{lt, le, gt, ge}`` and ``eq`` are provided.
+
+ :param Optional[callable] eq: `callable` used to evaluate equality of two
+ objects.
+ :param Optional[callable] lt: `callable` used to evaluate whether one
+ object is less than another object.
+ :param Optional[callable] le: `callable` used to evaluate whether one
+ object is less than or equal to another object.
+ :param Optional[callable] gt: `callable` used to evaluate whether one
+ object is greater than another object.
+ :param Optional[callable] ge: `callable` used to evaluate whether one
+ object is greater than or equal to another object.
:param bool require_same_type: When `True`, equality and ordering methods
will return `NotImplemented` if objects are not of the same type.
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
index 35a85a3..c3bf5e3 100644
--- a/src/attr/_compat.py
+++ b/src/attr/_compat.py
@@ -9,9 +9,11 @@ import types
import warnings
from collections.abc import Mapping, Sequence # noqa
+from typing import _GenericAlias
PYPY = platform.python_implementation() == "PyPy"
+PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
PY310 = sys.version_info[:2] >= (3, 10)
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
@@ -81,32 +83,32 @@ def make_set_closure_cell():
# Otherwise gotta do it the hard way.
- # Create a function that will set its first cellvar to `value`.
- def set_first_cellvar_to(value):
- x = value
- return
-
- # This function will be eliminated as dead code, but
- # not before its reference to `x` forces `x` to be
- # represented as a closure cell rather than a local.
- def force_x_to_be_a_cell(): # pragma: no cover
- return x
-
try:
- # Extract the code object and make sure our assumptions about
- # the closure behavior are correct.
- co = set_first_cellvar_to.__code__
- if co.co_cellvars != ("x",) or co.co_freevars != ():
- raise AssertionError # 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):
def set_closure_cell(cell, value):
cell.cell_contents = value
else:
+ # Create a function that will set its first cellvar to `value`.
+ def set_first_cellvar_to(value):
+ x = value
+ return
+
+ # This function will be eliminated as dead code, but
+ # not before its reference to `x` forces `x` to be
+ # represented as a closure cell rather than a local.
+ def force_x_to_be_a_cell(): # pragma: no cover
+ return x
+
+ # Extract the code object and make sure our assumptions about
+ # the closure behavior are correct.
+ co = set_first_cellvar_to.__code__
+ if co.co_cellvars != ("x",) or co.co_freevars != ():
+ raise AssertionError # pragma: no cover
+
+ # Convert this code object to a code object that sets the
+ # function's first _freevar_ (not cellvar) to the argument.
args = [co.co_argcount]
args.append(co.co_kwonlyargcount)
args.extend(
@@ -174,3 +176,10 @@ set_closure_cell = make_set_closure_cell()
# don't have a direct reference to the thread-local in their globals dict.
# If they have such a reference, it breaks cloudpickle.
repr_context = threading.local()
+
+
+def get_generic_base(cl):
+ """If this is a generic class (A[str]), return the generic base for it."""
+ if cl.__class__ is _GenericAlias:
+ return cl.__origin__
+ return None
diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
index 1f573c1..7f5d961 100644
--- a/src/attr/_funcs.py
+++ b/src/attr/_funcs.py
@@ -3,6 +3,7 @@
import copy
+from ._compat import PY_3_9_PLUS, get_generic_base
from ._make import NOTHING, _obj_setattr, fields
from .exceptions import AttrsAttributeNotFoundError
@@ -16,13 +17,13 @@ def asdict(
value_serializer=None,
):
"""
- Return the ``attrs`` attribute values of *inst* as a dict.
+ Return the *attrs* attribute values of *inst* as a dict.
- Optionally recurse into other ``attrs``-decorated classes.
+ Optionally recurse into other *attrs*-decorated classes.
- :param inst: Instance of an ``attrs``-decorated class.
+ :param inst: Instance of an *attrs*-decorated class.
:param bool recurse: Recurse into classes that are also
- ``attrs``-decorated.
+ *attrs*-decorated.
:param callable filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the `attrs.Attribute` as the first argument and the
@@ -40,7 +41,7 @@ def asdict(
:rtype: return type of *dict_factory*
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
.. versionadded:: 16.0.0 *dict_factory*
@@ -195,13 +196,13 @@ def astuple(
retain_collection_types=False,
):
"""
- Return the ``attrs`` attribute values of *inst* as a tuple.
+ Return the *attrs* attribute values of *inst* as a tuple.
- Optionally recurse into other ``attrs``-decorated classes.
+ Optionally recurse into other *attrs*-decorated classes.
- :param inst: Instance of an ``attrs``-decorated class.
+ :param inst: Instance of an *attrs*-decorated class.
:param bool recurse: Recurse into classes that are also
- ``attrs``-decorated.
+ *attrs*-decorated.
:param callable filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the `attrs.Attribute` as the first argument and the
@@ -215,7 +216,7 @@ def astuple(
:rtype: return type of *tuple_factory*
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
.. versionadded:: 16.2.0
@@ -289,28 +290,48 @@ def astuple(
def has(cls):
"""
- Check whether *cls* is a class with ``attrs`` attributes.
+ Check whether *cls* is a class with *attrs* attributes.
:param type cls: Class to introspect.
:raise TypeError: If *cls* is not a class.
:rtype: bool
"""
- return getattr(cls, "__attrs_attrs__", None) is not None
+ attrs = getattr(cls, "__attrs_attrs__", None)
+ if attrs is not None:
+ return True
+
+ # No attrs, maybe it's a specialized generic (A[str])?
+ generic_base = get_generic_base(cls)
+ if generic_base is not None:
+ generic_attrs = getattr(generic_base, "__attrs_attrs__", None)
+ if generic_attrs is not None:
+ # Stick it on here for speed next time.
+ cls.__attrs_attrs__ = generic_attrs
+ return generic_attrs is not None
+ return False
def assoc(inst, **changes):
"""
Copy *inst* and apply *changes*.
- :param inst: Instance of a class with ``attrs`` attributes.
+ This is different from `evolve` that applies the changes to the arguments
+ that create the new instance.
+
+ `evolve`'s behavior is preferable, but there are `edge cases`_ where it
+ doesn't work. Therefore `assoc` is deprecated, but will not be removed.
+
+ .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251
+
+ :param inst: Instance of a class with *attrs* attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
- :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't
- be found on *cls*.
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name*
+ couldn't be found on *cls*.
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
.. deprecated:: 17.1.0
@@ -318,13 +339,6 @@ def assoc(inst, **changes):
This function will not be removed du to the slightly different approach
compared to `attrs.evolve`.
"""
- import warnings
-
- warnings.warn(
- "assoc is deprecated and will be removed after 2018/01.",
- DeprecationWarning,
- stacklevel=2,
- )
new = copy.copy(inst)
attrs = fields(inst.__class__)
for k, v in changes.items():
@@ -337,22 +351,55 @@ def assoc(inst, **changes):
return new
-def evolve(inst, **changes):
+def evolve(*args, **changes):
"""
- Create a new instance, based on *inst* with *changes* applied.
+ Create a new instance, based on the first positional argument with
+ *changes* applied.
- :param inst: Instance of a class with ``attrs`` attributes.
+ :param inst: Instance of a class with *attrs* attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
:raise TypeError: If *attr_name* couldn't be found in the class
``__init__``.
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
- .. versionadded:: 17.1.0
+ .. versionadded:: 17.1.0
+ .. deprecated:: 23.1.0
+ It is now deprecated to pass the instance using the keyword argument
+ *inst*. It will raise a warning until at least April 2024, after which
+ it will become an error. Always pass the instance as a positional
+ argument.
"""
+ # Try to get instance by positional argument first.
+ # Use changes otherwise and warn it'll break.
+ if args:
+ try:
+ (inst,) = args
+ except ValueError:
+ raise TypeError(
+ f"evolve() takes 1 positional argument, but {len(args)} "
+ "were given"
+ ) from None
+ else:
+ try:
+ inst = changes.pop("inst")
+ except KeyError:
+ raise TypeError(
+ "evolve() missing 1 required positional argument: 'inst'"
+ ) from None
+
+ import warnings
+
+ warnings.warn(
+ "Passing the instance per keyword argument is deprecated and "
+ "will stop working in, or after, April 2024.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
cls = inst.__class__
attrs = fields(cls)
for a in attrs:
@@ -366,7 +413,9 @@ def evolve(inst, **changes):
return cls(**changes)
-def resolve_types(cls, globalns=None, localns=None, attribs=None):
+def resolve_types(
+ cls, globalns=None, localns=None, attribs=None, include_extras=True
+):
"""
Resolve any strings and forward annotations in type annotations.
@@ -385,10 +434,14 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None):
:param Optional[dict] localns: Dictionary containing local variables.
:param Optional[list] attribs: List of attribs for the given class.
This is necessary when calling from inside a ``field_transformer``
- since *cls* is not an ``attrs`` class yet.
+ since *cls* is not an *attrs* class yet.
+ :param bool include_extras: Resolve more accurately, if possible.
+ Pass ``include_extras`` to ``typing.get_hints``, if supported by the
+ typing module. On supported Python versions (3.9+), this resolves the
+ types more accurately.
:raise TypeError: If *cls* is not a class.
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class and you didn't pass any attribs.
:raise NameError: If types cannot be resolved because of missing variables.
@@ -398,6 +451,7 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None):
.. versionadded:: 20.1.0
.. versionadded:: 21.1.0 *attribs*
+ .. versionadded:: 23.1.0 *include_extras*
"""
# Since calling get_type_hints is expensive we cache whether we've
@@ -405,7 +459,12 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None):
if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing
- hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
+ kwargs = {"globalns": globalns, "localns": localns}
+
+ if PY_3_9_PLUS:
+ kwargs["include_extras"] = include_extras
+
+ hints = typing.get_type_hints(cls, **kwargs)
for field in fields(cls) if attribs is None else attribs:
if field.name in hints:
# Since fields have been frozen we must work around it.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 9ee2200..d72f738 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -12,7 +12,12 @@ from operator import itemgetter
# 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 PY310, PYPY, _AnnotationExtractor, set_closure_cell
+from ._compat import (
+ PY310,
+ _AnnotationExtractor,
+ get_generic_base,
+ set_closure_cell,
+)
from .exceptions import (
DefaultAlreadySetError,
FrozenInstanceError,
@@ -109,9 +114,12 @@ def attrib(
.. warning::
Does *not* do anything unless the class is also decorated with
- `attr.s`!
+ `attr.s` / `attrs.define` / et cetera!
+
+ Please consider using `attrs.field` in new code (``attr.ib`` will *never*
+ go away, though).
- :param default: A value that is used if an ``attrs``-generated ``__init__``
+ :param default: A value that is used if an *attrs*-generated ``__init__``
is used and no value is passed while instantiating or the attribute is
excluded using ``init=False``.
@@ -130,7 +138,7 @@ def attrib(
:param callable factory: Syntactic sugar for
``default=attr.Factory(factory)``.
- :param validator: `callable` that is called by ``attrs``-generated
+ :param validator: `callable` that is called by *attrs*-generated
``__init__`` methods after the instance has been initialized. They
receive the initialized instance, the :func:`~attrs.Attribute`, and the
passed value.
@@ -142,7 +150,7 @@ def attrib(
all pass.
Validators can be globally disabled and re-enabled using
- `get_run_validators`.
+ `attrs.validators.get_disabled` / `attrs.validators.set_disabled`.
The validator can also be set using decorator notation as shown below.
@@ -184,7 +192,7 @@ def attrib(
value. In that case this attributed is unconditionally initialized
with the specified default value or factory.
:param callable converter: `callable` that is called by
- ``attrs``-generated ``__init__`` methods to convert attribute's value
+ *attrs*-generated ``__init__`` methods to convert attribute's value
to the desired format. It is given the passed-in value, and the
returned value will be used as the new value of the attribute. The
value is converted before being passed to the validator, if any.
@@ -197,7 +205,7 @@ def attrib(
Regardless of the approach used, the type will be stored on
``Attribute.type``.
- Please note that ``attrs`` doesn't do anything with this metadata by
+ Please note that *attrs* doesn't do anything with this metadata by
itself. You can use it as part of your own code or for
`static type checking <types>`.
:param kw_only: Make this attribute keyword-only in the generated
@@ -582,28 +590,19 @@ def _transform_attrs(
return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map))
-if PYPY:
-
- def _frozen_setattrs(self, name, value):
- """
- Attached to frozen classes as __setattr__.
- """
- if isinstance(self, BaseException) and name in (
- "__cause__",
- "__context__",
- ):
- BaseException.__setattr__(self, name, value)
- return
-
- raise FrozenInstanceError()
-
-else:
+def _frozen_setattrs(self, name, value):
+ """
+ Attached to frozen classes as __setattr__.
+ """
+ if isinstance(self, BaseException) and name in (
+ "__cause__",
+ "__context__",
+ "__traceback__",
+ ):
+ BaseException.__setattr__(self, name, value)
+ return
- def _frozen_setattrs(self, name, value):
- """
- Attached to frozen classes as __setattr__.
- """
- raise FrozenInstanceError()
+ raise FrozenInstanceError()
def _frozen_delattrs(self, name):
@@ -940,9 +939,15 @@ class _ClassBuilder:
Automatically created by attrs.
"""
__bound_setattr = _obj_setattr.__get__(self)
- for name in state_attr_names:
- if name in state:
- __bound_setattr(name, state[name])
+ if isinstance(state, tuple):
+ # Backward compatibility with attrs instances pickled with
+ # attrs versions before v22.2.0 which stored tuples.
+ for name, value in zip(state_attr_names, state):
+ __bound_setattr(name, value)
+ else:
+ for name in state_attr_names:
+ if name in state:
+ __bound_setattr(name, state[name])
# The hash code cache is not included when the object is
# serialized, but it still needs to be initialized to None to
@@ -1220,12 +1225,15 @@ def attrs(
A class decorator that adds :term:`dunder methods` according to the
specified attributes using `attr.ib` or the *these* argument.
+ Please consider using `attrs.define` / `attrs.frozen` in new code
+ (``attr.s`` will *never* go away, though).
+
:param these: A dictionary of name to `attr.ib` mappings. This is
useful to avoid the definition of your attributes within the class body
because you can't (e.g. if you want to add ``__repr__`` methods to
Django models) or don't want to.
- If *these* is not ``None``, ``attrs`` will *not* search the class body
+ If *these* is not ``None``, *attrs* will *not* search the class body
for attributes and will *not* remove any attributes from it.
The order is deduced from the order of the attributes inside *these*.
@@ -1242,14 +1250,14 @@ def attrs(
inherited from some base class).
So for example by implementing ``__eq__`` on a class yourself,
- ``attrs`` will deduce ``eq=False`` and will create *neither*
+ *attrs* will deduce ``eq=False`` and will create *neither*
``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible
``__ne__`` by default, so it *should* be enough to only implement
``__eq__`` in most cases).
.. warning::
- If you prevent ``attrs`` from creating the ordering methods for you
+ If you prevent *attrs* from creating the ordering methods for you
(``order=False``, e.g. by implementing ``__le__``), it becomes
*your* responsibility to make sure its ordering is sound. The best
way is to use the `functools.total_ordering` decorator.
@@ -1259,14 +1267,14 @@ def attrs(
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
:param bool repr: Create a ``__repr__`` method with a human readable
- representation of ``attrs`` attributes..
+ representation of *attrs* attributes..
:param bool str: Create a ``__str__`` method that is identical to
``__repr__``. This is usually not necessary except for
`Exception`\ s.
:param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__``
and ``__ne__`` methods that check two instances for equality.
- They compare the instances as if they were tuples of their ``attrs``
+ They compare the instances as if they were tuples of their *attrs*
attributes if and only if the types of both classes are *identical*!
:param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``,
``__gt__``, and ``__ge__`` methods that behave like *eq* above and
@@ -1277,7 +1285,7 @@ def attrs(
:param Optional[bool] unsafe_hash: If ``None`` (default), the ``__hash__``
method is generated according how *eq* and *frozen* are set.
- 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you.
+ 1. If *both* are True, *attrs* will generate a ``__hash__`` for you.
2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to
None, marking it unhashable (which it is).
3. If *eq* is False, ``__hash__`` will be left untouched meaning the
@@ -1285,7 +1293,7 @@ def attrs(
``object``, this means it will fall back to id-based hashing.).
Although not recommended, you can decide for yourself and force
- ``attrs`` to create one (e.g. if the class is immutable even though you
+ *attrs* to create one (e.g. if the class is immutable even though you
didn't freeze it programmatically) by passing ``True`` or not. Both of
these cases are rather special and should be used carefully.
@@ -1296,7 +1304,7 @@ def attrs(
:param Optional[bool] hash: Alias for *unsafe_hash*. *unsafe_hash* takes
precedence.
:param bool init: Create a ``__init__`` method that initializes the
- ``attrs`` attributes. Leading underscores are stripped for the argument
+ *attrs* attributes. Leading underscores are stripped for the argument
name. If a ``__attrs_pre_init__`` method exists on the class, it will
be called before the class is initialized. If a ``__attrs_post_init__``
method exists on the class, it will be called after the class is fully
@@ -1312,7 +1320,7 @@ def attrs(
we encourage you to read the :term:`glossary entry <slotted classes>`.
:param bool frozen: Make instances immutable after initialization. If
someone attempts to modify a frozen instance,
- `attr.exceptions.FrozenInstanceError` is raised.
+ `attrs.exceptions.FrozenInstanceError` is raised.
.. note::
@@ -1337,7 +1345,7 @@ def attrs(
:param bool auto_attribs: If ``True``, collect :pep:`526`-annotated
attributes from the class body.
- In this case, you **must** annotate every field. If ``attrs``
+ In this case, you **must** annotate every field. If *attrs*
encounters a field that is set to an `attr.ib` but lacks a type
annotation, an `attr.exceptions.UnannotatedAttributeError` is
raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't
@@ -1353,9 +1361,9 @@ def attrs(
.. warning::
For features that use the attribute name to create decorators (e.g.
- `validators <validators>`), you still *must* assign `attr.ib` to
- them. Otherwise Python will either not find the name or try to use
- the default value to call e.g. ``validator`` on it.
+ :ref:`validators <validators>`), you still *must* assign `attr.ib`
+ to them. Otherwise Python will either not find the name or try to
+ use the default value to call e.g. ``validator`` on it.
These errors can be quite confusing and probably the most common bug
report on our bug tracker.
@@ -1376,14 +1384,14 @@ def attrs(
class:
- the values for *eq*, *order*, and *hash* are ignored and the
- instances compare and hash by the instance's ids (N.B. ``attrs`` will
+ instances compare and hash by the instance's ids (N.B. *attrs* will
*not* remove existing implementations of ``__hash__`` or the equality
methods. It just won't add own ones.),
- all attributes that are either passed into ``__init__`` or have a
default value are additionally available as a tuple in the ``args``
attribute,
- the value of *str* is ignored leaving ``__str__`` to base classes.
- :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs``
+ :param bool collect_by_mro: Setting this to `True` fixes the way *attrs*
collects attributes from base classes. The default behavior is
incorrect in certain cases of multiple inheritance. It should be on by
default but is kept off for backward-compatibility.
@@ -1422,7 +1430,7 @@ def attrs(
:param Optional[callable] field_transformer:
A function that is called with the original class object and all
- fields right before ``attrs`` finalizes the class. You can use
+ fields right before *attrs* finalizes the class. You can use
this, e.g., to automatically add converters or validators to
fields based on their types. See `transform-fields` for more details.
@@ -1900,7 +1908,7 @@ def _add_repr(cls, ns=None, attrs=None):
def fields(cls):
"""
- Return the tuple of ``attrs`` attributes for a class.
+ Return the tuple of *attrs* attributes for a class.
The tuple also allows accessing the fields by their names (see below for
examples).
@@ -1908,31 +1916,45 @@ def fields(cls):
:param type cls: Class to introspect.
:raise TypeError: If *cls* is not a class.
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
:rtype: tuple (with name accessors) of `attrs.Attribute`
- .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
- by name.
+ .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
+ by name.
+ .. versionchanged:: 23.1.0 Add support for generic classes.
"""
- if not isinstance(cls, type):
+ generic_base = get_generic_base(cls)
+
+ if generic_base is None and not isinstance(cls, type):
raise TypeError("Passed object must be a class.")
+
attrs = getattr(cls, "__attrs_attrs__", None)
+
if attrs is None:
+ if generic_base is not None:
+ attrs = getattr(generic_base, "__attrs_attrs__", None)
+ if attrs is not None:
+ # Even though this is global state, stick it on here to speed
+ # it up. We rely on `cls` being cached for this to be
+ # efficient.
+ cls.__attrs_attrs__ = attrs
+ return attrs
raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.")
+
return attrs
def fields_dict(cls):
"""
- Return an ordered dictionary of ``attrs`` attributes for a class, whose
+ Return an ordered dictionary of *attrs* attributes for a class, whose
keys are the attribute names.
:param type cls: Class to introspect.
:raise TypeError: If *cls* is not a class.
- :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
+ :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
:rtype: dict
@@ -1953,7 +1975,7 @@ def validate(inst):
Leaves all exceptions through.
- :param inst: Instance of a class with ``attrs`` attributes.
+ :param inst: Instance of a class with *attrs* attributes.
"""
if _config._run_validators is False:
return
@@ -2391,6 +2413,10 @@ class Attribute:
"""
*Read-only* representation of an attribute.
+ .. warning::
+
+ You should never instantiate this class yourself.
+
The class has *all* arguments of `attr.ib` (except for ``factory``
which is only syntactic sugar for ``default=Factory(...)`` plus the
following:
@@ -2536,13 +2562,13 @@ class Attribute:
**inst_dict,
)
- # Don't use attr.evolve since fields(Attribute) doesn't work
+ # Don't use attrs.evolve since fields(Attribute) doesn't work
def evolve(self, **changes):
"""
Copy *self* and apply *changes*.
- This works similarly to `attr.evolve` but that function does not work
- with ``Attribute``.
+ This works similarly to `attrs.evolve` but that function does not work
+ with `Attribute`.
It is mainly meant to be used for `transform-fields`.
@@ -2777,10 +2803,6 @@ class Factory:
__slots__ = ("factory", "takes_self")
def __init__(self, factory, takes_self=False):
- """
- `Factory` is part of the default machinery so if we want a default
- value here, we have to implement it ourselves.
- """
self.factory = factory
self.takes_self = takes_self
@@ -2818,13 +2840,13 @@ Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f)
def make_class(name, attrs, bases=(object,), **attributes_arguments):
- """
+ r"""
A quick way to create a new class called *name* with *attrs*.
:param str name: The name for the new class.
:param attrs: A list of names or a dictionary of mappings of names to
- attributes.
+ `attr.ib`\ s / `attrs.field`\ s.
The order is deduced from the order of the names or attributes inside
*attrs*. Otherwise the order of the definition of the attributes is
diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py
index c59d848..8f7c0b9 100644
--- a/src/attr/_next_gen.py
+++ b/src/attr/_next_gen.py
@@ -46,7 +46,7 @@ def define(
match_args=True,
):
r"""
- Define an ``attrs`` class.
+ Define an *attrs* class.
Differences to the classic `attr.s` that it uses underneath:
@@ -167,6 +167,7 @@ def field(
hash=None,
init=True,
metadata=None,
+ type=None,
converter=None,
factory=None,
kw_only=False,
@@ -179,6 +180,10 @@ def field(
Identical to `attr.ib`, except keyword-only and with some arguments
removed.
+ .. versionadded:: 23.1.0
+ The *type* parameter has been re-added; mostly for
+ {func}`attrs.make_class`. Please note that type checkers ignore this
+ metadata.
.. versionadded:: 20.1.0
"""
return attrib(
@@ -188,6 +193,7 @@ def field(
hash=hash,
init=init,
metadata=metadata,
+ type=type,
converter=converter,
factory=factory,
kw_only=kw_only,
diff --git a/src/attr/exceptions.py b/src/attr/exceptions.py
index 5dc51e0..2883493 100644
--- a/src/attr/exceptions.py
+++ b/src/attr/exceptions.py
@@ -34,7 +34,7 @@ class FrozenAttributeError(FrozenError):
class AttrsAttributeNotFoundError(ValueError):
"""
- An ``attrs`` function couldn't find an attribute that the user asked for.
+ An *attrs* function couldn't find an attribute that the user asked for.
.. versionadded:: 16.2.0
"""
@@ -42,7 +42,7 @@ class AttrsAttributeNotFoundError(ValueError):
class NotAnAttrsClassError(ValueError):
"""
- A non-``attrs`` class has been passed into an ``attrs`` function.
+ A non-*attrs* class has been passed into an *attrs* function.
.. versionadded:: 16.2.0
"""
@@ -50,7 +50,7 @@ class NotAnAttrsClassError(ValueError):
class DefaultAlreadySetError(RuntimeError):
"""
- A default has been set using ``attr.ib()`` and is attempted to be reset
+ A default has been set when defining the field and is attempted to be reset
using the decorator.
.. versionadded:: 17.1.0
@@ -59,8 +59,7 @@ class DefaultAlreadySetError(RuntimeError):
class UnannotatedAttributeError(RuntimeError):
"""
- A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type
- annotation.
+ A class with ``auto_attribs=True`` has a field without a type annotation.
.. versionadded:: 17.3.0
"""
@@ -68,7 +67,7 @@ class UnannotatedAttributeError(RuntimeError):
class PythonTooOldError(RuntimeError):
"""
- It was attempted to use an ``attrs`` feature that requires a newer Python
+ It was attempted to use an *attrs* feature that requires a newer Python
version.
.. versionadded:: 18.2.0
@@ -77,8 +76,8 @@ class PythonTooOldError(RuntimeError):
class NotCallableError(TypeError):
"""
- A ``attr.ib()`` requiring a callable has been set with a value
- that is not callable.
+ A field requiring a callable has been set with a value that is not
+ callable.
.. versionadded:: 19.2.0
"""
diff --git a/src/attr/filters.py b/src/attr/filters.py
index baa25e9..a1e40c9 100644
--- a/src/attr/filters.py
+++ b/src/attr/filters.py
@@ -13,6 +13,7 @@ def _split_what(what):
"""
return (
frozenset(cls for cls in what if isinstance(cls, type)),
+ frozenset(cls for cls in what if isinstance(cls, str)),
frozenset(cls for cls in what if isinstance(cls, Attribute)),
)
@@ -22,14 +23,21 @@ def include(*what):
Include *what*.
:param what: What to include.
- :type what: `list` of `type` or `attrs.Attribute`\\ s
+ :type what: `list` of classes `type`, field names `str` or
+ `attrs.Attribute`\\ s
:rtype: `callable`
+
+ .. versionchanged:: 23.1.0 Accept strings with field names.
"""
- cls, attrs = _split_what(what)
+ cls, names, attrs = _split_what(what)
def include_(attribute, value):
- return value.__class__ in cls or attribute in attrs
+ return (
+ value.__class__ in cls
+ or attribute.name in names
+ or attribute in attrs
+ )
return include_
@@ -39,13 +47,20 @@ def exclude(*what):
Exclude *what*.
:param what: What to exclude.
- :type what: `list` of classes or `attrs.Attribute`\\ s.
+ :type what: `list` of classes `type`, field names `str` or
+ `attrs.Attribute`\\ s.
:rtype: `callable`
+
+ .. versionchanged:: 23.3.0 Accept field name string as input argument
"""
- cls, attrs = _split_what(what)
+ cls, names, attrs = _split_what(what)
def exclude_(attribute, value):
- return value.__class__ not in cls and attribute not in attrs
+ return not (
+ value.__class__ in cls
+ or attribute.name in names
+ or attribute in attrs
+ )
return exclude_
diff --git a/src/attr/filters.pyi b/src/attr/filters.pyi
index 9938668..8a02fa0 100644
--- a/src/attr/filters.pyi
+++ b/src/attr/filters.pyi
@@ -2,5 +2,5 @@ from typing import Any, Union
from . import Attribute, _FilterType
-def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...
-def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...
+def include(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ...
+def exclude(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ...
diff --git a/src/attr/validators.py b/src/attr/validators.py
index 852ae96..1488554 100644
--- a/src/attr/validators.py
+++ b/src/attr/validators.py
@@ -9,6 +9,7 @@ import operator
import re
from contextlib import contextmanager
+from re import Pattern
from ._config import get_run_validators, set_run_validators
from ._make import _AndValidator, and_, attrib, attrs
@@ -16,12 +17,6 @@ from .converters import default_if_none
from .exceptions import NotCallableError
-try:
- Pattern = re.Pattern
-except AttributeError: # Python <3.7 lacks a Pattern type.
- Pattern = type(re.compile(""))
-
-
__all__ = [
"and_",
"deep_iterable",
@@ -249,7 +244,17 @@ def provides(interface):
:raises TypeError: With a human readable error message, the attribute
(of type `attrs.Attribute`), the expected interface, and the
value it got.
+
+ .. deprecated:: 23.1.0
"""
+ import warnings
+
+ warnings.warn(
+ "attrs's zope-interface support is deprecated and will be removed in, "
+ "or after, April 2024.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return _ProvidesValidator(interface)
@@ -275,15 +280,16 @@ def optional(validator):
which can be set to ``None`` in addition to satisfying the requirements of
the sub-validator.
- :param validator: A validator (or a list of validators) that is used for
- non-``None`` values.
- :type validator: callable or `list` of callables.
+ :param Callable | tuple[Callable] | list[Callable] validator: A validator
+ (or validators) that is used for non-``None`` values.
.. versionadded:: 15.1.0
.. versionchanged:: 17.1.0 *validator* can be a list of validators.
+ .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators.
"""
- if isinstance(validator, list):
+ if isinstance(validator, (list, tuple)):
return _OptionalValidator(_AndValidator(validator))
+
return _OptionalValidator(validator)
@@ -359,13 +365,13 @@ class _IsCallableValidator:
def is_callable():
"""
- A validator that raises a `attr.exceptions.NotCallableError` if the
+ A validator that raises a `attrs.exceptions.NotCallableError` if the
initializer is called with a value for this particular attribute
that is not callable.
.. versionadded:: 19.1.0
- :raises `attr.exceptions.NotCallableError`: With a human readable error
+ :raises attrs.exceptions.NotCallableError: With a human readable error
message containing the attribute (`attrs.Attribute`) name,
and the value it got.
"""
diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi
index fd9206d..d194a75 100644
--- a/src/attr/validators.pyi
+++ b/src/attr/validators.pyi
@@ -51,7 +51,9 @@ def instance_of(
def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ...
def provides(interface: Any) -> _ValidatorType[Any]: ...
def optional(
- validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]
+ validator: Union[
+ _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]]
+ ]
) -> _ValidatorType[Optional[_T]]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
@@ -82,5 +84,5 @@ def not_(
validator: _ValidatorType[_T],
*,
msg: Optional[str] = None,
- exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ...
+ exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ...,
) -> _ValidatorType[_T]: ...
diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py
index 81dd6b2..0c24815 100644
--- a/src/attrs/__init__.py
+++ b/src/attrs/__init__.py
@@ -5,16 +5,7 @@ from attr import (
Attribute,
AttrsInstance,
Factory,
- __author__,
- __copyright__,
- __description__,
- __doc__,
- __email__,
- __license__,
- __title__,
- __url__,
- __version__,
- __version_info__,
+ _make_getattr,
assoc,
cmp_using,
define,
@@ -70,3 +61,5 @@ __all__ = [
"validate",
"validators",
]
+
+__getattr__ = _make_getattr(__name__)
diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi
index 4ea64d8..9372cfe 100644
--- a/src/attrs/__init__.pyi
+++ b/src/attrs/__init__.pyi
@@ -46,7 +46,7 @@ from attr import validators as validators
# TODO: see definition of attr.asdict/astuple
def asdict(
- inst: Any,
+ inst: AttrsInstance,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
dict_factory: Type[Mapping[Any, Any]] = ...,
@@ -59,7 +59,7 @@ def asdict(
# TODO: add support for returning NamedTuple from the mypy plugin
def astuple(
- inst: Any,
+ inst: AttrsInstance,
recurse: bool = ...,
filter: Optional[_FilterType[Any]] = ...,
tuple_factory: Type[Sequence[Any]] = ...,
diff --git a/tests/dataclass_transform_example.py b/tests/dataclass_transform_example.py
index c65ea61..aa0797d 100644
--- a/tests/dataclass_transform_example.py
+++ b/tests/dataclass_transform_example.py
@@ -22,7 +22,6 @@ class DefineConverter:
reveal_type(DefineConverter.__init__) # noqa
-# mypy plugin supports attr.frozen, pyright does not
@attr.frozen()
class Frozen:
a: str
@@ -34,7 +33,6 @@ d.a = "new"
reveal_type(d.a) # noqa
-# but pyright supports attr.define(frozen)
@attr.define(frozen=True)
class FrozenDefine:
a: str
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
index 0ff68c7..f5ad41d 100644
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -516,6 +516,34 @@ class TestAnnotations:
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type
+ @pytest.mark.skipif(
+ sys.version_info[:2] < (3, 9),
+ reason="Incompatible behavior on older Pythons",
+ )
+ def test_extra_resolve(self):
+ """
+ `get_type_hints` returns extra type hints.
+ """
+ from typing import Annotated
+
+ globals = {"Annotated": Annotated}
+
+ @attr.define
+ class C:
+ x: 'Annotated[float, "test"]'
+
+ attr.resolve_types(C, globals)
+
+ assert attr.fields(C).x.type == Annotated[float, "test"]
+
+ @attr.define
+ class D:
+ x: 'Annotated[float, "test"]'
+
+ attr.resolve_types(D, globals, include_extras=False)
+
+ assert attr.fields(D).x.type == float
+
def test_resolve_types_auto_attrib(self, slots):
"""
Types can be resolved even when strings are involved.
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 6945bd2..6bcc975 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -30,8 +30,9 @@ class TestSplitWhat:
"""
assert (
frozenset((int, str)),
+ frozenset(("abcd", "123")),
frozenset((fields(C).a,)),
- ) == _split_what((str, fields(C).a, int))
+ ) == _split_what((str, "123", fields(C).a, int, "abcd"))
class TestInclude:
@@ -46,6 +47,10 @@ class TestInclude:
((str,), "hello"),
((str, fields(C).a), 42),
((str, fields(C).b), "hello"),
+ (("a",), 42),
+ (("a",), "hello"),
+ (("a", str), 42),
+ (("a", fields(C).b), "hello"),
],
)
def test_allow(self, incl, value):
@@ -62,6 +67,10 @@ class TestInclude:
((int,), "hello"),
((str, fields(C).b), 42),
((int, fields(C).b), "hello"),
+ (("b",), 42),
+ (("b",), "hello"),
+ (("b", str), 42),
+ (("b", fields(C).b), "hello"),
],
)
def test_drop_class(self, incl, value):
@@ -84,6 +93,10 @@ class TestExclude:
((int,), "hello"),
((str, fields(C).b), 42),
((int, fields(C).b), "hello"),
+ (("b",), 42),
+ (("b",), "hello"),
+ (("b", str), 42),
+ (("b", fields(C).b), "hello"),
],
)
def test_allow(self, excl, value):
@@ -100,6 +113,10 @@ class TestExclude:
((str,), "hello"),
((str, fields(C).a), 42),
((str, fields(C).b), "hello"),
+ (("a",), 42),
+ (("a",), "hello"),
+ (("a", str), 42),
+ (("a", fields(C).b), "hello"),
],
)
def test_drop_class(self, excl, value):
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index f77bfd4..82f0f64 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -6,6 +6,7 @@ Tests for `attr._funcs`.
from collections import OrderedDict
+from typing import Generic, TypeVar
import pytest
@@ -418,6 +419,37 @@ class TestHas:
"""
assert not has(object)
+ def test_generics(self):
+ """
+ Works with generic classes.
+ """
+ T = TypeVar("T")
+
+ @attr.define
+ class A(Generic[T]):
+ a: T
+
+ assert has(A)
+
+ assert has(A[str])
+ # Verify twice, since there's caching going on.
+ assert has(A[str])
+
+ def test_generics_negative(self):
+ """
+ Returns `False` on non-decorated generic classes.
+ """
+ T = TypeVar("T")
+
+ class A(Generic[T]):
+ a: T
+
+ assert not has(A)
+
+ assert not has(A[str])
+ # Verify twice, since there's caching going on.
+ assert not has(A[str])
+
class TestAssoc:
"""
@@ -435,8 +467,7 @@ class TestAssoc:
pass
i1 = C()
- with pytest.deprecated_call():
- i2 = assoc(i1)
+ i2 = assoc(i1)
assert i1 is not i2
assert i1 == i2
@@ -447,8 +478,7 @@ class TestAssoc:
No changes means a verbatim copy.
"""
i1 = C()
- with pytest.deprecated_call():
- i2 = assoc(i1)
+ i2 = assoc(i1)
assert i1 is not i2
assert i1 == i2
@@ -465,8 +495,7 @@ class TestAssoc:
chosen_names = data.draw(st.sets(st.sampled_from(field_names)))
change_dict = {name: data.draw(st.integers()) for name in chosen_names}
- with pytest.deprecated_call():
- changed = assoc(original, **change_dict)
+ changed = assoc(original, **change_dict)
for k, v in change_dict.items():
assert getattr(changed, k) == v
@@ -495,22 +524,7 @@ class TestAssoc:
x = attr.ib()
y = attr.ib()
- with pytest.deprecated_call():
- assert C(3, 2) == assoc(C(1, 2), x=3)
-
- def test_warning(self):
- """
- DeprecationWarning points to the correct file.
- """
-
- @attr.s
- class C:
- x = attr.ib()
-
- with pytest.warns(DeprecationWarning) as wi:
- assert C(2) == assoc(C(1), x=2)
-
- assert __file__ == wi.list[0].filename
+ assert C(3, 2) == assoc(C(1, 2), x=3)
class TestEvolve:
@@ -675,3 +689,47 @@ class TestEvolve:
assert Cls1({"foo": 42, "param2": 42}) == attr.evolve(
obj1a, param1=obj2b
)
+
+ def test_inst_kw(self):
+ """
+ If `inst` is passed per kw argument, a warning is raised.
+ See #1109
+ """
+
+ @attr.s
+ class C:
+ pass
+
+ with pytest.warns(DeprecationWarning) as wi:
+ evolve(inst=C())
+
+ assert __file__ == wi.list[0].filename
+
+ def test_no_inst(self):
+ """
+ Missing inst argument raises a TypeError like Python would.
+ """
+ with pytest.raises(TypeError, match=r"evolve\(\) missing 1"):
+ evolve(x=1)
+
+ def test_too_many_pos_args(self):
+ """
+ More than one positional argument raises a TypeError like Python would.
+ """
+ with pytest.raises(
+ TypeError,
+ match=r"evolve\(\) takes 1 positional argument, but 2 were given",
+ ):
+ evolve(1, 2)
+
+ def test_can_change_inst(self):
+ """
+ If the instance is passed by positional argument, a field named `inst`
+ can be changed.
+ """
+
+ @attr.define
+ class C:
+ inst: int
+
+ assert C(42) == evolve(C(23), inst=42)
diff --git a/tests/test_make.py b/tests/test_make.py
index 2bdb41e..127de5d 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -13,6 +13,7 @@ import itertools
import sys
from operator import attrgetter
+from typing import Generic, TypeVar
import pytest
@@ -401,7 +402,6 @@ class TestTransformAttrs:
@attr.s(collect_by_mro=True)
class A:
-
x = attr.ib(10)
def xx(self):
@@ -1106,7 +1106,7 @@ class TestFields:
def test_handler_non_attrs_class(self):
"""
- Raises `ValueError` if passed a non-``attrs`` instance.
+ Raises `ValueError` if passed a non-*attrs* instance.
"""
with pytest.raises(NotAnAttrsClassError) as e:
fields(object)
@@ -1115,6 +1115,22 @@ class TestFields:
f"{object!r} is not an attrs-decorated class."
) == e.value.args[0]
+ def test_handler_non_attrs_generic_class(self):
+ """
+ Raises `ValueError` if passed a non-*attrs* generic class.
+ """
+ T = TypeVar("T")
+
+ class B(Generic[T]):
+ pass
+
+ with pytest.raises(NotAnAttrsClassError) as e:
+ fields(B[str])
+
+ assert (
+ f"{B[str]!r} is not an attrs-decorated class."
+ ) == e.value.args[0]
+
@given(simple_classes())
def test_fields(self, C):
"""
@@ -1130,6 +1146,24 @@ class TestFields:
for attribute in fields(C):
assert getattr(fields(C), attribute.name) is attribute
+ def test_generics(self):
+ """
+ Fields work with generic classes.
+ """
+ T = TypeVar("T")
+
+ @attr.define
+ class A(Generic[T]):
+ a: T
+
+ assert len(fields(A)) == 1
+ assert fields(A).a.name == "a"
+ assert fields(A).a.default is attr.NOTHING
+
+ assert len(fields(A[str])) == 1
+ assert fields(A[str]).a.name == "a"
+ assert fields(A[str]).a.default is attr.NOTHING
+
class TestFieldsDict:
"""
@@ -1148,7 +1182,7 @@ class TestFieldsDict:
def test_handler_non_attrs_class(self):
"""
- Raises `ValueError` if passed a non-``attrs`` instance.
+ Raises `ValueError` if passed a non-*attrs* instance.
"""
with pytest.raises(NotAnAttrsClassError) as e:
fields_dict(object)
diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml
index cdbb8e8..2e5eef5 100644
--- a/tests/test_mypy.yml
+++ b/tests/test_mypy.yml
@@ -7,20 +7,20 @@
@attr.s
class C:
{{ val }}
- C() # E: Missing positional argument "a" in call to "C"
+ C() # E: Missing positional argument "a" in call to "C" [call-arg]
C(1)
C(a=1)
- C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int"
+ C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" [arg-type]
- case: attr_s_with_type_annotations
main: |
import attr
@attr.s
class C:
a: int = attr.ib()
- C() # E: Missing positional argument "a" in call to "C"
+ C() # E: Missing positional argument "a" in call to "C" [call-arg]
C(1)
C(a=1)
- C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int"
+ C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" [arg-type]
- case: testAttrsSimple
main: |
@@ -39,7 +39,7 @@
A(1, [2])
A(1, [2], '3', 4)
A(1, 2, 3, 4)
- A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
+ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg]
- case: testAttrsAnnotated
main: |
@@ -56,8 +56,8 @@
reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A"
A(1, [2])
A(1, [2], '3', 4)
- A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str"
- A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
+ A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" [arg-type] # E: Argument 3 to "A" has incompatible type "int"; expected "str" [arg-type]
+ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg]
- case: testAttrsPython2Annotations
main: |
@@ -74,8 +74,8 @@
reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A"
A(1, [2])
A(1, [2], '3', 4)
- A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str"
- A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
+ A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" [arg-type] # E: Argument 3 to "A" has incompatible type "int"; expected "str" [arg-type]
+ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg]
- case: testAttrsAutoAttribs
main: |
@@ -92,8 +92,8 @@
reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A"
A(1, [2])
A(1, [2], '3', 4)
- A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str"
- A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
+ A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" [arg-type] # E: Argument 3 to "A" has incompatible type "int"; expected "str" [arg-type]
+ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg]
- case: testAttrsUntypedNoUntypedDefs
mypy_config: |
@@ -102,10 +102,10 @@
import attr
@attr.s
class A:
- a = attr.ib() # E: Need type annotation for "a"
- _b = attr.ib() # E: Need type annotation for "_b"
- c = attr.ib(18) # E: Need type annotation for "c"
- _d = attr.ib(validator=None, default=18) # E: Need type annotation for "_d"
+ a = attr.ib() # E: Need type annotation for "a" [var-annotated]
+ _b = attr.ib() # E: Need type annotation for "_b" [var-annotated]
+ c = attr.ib(18) # E: Need type annotation for "c" [var-annotated]
+ _d = attr.ib(validator=None, default=18) # E: Need type annotation for "_d" [var-annotated]
E = 18
- case: testAttrsWrongReturnValue
@@ -115,22 +115,22 @@
class A:
x: int = attr.ib(8)
def foo(self) -> str:
- return self.x # E: Incompatible return value type (got "int", expected "str")
+ return self.x # E: Incompatible return value type (got "int", expected "str") [return-value]
@attr.s
class B:
x = attr.ib(8) # type: int
def foo(self) -> str:
- return self.x # E: Incompatible return value type (got "int", expected "str")
+ return self.x # E: Incompatible return value type (got "int", expected "str") [return-value]
@attr.dataclass
class C:
x: int = 8
def foo(self) -> str:
- return self.x # E: Incompatible return value type (got "int", expected "str")
+ return self.x # E: Incompatible return value type (got "int", expected "str") [return-value]
@attr.s
class D:
x = attr.ib(8, type=int)
def foo(self) -> str:
- return self.x # E: Incompatible return value type (got "int", expected "str")
+ return self.x # E: Incompatible return value type (got "int", expected "str") [return-value]
- case: testAttrsSeriousNames
main: |
@@ -146,8 +146,8 @@
reveal_type(A) # N: Revealed type is "def (a: Any, b: builtins.list[builtins.int], c: Any =, d: Any =) -> main.A"
A(1, [2])
A(1, [2], '3', 4)
- A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]"
- A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
+ A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" [arg-type]
+ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg]
- case: testAttrsDefaultErrors
main: |
@@ -155,19 +155,19 @@
@attr.s
class A:
x = attr.ib(default=17)
- y = attr.ib() # E: Non-default attributes not allowed after default attributes.
+ y = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc]
@attr.s(auto_attribs=True)
class B:
x: int = 17
- y: int # E: Non-default attributes not allowed after default attributes.
+ y: int # E: Non-default attributes not allowed after default attributes. [misc]
@attr.s(auto_attribs=True)
class C:
x: int = attr.ib(default=17)
- y: int # E: Non-default attributes not allowed after default attributes.
+ y: int # E: Non-default attributes not allowed after default attributes. [misc]
@attr.s
class D:
x = attr.ib()
- y = attr.ib() # E: Non-default attributes not allowed after default attributes.
+ y = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc]
@x.default
def foo(self):
@@ -177,9 +177,9 @@
main: |
import attr
x = True
- @attr.s(cmp=x) # E: "cmp" argument must be True or False.
+ @attr.s(cmp=x) # E: "cmp" argument must be a True, False, or None literal [literal-required]
class A:
- a = attr.ib(init=x) # E: "init" argument must be True or False.
+ a = attr.ib(init=x) # E: "init" argument must be a True or False literal [literal-required]
- case: testAttrsInitFalse
main: |
@@ -192,8 +192,8 @@
_d: int = attrib(validator=None, default=18)
reveal_type(A) # N: Revealed type is "def () -> main.A"
A()
- A(1, [2]) # E: Too many arguments for "A"
- A(1, [2], '3', 4) # E: Too many arguments for "A"
+ A(1, [2]) # E: Too many arguments for "A" [call-arg]
+ A(1, [2], '3', 4) # E: Too many arguments for "A" [call-arg]
- case: testAttrsInitAttribFalse
main: |
@@ -223,17 +223,17 @@
A(1) == A(2)
A(1) != A(2)
- A(1) < 1 # E: Unsupported operand types for < ("A" and "int")
- A(1) <= 1 # E: Unsupported operand types for <= ("A" and "int")
- A(1) > 1 # E: Unsupported operand types for > ("A" and "int")
- A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int")
+ A(1) < 1 # E: Unsupported operand types for < ("A" and "int") [operator]
+ A(1) <= 1 # E: Unsupported operand types for <= ("A" and "int") [operator]
+ A(1) > 1 # E: Unsupported operand types for > ("A" and "int") [operator]
+ A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int") [operator]
A(1) == 1
A(1) != 1
- 1 < A(1) # E: Unsupported operand types for < ("int" and "A")
- 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A")
- 1 > A(1) # E: Unsupported operand types for > ("int" and "A")
- 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A")
+ 1 < A(1) # E: Unsupported operand types for < ("int" and "A") [operator]
+ 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") [operator]
+ 1 > A(1) # E: Unsupported operand types for > ("int" and "A") [operator]
+ 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") [operator]
1 == A(1)
1 != A(1)
@@ -247,24 +247,24 @@
reveal_type(A.__eq__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool"
reveal_type(A.__ne__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool"
- A(1) < A(2) # E: Unsupported left operand type for < ("A")
- A(1) <= A(2) # E: Unsupported left operand type for <= ("A")
- A(1) > A(2) # E: Unsupported left operand type for > ("A")
- A(1) >= A(2) # E: Unsupported left operand type for >= ("A")
+ A(1) < A(2) # E: Unsupported left operand type for < ("A") [operator]
+ A(1) <= A(2) # E: Unsupported left operand type for <= ("A") [operator]
+ A(1) > A(2) # E: Unsupported left operand type for > ("A") [operator]
+ A(1) >= A(2) # E: Unsupported left operand type for >= ("A") [operator]
A(1) == A(2)
A(1) != A(2)
- A(1) < 1 # E: Unsupported operand types for > ("int" and "A")
- A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A")
- A(1) > 1 # E: Unsupported operand types for < ("int" and "A")
- A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A")
+ A(1) < 1 # E: Unsupported operand types for > ("int" and "A") [operator]
+ A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") [operator]
+ A(1) > 1 # E: Unsupported operand types for < ("int" and "A") [operator]
+ A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") [operator]
A(1) == 1
A(1) != 1
- 1 < A(1) # E: Unsupported operand types for < ("int" and "A")
- 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A")
- 1 > A(1) # E: Unsupported operand types for > ("int" and "A")
- 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A")
+ 1 < A(1) # E: Unsupported operand types for < ("int" and "A") [operator]
+ 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") [operator]
+ 1 > A(1) # E: Unsupported operand types for > ("int" and "A") [operator]
+ 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") [operator]
1 == A(1)
1 != A(1)
@@ -276,24 +276,24 @@
a: int
reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A"
- A(1) < A(2) # E: Unsupported left operand type for < ("A")
- A(1) <= A(2) # E: Unsupported left operand type for <= ("A")
- A(1) > A(2) # E: Unsupported left operand type for > ("A")
- A(1) >= A(2) # E: Unsupported left operand type for >= ("A")
+ A(1) < A(2) # E: Unsupported left operand type for < ("A") [operator]
+ A(1) <= A(2) # E: Unsupported left operand type for <= ("A") [operator]
+ A(1) > A(2) # E: Unsupported left operand type for > ("A") [operator]
+ A(1) >= A(2) # E: Unsupported left operand type for >= ("A") [operator]
A(1) == A(2)
A(1) != A(2)
- A(1) < 1 # E: Unsupported operand types for > ("int" and "A")
- A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A")
- A(1) > 1 # E: Unsupported operand types for < ("int" and "A")
- A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A")
+ A(1) < 1 # E: Unsupported operand types for > ("int" and "A") [operator]
+ A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") [operator]
+ A(1) > 1 # E: Unsupported operand types for < ("int" and "A") [operator]
+ A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") [operator]
A(1) == 1
A(1) != 1
- 1 < A(1) # E: Unsupported operand types for < ("int" and "A")
- 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A")
- 1 > A(1) # E: Unsupported operand types for > ("int" and "A")
- 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A")
+ 1 < A(1) # E: Unsupported operand types for < ("int" and "A") [operator]
+ 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") [operator]
+ 1 > A(1) # E: Unsupported operand types for > ("int" and "A") [operator]
+ 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") [operator]
1 == A(1)
1 != A(1)
@@ -308,11 +308,11 @@
class DeprecatedFalse:
...
- @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order"
+ @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order" [misc]
class Mixed:
...
- @attrs(order=True, eq=False) # E: eq must be True if order is True
+ @attrs(order=True, eq=False) # E: eq must be True if order is True [misc]
class Confused:
...
@@ -384,7 +384,7 @@
a = attr.ib()
a = A(5)
- a.a = 16 # E: Property "a" defined in "A" is read-only
+ a.a = 16 # E: Property "a" defined in "A" is read-only [misc]
- case: testAttrsNextGenFrozen
main: |
from attr import frozen, field
@@ -394,7 +394,7 @@
a = field()
a = A(5)
- a.a = 16 # E: Property "a" defined in "A" is read-only
+ a.a = 16 # E: Property "a" defined in "A" is read-only [misc]
- case: testAttrsNextGenDetect
main: |
@@ -464,15 +464,15 @@
def bar(self) -> T:
return self.x[0]
def problem(self) -> T:
- return self.x # E: Incompatible return value type (got "List[T]", expected "T")
+ return self.x # E: Incompatible return value type (got "List[T]", expected "T") [return-value]
reveal_type(A) # N: Revealed type is "def [T] (x: builtins.list[T`1], y: T`1) -> main.A[T`1]"
a = A([1], 2)
reveal_type(a) # N: Revealed type is "main.A[builtins.int]"
reveal_type(a.x) # N: Revealed type is "builtins.list[builtins.int]"
reveal_type(a.y) # N: Revealed type is "builtins.int"
- A(['str'], 7) # E: Cannot infer type argument 1 of "A"
- A([1], '2') # E: Cannot infer type argument 1 of "A"
+ A(['str'], 7) # E: Cannot infer type argument 1 of "A" [misc]
+ A([1], '2') # E: Cannot infer type argument 1 of "A" [misc]
- case: testAttrsUntypedGenericInheritance
main: |
@@ -643,7 +643,7 @@
return cls(6, 'hello')
@classmethod
def bad(cls) -> A:
- return cls(17) # E: Missing positional argument "b" in call to "A"
+ return cls(17) # E: Missing positional argument "b" in call to "A" [call-arg]
def foo(self) -> int:
return self.a
reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> main.A"
@@ -781,10 +781,10 @@
bad_overloaded: int = attr.ib(converter=bad_overloaded_converter)
reveal_type(A)
out: |
- main:15: error: Cannot determine __init__ type from converter
- main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]"
- main:16: error: Cannot determine __init__ type from converter
- main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]"
+ main:15: error: Cannot determine __init__ type from converter [misc]
+ main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" [arg-type]
+ main:16: error: Cannot determine __init__ type from converter [misc]
+ main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" [arg-type]
main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A"
- case: testAttrsUsingBadConverterReprocess
@@ -809,10 +809,10 @@
bad_overloaded: int = attr.ib(converter=bad_overloaded_converter)
reveal_type(A)
out: |
- main:16: error: Cannot determine __init__ type from converter
- main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]"
- main:17: error: Cannot determine __init__ type from converter
- main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]"
+ main:16: error: Cannot determine __init__ type from converter [misc]
+ main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" [arg-type]
+ main:17: error: Cannot determine __init__ type from converter [misc]
+ main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" [arg-type]
main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A"
- case: testAttrsUsingUnsupportedConverter
@@ -820,15 +820,15 @@
import attr
class Thing:
def do_it(self, int) -> str:
- ...
+ return ""
thing = Thing()
def factory(default: int):
- ...
+ return 1
@attr.s
class C:
- x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions, types and lambdas are currently supported
+ x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions, types and lambdas are currently supported [misc]
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
+ z: str = attr.ib(converter=factory(8)) # E: Unsupported converter, only named functions, types and lambdas are currently supported [misc]
reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> main.C"
- case: testAttrsUsingConverterAndSubclass
@@ -887,22 +887,22 @@
A() < A()
B() < B()
- A() < B() # E: Unsupported operand types for < ("A" and "B")
+ A() < B() # E: Unsupported operand types for < ("A" and "B") [operator]
C() > A()
C() > B()
C() > C()
- C() > D() # E: Unsupported operand types for > ("C" and "D")
+ C() > D() # E: Unsupported operand types for > ("C" and "D") [operator]
D() >= A()
- D() >= B() # E: Unsupported operand types for >= ("D" and "B")
- D() >= C() # E: Unsupported operand types for >= ("D" and "C")
+ D() >= B() # E: Unsupported operand types for >= ("D" and "B") [operator]
+ D() >= C() # E: Unsupported operand types for >= ("D" and "C") [operator]
D() >= D()
- A() <= 1 # E: Unsupported operand types for <= ("A" and "int")
- B() <= 1 # E: Unsupported operand types for <= ("B" and "int")
- C() <= 1 # E: Unsupported operand types for <= ("C" and "int")
- D() <= 1 # E: Unsupported operand types for <= ("D" and "int")
+ A() <= 1 # E: Unsupported operand types for <= ("A" and "int") [operator]
+ B() <= 1 # E: Unsupported operand types for <= ("B" and "int") [operator]
+ C() <= 1 # E: Unsupported operand types for <= ("C" and "int") [operator]
+ D() <= 1 # E: Unsupported operand types for <= ("D" and "int") [operator]
- case: testAttrsComplexSuperclass
main: |
@@ -933,7 +933,7 @@
import attr
@attr.s
class A:
- x = y = z = attr.ib() # E: Too many names for one attribute
+ x = y = z = attr.ib() # E: Too many names for one attribute [misc]
- case: testAttrsPrivateInit
main: |
@@ -942,7 +942,7 @@
class C:
_x = attr.ib(init=False, default=42)
C()
- C(_x=42) # E: Unexpected keyword argument "_x" for "C"
+ C(_x=42) # E: Unexpected keyword argument "_x" for "C" [call-arg]
- case: testAttrsAutoMustBeAll
main: |
@@ -952,9 +952,9 @@
a: int
b = 17
# The following forms are not allowed with auto_attribs=True
- c = attr.ib() # E: Need type annotation for "c"
- d, e = attr.ib(), attr.ib() # E: Need type annotation for "d" # E: Need type annotation for "e"
- f = g = attr.ib() # E: Need type annotation for "f" # E: Need type annotation for "g"
+ c = attr.ib() # E: Need type annotation for "c" [var-annotated]
+ d, e = attr.ib(), attr.ib() # E: Need type annotation for "d" [var-annotated] # E: Need type annotation for "e" [var-annotated]
+ f = g = attr.ib() # E: Need type annotation for "f" [var-annotated] # E: Need type annotation for "g" [var-annotated]
- case: testAttrsRepeatedName
main: |
@@ -969,13 +969,13 @@
class B:
a: int = attr.ib(default=8)
b: int = attr.ib()
- a: int = attr.ib() # E: Name "a" already defined on line 10
+ a: int = attr.ib() # E: Name "a" already defined on line 10 [no-redef]
reveal_type(B) # N: Revealed type is "def (b: builtins.int, a: builtins.int) -> main.B"
@attr.s(auto_attribs=True)
class C:
a: int = 8
b: int
- a: int = attr.ib() # E: Name "a" already defined on line 16
+ a: int = attr.ib() # E: Name "a" already defined on line 16 [no-redef]
reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.int) -> main.C"
- case: testAttrsFrozenSubclass
@@ -1006,19 +1006,19 @@
non_frozen_base = NonFrozenBase(1)
non_frozen_base.a = 17
frozen_base = FrozenBase(1)
- frozen_base.a = 17 # E: Property "a" defined in "FrozenBase" is read-only
+ frozen_base.a = 17 # E: Property "a" defined in "FrozenBase" is read-only [misc]
a = FrozenNonFrozen(1, 2)
- a.a = 17 # E: Property "a" defined in "FrozenNonFrozen" is read-only
- a.b = 17 # E: Property "b" defined in "FrozenNonFrozen" is read-only
+ a.a = 17 # E: Property "a" defined in "FrozenNonFrozen" is read-only [misc]
+ a.b = 17 # E: Property "b" defined in "FrozenNonFrozen" is read-only [misc]
b = FrozenFrozen(1, 2)
- b.a = 17 # E: Property "a" defined in "FrozenFrozen" is read-only
- b.b = 17 # E: Property "b" defined in "FrozenFrozen" is read-only
+ b.a = 17 # E: Property "a" defined in "FrozenFrozen" is read-only [misc]
+ b.b = 17 # E: Property "b" defined in "FrozenFrozen" is read-only [misc]
c = NonFrozenFrozen(1, 2)
- c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only
- c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only
+ c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only [misc]
+ c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only [misc]
- case: testAttrsCallableAttributes
main: |
from typing import Callable
@@ -1060,7 +1060,7 @@
import attr
@attr.s
class A:
- x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory".
+ x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory". [misc]
- case: testAttrsFactoryBadReturn
main: |
@@ -1069,8 +1069,8 @@
return 7
@attr.s
class A:
- x: int = attr.ib(factory=list) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int")
- y: str = attr.ib(factory=my_factory) # E: Incompatible types in assignment (expression has type "int", variable has type "str")
+ x: int = attr.ib(factory=list) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") [assignment]
+ y: str = attr.ib(factory=my_factory) # E: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
- case: testAttrsDefaultAndInit
main: |
@@ -1082,7 +1082,7 @@
b = attr.ib() # Ok because previous attribute is init=False
c = attr.ib(default=44)
d = attr.ib(init=False) # Ok because this attribute is init=False
- e = attr.ib() # E: Non-default attributes not allowed after default attributes.
+ e = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc]
- case: testAttrsOptionalConverter
main: |
@@ -1121,8 +1121,8 @@
@attr.s
class A:
a = attr.ib(kw_only=True)
- A() # E: Missing named argument "a" for "A"
- A(15) # E: Too many positional arguments for "A"
+ A() # E: Missing named argument "a" for "A" [call-arg]
+ A(15) # E: Too many positional arguments for "A" [misc]
A(a=15)
- case: testAttrsKwOnlyClass
@@ -1132,7 +1132,7 @@
class A:
a: int
b: bool
- A() # E: Missing named argument "a" for "A" # E: Missing named argument "b" for "A"
+ A() # E: Missing named argument "a" for "A" [call-arg] # E: Missing named argument "b" for "A" [call-arg]
A(b=True, a=15)
- case: testAttrsKwOnlyClassNoInit
@@ -1211,7 +1211,7 @@
@attr.s
class B:
- x = attr.ib() # E: Need type annotation for "x"
+ x = attr.ib() # E: Need type annotation for "x" [var-annotated]
reveal_type(B) # N: Revealed type is "def (x: Any) -> main.B"
@@ -1253,7 +1253,7 @@
@attr.s
class C:
- total = attr.ib(type=Bad) # E: Name "Bad" is not defined
+ total = attr.ib(type=Bad) # E: Name "Bad" is not defined [name-defined]
- case: testTypeInAttrForwardInRuntime
main: |
@@ -1264,7 +1264,7 @@
total = attr.ib(type=Forward)
reveal_type(C.total) # N: Revealed type is "main.Forward"
- C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "Forward"
+ C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "Forward" [arg-type]
class Forward: ...
- case: testDefaultInAttrForward
@@ -1275,11 +1275,11 @@
class C:
total = attr.ib(default=func())
- def func() -> int: ...
+ def func() -> int: return 5
C()
C(1)
- C(1, 2) # E: Too many arguments for "C"
+ C(1, 2) # E: Too many arguments for "C" [call-arg]
- case: testTypeInAttrUndefinedFrozen
main: |
@@ -1287,9 +1287,9 @@
@attr.s(frozen=True)
class C:
- total = attr.ib(type=Bad) # E: Name "Bad" is not defined
+ total = attr.ib(type=Bad) # E: Name "Bad" is not defined [name-defined]
- C(0).total = 1 # E: Property "total" defined in "C" is read-only
+ C(0).total = 1 # E: Property "total" defined in "C" is read-only [misc]
- case: testTypeInAttrDeferredStar
main: |
@@ -1306,8 +1306,8 @@
class C:
total = attr.ib(type=int)
- C() # E: Missing positional argument "total" in call to "C"
- C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "int"
+ C() # E: Missing positional argument "total" in call to "C" [call-arg]
+ C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "int" [arg-type]
- path: other.py
content: |
import lib
@@ -1322,7 +1322,7 @@
from b import A1, A2
@attr.s
- class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes.
+ class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes. [misc]
pass
- path: b.py
content: |
@@ -1370,7 +1370,26 @@
a: int
b: str
- fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected "Type[AttrsInstance]"
+ fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected "Type[AttrsInstance]" [arg-type]
+
+- case: testAsDict
+ main: |
+ from attrs import asdict, define
+
+ @define
+ class A:
+ a: int
+
+ asdict(A(1))
+
+- case: testAsDictError
+ main: |
+ from attrs import asdict
+
+ class A:
+ a: int
+
+ asdict(A()) # E: Argument 1 to "asdict" has incompatible type "A"; expected "AttrsInstance" [arg-type]
- case: testHasTypeGuard
main: |
diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py
index 78fd0e5..7908a41 100644
--- a/tests/test_next_gen.py
+++ b/tests/test_next_gen.py
@@ -6,6 +6,7 @@ Integration tests for next-generation APIs.
import re
+from contextlib import contextmanager
from functools import partial
import pytest
@@ -27,6 +28,16 @@ class TestNextGen:
"""
C("1", 2)
+ def test_field_type(self):
+ """
+ Make class with attrs.field and type parameter.
+ """
+ classFields = {"testint": attrs.field(type=int)}
+
+ A = attrs.make_class("A", classFields)
+
+ assert int == attrs.fields(A).testint.type
+
def test_no_slots(self):
"""
slots can be deactivated.
@@ -312,6 +323,38 @@ class TestNextGen:
assert "foo" == ei.value.x
assert ei.value.__cause__ is None
+ @pytest.mark.parametrize(
+ "decorator",
+ [
+ partial(_attr.s, frozen=True, slots=True, auto_exc=True),
+ attrs.frozen,
+ attrs.define,
+ attrs.mutable,
+ ],
+ )
+ def test_setting_traceback_on_exception(self, decorator):
+ """
+ contextlib.contextlib (re-)sets __traceback__ on raised exceptions.
+
+ Ensure that works, as well as if done explicitly
+ """
+
+ @decorator
+ class MyException(Exception):
+ pass
+
+ @contextmanager
+ def do_nothing():
+ yield
+
+ with do_nothing(), pytest.raises(MyException) as ei:
+ raise MyException()
+
+ assert isinstance(ei.value, MyException)
+
+ # this should not raise an exception either
+ ei.value.__traceback__ = ei.value.__traceback__
+
def test_converts_and_validates_by_default(self):
"""
If no on_setattr is set, assume setters.convert, setters.validate.
@@ -346,7 +389,6 @@ class TestNextGen:
@attrs.define
class A:
-
x: int = 10
def xx(self):
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
new file mode 100644
index 0000000..c197bcc
--- /dev/null
+++ b/tests/test_packaging.py
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: MIT
+
+import sys
+
+import pytest
+
+import attr
+import attrs
+
+
+if sys.version_info < (3, 8):
+ import importlib_metadata as metadata
+else:
+ from importlib import metadata
+
+
+@pytest.fixture(name="mod", params=(attr, attrs))
+def _mod(request):
+ yield request.param
+
+
+class TestLegacyMetadataHack:
+ def test_title(self, mod):
+ """
+ __title__ returns attrs.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "attrs" == mod.__title__
+
+ assert (
+ f"Accessing {mod.__name__}.__title__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_copyright(self, mod):
+ """
+ __copyright__ returns the correct blurp.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "Copyright (c) 2015 Hynek Schlawack" == mod.__copyright__
+
+ assert (
+ f"Accessing {mod.__name__}.__copyright__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_version(self, mod):
+ """
+ __version__ returns the correct version.
+ """
+ with pytest.deprecated_call() as ws:
+ assert metadata.version("attrs") == mod.__version__
+
+ assert (
+ f"Accessing {mod.__name__}.__version__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_description(self, mod):
+ """
+ __description__ returns the correct description.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "Classes Without Boilerplate" == mod.__description__
+
+ assert (
+ f"Accessing {mod.__name__}.__description__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ @pytest.mark.parametrize("name", ["__uri__", "__url__"])
+ def test_uri(self, mod, name):
+ """
+ __uri__ & __url__ returns the correct project URL.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "https://www.attrs.org/" == getattr(mod, name)
+
+ assert (
+ f"Accessing {mod.__name__}.{name} is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_author(self, mod):
+ """
+ __author__ returns Hynek.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "Hynek Schlawack" == mod.__author__
+
+ assert (
+ f"Accessing {mod.__name__}.__author__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_email(self, mod):
+ """
+ __email__ returns Hynek's email address.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "hs@ox.cx" == mod.__email__
+
+ assert (
+ f"Accessing {mod.__name__}.__email__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_license(self, mod):
+ """
+ __license__ returns MIT.
+ """
+ with pytest.deprecated_call() as ws:
+ assert "MIT" == mod.__license__
+
+ assert (
+ f"Accessing {mod.__name__}.__license__ is deprecated"
+ in ws.list[0].message.args[0]
+ )
+
+ def test_does_not_exist(self, mod):
+ """
+ Asking for unsupported dunders raises an AttributeError.
+ """
+ with pytest.raises(
+ AttributeError,
+ match=f"module {mod.__name__} has no attribute __yolo__",
+ ):
+ mod.__yolo__
+
+ def test_version_info(self, recwarn, mod):
+ """
+ ___version_info__ is not deprected, therefore doesn't raise a warning
+ and parses correctly.
+ """
+ assert isinstance(mod.__version_info__, attr.VersionInfo)
+ assert [] == recwarn.list
diff --git a/tests/test_pyright.py b/tests/test_pyright.py
index 98f5038..7fd6698 100644
--- a/tests/test_pyright.py
+++ b/tests/test_pyright.py
@@ -62,6 +62,11 @@ def test_pyright_baseline():
message='Type of "DefineConverter.__init__" is '
'"(self: DefineConverter, with_converter: int) -> None"',
),
+ PyrightDiagnostic(
+ severity="error",
+ message='Cannot assign member "a" for type '
+ '"Frozen"\n\xa0\xa0"Frozen" is frozen',
+ ),
PyrightDiagnostic(
severity="information",
message='Type of "d.a" is "Literal[\'new\']"',
diff --git a/tests/test_slots.py b/tests/test_slots.py
index f436b3b..18513ce 100644
--- a/tests/test_slots.py
+++ b/tests/test_slots.py
@@ -754,13 +754,12 @@ class A:
c = attr.ib()
-@pytest.mark.parametrize("cls", [A])
-def test_slots_unpickle_after_attr_removed(cls):
+def test_slots_unpickle_after_attr_removed():
"""
We don't assign attributes we don't have anymore if the class has
removed it.
"""
- a = cls(1, 2, 3)
+ a = A(1, 2, 3)
a_pickled = pickle.dumps(a)
a_unpickled = pickle.loads(a_pickled)
assert a_unpickled == a
@@ -778,12 +777,11 @@ def test_slots_unpickle_after_attr_removed(cls):
assert not hasattr(new_a, "b")
-@pytest.mark.parametrize("cls", [A])
-def test_slots_unpickle_after_attr_added(cls, frozen):
+def test_slots_unpickle_after_attr_added(frozen):
"""
We don't assign attribute we haven't had before if the class has one added.
"""
- a = cls(1, 2, 3)
+ a = A(1, 2, 3)
a_pickled = pickle.dumps(a)
a_unpickled = pickle.loads(a_pickled)
@@ -803,3 +801,20 @@ def test_slots_unpickle_after_attr_added(cls, frozen):
assert new_a.b == 2
assert new_a.c == 3
assert not hasattr(new_a, "d")
+
+
+def test_slots_unpickle_is_backward_compatible(frozen):
+ """
+ Ensure object pickled before v22.2.0 can still be unpickled.
+ """
+ a = A(1, 2, 3)
+
+ a_pickled = (
+ b"\x80\x04\x95&\x00\x00\x00\x00\x00\x00\x00\x8c\x10"
+ + a.__module__.encode()
+ + b"\x94\x8c\x01A\x94\x93\x94)\x81\x94K\x01K\x02K\x03\x87\x94b."
+ )
+
+ a_unpickled = pickle.loads(a_pickled)
+
+ assert a_unpickled == a
diff --git a/tests/test_validators.py b/tests/test_validators.py
index d486ca4..d42acf4 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -349,7 +349,9 @@ class TestProvides:
def f(self):
pass
- v = provides(ifoo)
+ with pytest.deprecated_call():
+ v = provides(ifoo)
+
v(None, simple_attr("x"), C())
def test_fail(self, ifoo):
@@ -359,9 +361,12 @@ class TestProvides:
value = object()
a = simple_attr("x")
- v = provides(ifoo)
+ with pytest.deprecated_call():
+ v = provides(ifoo)
+
with pytest.raises(TypeError) as e:
v(None, a, value)
+
assert (
"'x' must provide {interface!r} which {value!r} doesn't.".format(
interface=ifoo, value=value
@@ -375,7 +380,9 @@ class TestProvides:
"""
Returned validator has a useful `__repr__`.
"""
- v = provides(ifoo)
+ with pytest.deprecated_call():
+ v = provides(ifoo)
+
assert (
"<provides validator for interface {interface!r}>".format(
interface=ifoo
@@ -384,7 +391,12 @@ class TestProvides:
@pytest.mark.parametrize(
- "validator", [instance_of(int), [always_pass, instance_of(int)]]
+ "validator",
+ [
+ instance_of(int),
+ [always_pass, instance_of(int)],
+ (always_pass, instance_of(int)),
+ ],
)
class TestOptional:
"""
@@ -437,6 +449,11 @@ class TestOptional:
"<optional validator for _AndValidator(_validators=[{func}, "
"<instance_of validator for type <class 'int'>>]) or None>"
).format(func=repr(always_pass))
+ elif isinstance(validator, tuple):
+ repr_s = (
+ "<optional validator for _AndValidator(_validators=({func}, "
+ "<instance_of validator for type <class 'int'>>)) or None>"
+ ).format(func=repr(always_pass))
else:
repr_s = (
"<optional validator for <instance_of validator for type "
diff --git a/tests/typing_example.py b/tests/typing_example.py
index 4008086..5f5f79a 100644
--- a/tests/typing_example.py
+++ b/tests/typing_example.py
@@ -236,6 +236,15 @@ class Validated:
p: Any = attr.ib(
validator=attr.validators.not_(attr.validators.in_("abc"), msg=None)
)
+ q: Any = attr.ib(
+ validator=attrs.validators.optional(attrs.validators.instance_of(C))
+ )
+ r: Any = attr.ib(
+ validator=attrs.validators.optional([attrs.validators.instance_of(C)])
+ )
+ s: Any = attr.ib(
+ validator=attrs.validators.optional((attrs.validators.instance_of(C),))
+ )
@attr.define
@@ -432,6 +441,7 @@ def accessing_from_attr() -> None:
attr.converters.optional
attr.exceptions.FrozenError
attr.filters.include
+ attr.filters.exclude
attr.setters.frozen
attr.validators.and_
attr.cmp_using
@@ -444,6 +454,7 @@ def accessing_from_attrs() -> None:
attrs.converters.optional
attrs.exceptions.FrozenError
attrs.filters.include
+ attrs.filters.exclude
attrs.setters.frozen
attrs.validators.and_
attrs.cmp_using
diff --git a/tox.ini b/tox.ini
index 0a31e9c..faf916d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,63 +1,38 @@
-# Keep docs in sync with docs env and .readthedocs.yml.
-[gh-actions]
-python =
- 3.6: py36, mypy
- 3.7: py37
- 3.8: py38, changelog
- 3.9: py39
- 3.10: py310, mypy
- 3.11: py311
- 3.12: py312
- pypy-3: pypy3
-
-
[tox]
-envlist = mypy,pre-commit,py36,py37,py38,py39,py310,py311,py312,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
+envlist = mypy,pre-commit,py37,py38,py39,py310,py311,py312,pypy3,pyright,docs,changelog,coverage-report
isolated_build = True
[testenv:docs]
-# Keep basepython in sync with ci.yml/docs and .readthedocs.yml.
-basepython = python3.10
+# Keep basepython in sync with ci.yml/docs and .readthedocs.yaml.
+basepython = python3.11
extras = docs
commands =
sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html
+
[testenv]
extras = tests
-commands = python -m pytest {posargs:-n auto}
-
-
-[testenv:py36]
-extras = cov
-setenv = COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
-commands = coverage run -m pytest {posargs:-n auto}
-
-
-[testenv:py310]
-extras = cov
-setenv =
- PYTHONWARNINGS=d
- {[testenv:py36]setenv}
-commands = {[testenv:py36]commands}
+commands = pytest {posargs:-n auto}
+passenv = SETUPTOOLS_SCM_PRETEND_VERSION
-[testenv:py31{1,2}]
+[testenv:py3{7,10,11}]
extras = cov
# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.
-install_command = python -m pip install --no-compile {opts} {packages}
+install_command = python -Im pip install --no-compile {opts} {packages}
setenv =
+ HATCH_BUILD_HOOK_ENABLE_AUTORUN=1
+ COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
PYTHONWARNINGS=d
- {[testenv:py36]setenv}
-# xdist is currently broken on 3.11rc2
-commands = coverage run -m pytest {posargs}
+commands = coverage run -m pytest {posargs:-n auto}
[testenv:coverage-report]
-basepython = python3.10
-depends = py36,py310
+basepython = python3.11
+depends = py3{7,10,11}
skip_install = true
deps = coverage[toml]>=5.3
commands =
@@ -68,37 +43,17 @@ commands =
[testenv:pre-commit]
skip_install = true
deps = pre-commit
-passenv = HOMEPATH # needed on Windows
-commands = pre-commit run --all-files --show-diff-on-failure
-
-
-[testenv:manifest]
-basepython = python3.10
-deps = check-manifest
-skip_install = true
-commands = check-manifest
-
-
-[testenv:pypi-description]
-basepython = python3.8
-skip_install = true
-deps =
- twine
- pip >= 18.0.0
-commands =
- pip wheel -w {envtmpdir}/build --no-deps .
- twine check {envtmpdir}/build/*
+commands = pre-commit run --all-files
[testenv:changelog]
-basepython = python3.8
deps = towncrier
skip_install = true
commands = towncrier build --version main --draft
[testenv:mypy]
-deps = mypy>=0.902
+deps = mypy>=0.991
commands =
mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi
mypy tests/typing_example.py
More details
Historical runs
- failed: E: pybuild pybuild:388: build: plugin pyproject failed with: exit code=1: python3.11 -m build --skip-dependency-check --no-isolation --wheel --outdir /<<PKGBUILDDIR>>/.pybuild/cpython3_3.11_attr
- nothing-to-do: Last upstream version 22.2.0 already imported.
- nothing-to-do: Last upstream version 22.1.0 already imported.
- apt-file-fetch-failure: E: Failed to fetch http://deb.debian.org/debian/pool/main/p/python3.10/python3.10-minimal_3.10.2-2_amd64.deb 404 Not Found [IP: 151.101.54.132 80]