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

Full run details

Historical runs