From 5d51455cf9edfa06d629269274b2f128741300b4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 26 May 2023 11:02:11 +0100 Subject: [PATCH 1/9] Skip a problematic test on CPython 3.12.0b1 (#200) --- CHANGELOG.md | 6 ++++++ src/test_typing_extensions.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6165d9c..18d5f370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Skip a problematic test on Python 3.12.0b1. The test fails on 3.12.0b1 due to + a bug in CPython, which will be fixed in 3.12.0b2. The + `typing_extensions` test suite now passes on 3.12.0b1. + # Release 4.6.2 (May 25, 2023) - Fix use of `@deprecated` on classes with `__new__` but no `__init__`. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fd2a91c3..ac0a2691 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2267,6 +2267,10 @@ class Foo: ... del f.x self.assertNotIsInstance(f, HasX) + @skipIf( + sys.version_info == (3, 12, 0, 'beta', 1), + "CPython had a bug in 3.12.0b1" + ) def test_protocols_isinstance_generic_classes(self): T = TypeVar("T") From 06f73609ffea80780c9418af24daa624427a837d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 26 May 2023 11:29:06 +0100 Subject: [PATCH 2/9] Minor improvements to CI workflows (#201) - Use the new `allow-prereleases` key for `actions/setup-python`. If we set this to `true`, then we can just use `3.12` in our CI matrix, instead of `3.12-dev`. That means we won't have to worry about updating the matrix from `3.12-dev` to `3.12` when 3.12.0 comes out in the autumn; our CI will automatically start using the latest version of Python 3.12. - Colorise the output of flake8. - Don't bother upgrading pip in CI. It's not really necessary, and wastes a few seconds. Disable the "you're using an older version of pip" message instead, using the `PIP_DISABLE_PIP_VERSION_CHECK` environment variable. --- .github/workflows/ci.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b2fca67..8b00fad4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ on: permissions: contents: read +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -25,7 +28,7 @@ jobs: # Python version, because typing sometimes changed between bugfix releases. # For available versions, see: # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - python-version: ["3.7", "3.7.1", "3.8", "3.8.0", "3.9", "3.9.0", "3.10", "3.10.0", "3.11", "3.11.0", "3.12-dev", "pypy3.9"] + python-version: ["3.7", "3.7.1", "3.8", "3.8.0", "3.9", "3.9.0", "3.10", "3.10.0", "3.11", "3.11.0", "3.12", "pypy3.9"] runs-on: ubuntu-20.04 @@ -36,9 +39,10 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Test typing_extensions - continue-on-error: ${{ matrix.python-version == '3.12-dev' }} + continue-on-error: ${{ matrix.python-version == '3.12' }} run: | # Be wary of running `pip install` here, since it becomes easy for us to # accidentally pick up typing_extensions as installed by a dependency @@ -61,14 +65,13 @@ jobs: - name: Install dependencies run: | - pip install --upgrade pip pip install -r test-requirements.txt # not included in test-requirements.txt as it depends on typing-extensions, # so it's a pain to have it installed locally pip install flake8-noqa - name: Lint implementation - run: flake8 + run: flake8 --color always - name: Lint tests - run: flake8 --config=.flake8-tests src/test_typing_extensions.py + run: flake8 --config=.flake8-tests src/test_typing_extensions.py --color always From 3c630b92b2a0ef1aa2910a0b7b496e9093f1241b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 26 May 2023 13:29:23 +0100 Subject: [PATCH 3/9] Backport some new protocol tests from CPython (#202) --- src/test_typing_extensions.py | 60 ++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ac0a2691..24f51e65 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1696,6 +1696,12 @@ class NT(NamedTuple): y: int +skip_if_py312b1 = skipIf( + sys.version_info == (3, 12, 0, 'beta', 1), + "CPython had a bug in 3.12.0b1" +) + + class ProtocolTests(BaseTestCase): def test_runtime_alias(self): self.assertIs(runtime, runtime_checkable) @@ -2267,10 +2273,56 @@ class Foo: ... del f.x self.assertNotIsInstance(f, HasX) - @skipIf( - sys.version_info == (3, 12, 0, 'beta', 1), - "CPython had a bug in 3.12.0b1" - ) + @skip_if_py312b1 + def test_runtime_checkable_generic_non_protocol(self): + # Make sure this doesn't raise AttributeError + with self.assertRaisesRegex( + TypeError, + "@runtime_checkable can be only applied to protocol classes", + ): + @runtime_checkable + class Foo(Generic[T]): ... + + def test_runtime_checkable_generic(self): + @runtime_checkable + class Foo(Protocol[T]): + def meth(self) -> T: ... + + class Impl: + def meth(self) -> int: ... + + self.assertIsSubclass(Impl, Foo) + + class NotImpl: + def method(self) -> int: ... + + self.assertNotIsSubclass(NotImpl, Foo) + + if sys.version_info >= (3, 12): + exec(textwrap.dedent( + """ + @skip_if_py312b1 + def test_pep695_generics_can_be_runtime_checkable(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class Bar[T]: + x: T + def __init__(self, x): + self.x = x + + class Capybara[T]: + y: str + def __init__(self, y): + self.y = y + + self.assertIsInstance(Bar(1), HasX) + self.assertNotIsInstance(Capybara('a'), HasX) + """ + )) + + @skip_if_py312b1 def test_protocols_isinstance_generic_classes(self): T = TypeVar("T") From 252a4a4f52d498019774f0cedd21bccaa19d0d65 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 26 May 2023 11:50:44 -0700 Subject: [PATCH 4/9] Add more detailed versioning policy (#197) --- CONTRIBUTING.md | 7 +++++-- doc/index.rst | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2585ac70..3b1a093b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,11 +23,14 @@ CPython's `main` branch. # Versioning scheme Starting with version 4.0.0, `typing_extensions` uses -[Semantic Versioning](https://semver.org/). The major version is incremented for all -backwards-incompatible changes. +[Semantic Versioning](https://semver.org/). See the documentation +for more detail. # Workflow for PyPI releases +- Make sure you follow the versioning policy in the documentation + (e.g., release candidates before any feature release) + - Ensure that GitHub Actions reports no errors. - Update the version number in `typing_extensions/pyproject.toml` and in diff --git a/doc/index.rst b/doc/index.rst index e790a2fd..6b1a6f0b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -37,6 +37,17 @@ In view of the wide usage of ``typing_extensions`` across the ecosystem, we are highly hesitant to break backwards compatibility, and we do not expect to increase the major version number in the foreseeable future. +Feature releases, with version numbers of the form 4.N.0, are made at +irregular intervals when enough new features accumulate. Before a +feature release, at least one release candidate (with a version number +of the form 4.N.0rc1) should be released to give downstream users time +to test. After at least a week of testing, the new feature version +may then be released. If necessary, additional release candidates can +be added. + +Bugfix releases, with version numbers of the form 4.N.1 or higher, +may be made if bugs are discovered after a feature release. + Before version 4.0.0, the versioning scheme loosely followed the Python version from which features were backported; for example, ``typing_extensions`` 3.10.0.0 was meant to reflect ``typing`` as of From 32887d3315326a6cfeb7b710f379328345209cec Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 26 May 2023 23:12:31 -0700 Subject: [PATCH 5/9] Sync LICENSE with CPython (#205) --- CHANGELOG.md | 3 +++ LICENSE | 33 +++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d5f370..5e13e291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- Sync the repository's LICENSE file with that of CPython. + `typing_extensions` is distributed under the same license as + CPython itself. - Skip a problematic test on Python 3.12.0b1. The test fails on 3.12.0b1 due to a bug in CPython, which will be fixed in 3.12.0b2. The `typing_extensions` test suite now passes on 3.12.0b1. diff --git a/LICENSE b/LICENSE index 1df6b3b8..f26bcf4d 100644 --- a/LICENSE +++ b/LICENSE @@ -2,12 +2,12 @@ A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. @@ -19,7 +19,7 @@ https://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation was a sponsoring member of the PSF. -All Python releases are Open Source (see http://www.opensource.org for +All Python releases are Open Source (see https://opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. @@ -59,6 +59,17 @@ direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- @@ -73,7 +84,7 @@ analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation; +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. @@ -252,3 +263,17 @@ FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. From b0be88cdede662b1404e7ba52d6a55d1bf1fefff Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 31 May 2023 19:29:56 +0100 Subject: [PATCH 6/9] Add a cron job for testing third-party users of typing_extensions (#206) Co-authored-by: Jelle Zijlstra Co-authored-by: Sebastian Rittau --- .github/workflows/ci.yml | 42 ++++ .github/workflows/third_party.yml | 337 ++++++++++++++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 .github/workflows/third_party.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b00fad4..5b5ac6c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,8 @@ name: Test and lint on: + schedule: + - cron: "0 2 * * *" # 2am UTC push: branches: - main @@ -21,6 +23,14 @@ jobs: tests: name: Run tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: fail-fast: false matrix: @@ -52,6 +62,9 @@ jobs: linting: name: Lint + # no reason to run this as a cron job + if: github.event_name != 'schedule' + runs-on: ubuntu-latest steps: @@ -75,3 +88,32 @@ jobs: - name: Lint tests run: flake8 --config=.flake8-tests src/test_typing_extensions.py --color always + + create-issue-on-failure: + name: Create an issue if daily tests failed + runs-on: ubuntu-latest + + needs: [tests] + + if: >- + ${{ + github.repository == 'python/typing_extensions' + && always() + && github.event_name == 'schedule' + && needs.tests.result == 'failure' + }} + + permissions: + issues: write + + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.create({ + owner: "python", + repo: "typing_extensions", + title: `Daily tests failed on ${new Date().toDateString()}`, + body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/ci.yml", + }) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml new file mode 100644 index 00000000..19be0cf3 --- /dev/null +++ b/.github/workflows/third_party.yml @@ -0,0 +1,337 @@ +# This workflow is a daily cron job, +# running the tests of various third-party libraries that use us. +# This helps us spot regressions early, +# and helps flag when third-party libraries are making incorrect assumptions +# that might cause them to break when we cut a new release. + +name: Third-party tests + +on: + schedule: + - cron: "30 2 * * *" # 02:30 UTC + pull_request: + paths: + - ".github/workflows/third_party.yml" + workflow_dispatch: + +permissions: + contents: read + +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + FORCE_COLOR: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + pydantic: + name: pydantic tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout pydantic + uses: actions/checkout@v3 + with: + repository: pydantic/pydantic + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup pdm for pydantic tests + uses: pdm-project/setup-pdm@v3 + with: + python-version: ${{ matrix.python-version }} + cache: true + - name: Add local version of typing_extensions as a dependency + run: pdm add ./typing-extensions-latest + - name: Install pydantic test dependencies + run: pdm install -G testing -G email + - name: List installed dependencies + run: pdm list -vv # pdm equivalent to `pip list` + - name: Run pydantic tests + run: pdm run pytest + + typing_inspect: + name: typing_inspect tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout typing_inspect + uses: actions/checkout@v3 + with: + repository: ilevkivskyi/typing_inspect + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install typing_inspect test dependencies + run: pip install -r test-requirements.txt + - name: Install typing_extensions latest + run: pip install ./typing-extensions-latest + - name: List all installed dependencies + run: pip freeze --all + - name: Run typing_inspect tests + run: pytest + + pyanalyze: + name: pyanalyze tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out pyanalyze + uses: actions/checkout@v3 + with: + repository: quora/pyanalyze + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install pyanalyze test requirements + run: pip install .[tests] + - name: Install typing_extensions latest + run: pip install ./typing-extensions-latest + - name: List all installed dependencies + run: pip freeze --all + - name: Run pyanalyze tests + run: pytest pyanalyze/ + + typeguard: + name: typeguard tests + if: false # TODO: unskip when typeguard's tests pass on typing_extensions>=4.6.0 + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out typeguard + uses: actions/checkout@v3 + with: + repository: agronholm/typeguard + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install typeguard test requirements + run: pip install -e .[test] + - name: Install typing_extensions latest + run: pip install ./typing-extensions-latest + - name: List all installed dependencies + run: pip freeze --all + - name: Run typeguard tests + run: pytest + + typed-argument-parser: + name: typed-argument-parser tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out typed-argument-parser + uses: actions/checkout@v3 + with: + repository: swansonk14/typed-argument-parser + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Configure git for typed-argument-parser tests + # typed-argument parser does this in their CI, + # and the tests fail unless we do this + run: | + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + - name: Install typed-argument-parser test requirements + run: | + pip install -e . + pip install pytest + - name: Install typing_extensions latest + run: pip install ./typing-extensions-latest + - name: List all installed dependencies + run: pip freeze --all + - name: Run typed-argument-parser tests + run: pytest + + stubtest: + name: stubtest tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout mypy for stubtest tests + uses: actions/checkout@v3 + with: + repository: python/mypy + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install mypy test requirements + run: | + pip install -r test-requirements.txt + pip install -e . + - name: Install typing_extensions latest + run: pip install ./typing-extensions-latest + - name: List all installed dependencies + run: pip freeze --all + - name: Run stubtest tests + run: pytest ./mypy/test/teststubtest.py + + cattrs: + name: cattrs tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout cattrs + uses: actions/checkout@v3 + with: + repository: python-attrs/cattrs + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install poetry for cattrs + run: pip install poetry + - name: Add latest typing-extensions as a dependency + run: poetry add ./typing-extensions-latest + - name: Install cattrs test dependencies + run: poetry install -v --all-extras + - name: List all installed dependencies + run: poetry show + - name: Run cattrs tests + run: poetry run pytest tests + + create-issue-on-failure: + name: Create an issue if daily tests failed + runs-on: ubuntu-latest + + needs: + - pydantic + - typing_inspect + - pyanalyze + - typeguard + - typed-argument-parser + - stubtest + - cattrs + + if: >- + ${{ + github.repository == 'python/typing_extensions' + && always() + && github.event_name == 'schedule' + && ( + needs.pydantic.result == 'failure' + || needs.typing_inspect.result == 'failure' + || needs.pyanalyze.result == 'failure' + || needs.typeguard.result == 'failure' + || needs.typed-argument-parser.result == 'failure' + || needs.stubtest.result == 'failure' + || needs.cattrs.result == 'failure' + ) + }} + + permissions: + issues: write + + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.create({ + owner: "python", + repo: "typing_extensions", + title: `Third-party tests failed on ${new Date().toDateString()}`, + body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", + }) From b8a2ece6c09ed72e962b5b4a2166262c5b3190d2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 2 Jun 2023 00:31:35 +0100 Subject: [PATCH 7/9] Third-party tests: cattrs has switched to pdm (#209) --- .github/workflows/third_party.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 19be0cf3..cde11c14 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -281,16 +281,18 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install poetry for cattrs - run: pip install poetry + - name: Install pdm for cattrs + run: pip install pdm - name: Add latest typing-extensions as a dependency - run: poetry add ./typing-extensions-latest + run: | + pdm remove typing-extensions + pdm add --dev ./typing-extensions-latest - name: Install cattrs test dependencies - run: poetry install -v --all-extras + run: pdm install --dev -G :all - name: List all installed dependencies - run: poetry show + run: pdm list -vv - name: Run cattrs tests - run: poetry run pytest tests + run: pdm run pytest tests create-issue-on-failure: name: Create an issue if daily tests failed From 22c0e70f34e9feffc0c04b8cb213a5d3f4dbbdec Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 2 Jun 2023 00:35:00 +0100 Subject: [PATCH 8/9] Backport CPython PR 105152 (#208) --- CHANGELOG.md | 7 ++ src/test_typing_extensions.py | 116 ++++++++++++++++++++++++++++------ src/typing_extensions.py | 36 +++++------ 3 files changed, 120 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e13e291..98d81f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Unreleased +- Fix a regression introduced in v4.6.0 in the implementation of + runtime-checkable protocols. The regression meant + that doing `class Foo(X, typing_extensions.Protocol)`, where `X` was a class that + had `abc.ABCMeta` as its metaclass, would then cause subsequent + `isinstance(1, X)` calls to erroneously raise `TypeError`. Patch by + Alex Waygood (backporting the CPython PR + https://github.com/python/cpython/pull/105152). - Sync the repository's LICENSE file with that of CPython. `typing_extensions` is distributed under the same license as CPython itself. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 24f51e65..f9c3389c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1698,7 +1698,7 @@ class NT(NamedTuple): skip_if_py312b1 = skipIf( sys.version_info == (3, 12, 0, 'beta', 1), - "CPython had a bug in 3.12.0b1" + "CPython had bugs in 3.12.0b1" ) @@ -1902,40 +1902,75 @@ def x(self): ... self.assertIsSubclass(C, P) self.assertIsSubclass(C, PG) self.assertIsSubclass(BadP, PG) - with self.assertRaises(TypeError): + + no_subscripted_generics = ( + "Subscripted generics cannot be used with class and instance checks" + ) + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(C, PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(C, PG[C]) - with self.assertRaises(TypeError): + + only_runtime_checkable_protocols = ( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols): issubclass(C, BadP) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols): issubclass(C, BadPG) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(P, PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(PG, PG[int]) + only_classes_allowed = r"issubclass\(\) arg 1 must be a class" + + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, P) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, PG) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, BadP) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, BadPG) + def test_protocols_issubclass_non_callable(self): class C: x = 1 + @runtime_checkable class PNonCall(Protocol): x = 1 - with self.assertRaises(TypeError): + + non_callable_members_illegal = ( + "Protocols with non-method members don't support issubclass()" + ) + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) PNonCall.register(C) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) + # check that non-protocol subclasses are not affected class D(PNonCall): ... + self.assertNotIsSubclass(C, D) self.assertNotIsInstance(C(), D) D.register(C) self.assertIsSubclass(C, D) self.assertIsInstance(C(), D) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(D, PNonCall) def test_no_weird_caching_with_issubclass_after_isinstance(self): @@ -1954,7 +1989,10 @@ def __init__(self) -> None: # as the cached result of the isinstance() check immediately above # would mean the issubclass() call would short-circuit # before we got to the "raise TypeError" line - with self.assertRaises(TypeError): + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): issubclass(Eggs, Spam) def test_no_weird_caching_with_issubclass_after_isinstance_2(self): @@ -1971,7 +2009,10 @@ class Eggs: ... # as the cached result of the isinstance() check immediately above # would mean the issubclass() call would short-circuit # before we got to the "raise TypeError" line - with self.assertRaises(TypeError): + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): issubclass(Eggs, Spam) def test_no_weird_caching_with_issubclass_after_isinstance_3(self): @@ -1992,7 +2033,10 @@ def __getattr__(self, attr): # as the cached result of the isinstance() check immediately above # would mean the issubclass() call would short-circuit # before we got to the "raise TypeError" line - with self.assertRaises(TypeError): + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): issubclass(Eggs, Spam) def test_protocols_isinstance(self): @@ -2028,13 +2072,24 @@ def __init__(self): for proto in P, PG, WeirdProto, WeirdProto2, WeirderProto: with self.subTest(klass=klass.__name__, proto=proto.__name__): self.assertIsInstance(klass(), proto) - with self.assertRaises(TypeError): + + no_subscripted_generics = ( + "Subscripted generics cannot be used with class and instance checks" + ) + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): isinstance(C(), PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): isinstance(C(), PG[C]) - with self.assertRaises(TypeError): + + only_runtime_checkable_msg = ( + "Instance and class checks can only be used " + "with @runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg): isinstance(C(), BadP) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg): isinstance(C(), BadPG) def test_protocols_isinstance_properties_and_descriptors(self): @@ -2435,12 +2490,13 @@ def __subclasshook__(cls, other): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + @skip_if_py312b1 def test_issubclass_fails_correctly(self): @runtime_checkable class P(Protocol): x = 1 class C: pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"): issubclass(C(), P) def test_defining_generic_protocols(self): @@ -2768,6 +2824,30 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(Y.__parameters__, ()) self.assertEqual(Y.__args__, (int, bytes, memoryview)) + @skip_if_py312b1 + def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self): + # Ensure the cache is empty, or this test won't work correctly + collections.abc.Sized._abc_registry_clear() + + class Foo(collections.abc.Sized, Protocol): pass + + # CPython gh-105144: this previously raised TypeError + # if a Protocol subclass of Sized had been created + # before any isinstance() checks against Sized + self.assertNotIsInstance(1, collections.abc.Sized) + + @skip_if_py312b1 + def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self): + # Ensure the cache is empty, or this test won't work correctly + collections.abc.Sized._abc_registry_clear() + + class Foo(typing.Sized, Protocol): pass + + # CPython gh-105144: this previously raised TypeError + # if a Protocol subclass of Sized had been created + # before any isinstance() checks against Sized + self.assertNotIsInstance(1, typing.Sized) + class Point2DGeneric(Generic[T], TypedDict): a: T diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9aa84d7e..1b92c396 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -547,7 +547,7 @@ def _caller(depth=2): Protocol = typing.Protocol runtime_checkable = typing.runtime_checkable else: - def _allow_reckless_class_checks(depth=4): + def _allow_reckless_class_checks(depth=3): """Allow instance and class checks for special stdlib modules. The abc and functools modules indiscriminately call isinstance() and issubclass() on the whole MRO of a user class, which may contain protocols. @@ -572,14 +572,22 @@ def __init__(cls, *args, **kwargs): ) def __subclasscheck__(cls, other): + if not isinstance(other, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') if ( getattr(cls, '_is_protocol', False) - and not cls.__callable_proto_members_only__ - and not _allow_reckless_class_checks(depth=3) + and not _allow_reckless_class_checks() ): - raise TypeError( - "Protocols with non-method members don't support issubclass()" - ) + if not cls.__callable_proto_members_only__: + raise TypeError( + "Protocols with non-method members don't support issubclass()" + ) + if not getattr(cls, '_is_runtime_protocol', False): + raise TypeError( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) return super().__subclasscheck__(other) def __instancecheck__(cls, instance): @@ -591,7 +599,7 @@ def __instancecheck__(cls, instance): if ( not getattr(cls, '_is_runtime_protocol', False) and - not _allow_reckless_class_checks(depth=2) + not _allow_reckless_class_checks() ): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") @@ -632,18 +640,6 @@ def _proto_hook(cls, other): if not cls.__dict__.get('_is_protocol', False): return NotImplemented - # First, perform various sanity checks. - if not getattr(cls, '_is_runtime_protocol', False): - if _allow_reckless_class_checks(): - return NotImplemented - raise TypeError("Instance and class checks can only be used with" - " @runtime_checkable protocols") - - if not isinstance(other, type): - # Same error message as for issubclass(1, int). - raise TypeError('issubclass() arg 1 must be a class') - - # Second, perform the actual structural compatibility check. for attr in cls.__protocol_attrs__: for base in other.__mro__: # Check if the members appears in the class dictionary... @@ -658,8 +654,6 @@ def _proto_hook(cls, other): isinstance(annotations, collections.abc.Mapping) and attr in annotations and issubclass(other, (typing.Generic, _ProtocolMeta)) - # All subclasses of Generic have an _is_proto attribute on 3.8+ - # But not on 3.7 and getattr(other, "_is_protocol", False) ): break From c57333b77603298e2d79fdaf7632e09e40d11f54 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 1 Jun 2023 16:39:37 -0700 Subject: [PATCH 9/9] Update version to 4.6.3 --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d81f94..ecaea2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Release 4.6.3 (June 1, 2023) - Fix a regression introduced in v4.6.0 in the implementation of runtime-checkable protocols. The regression meant diff --git a/pyproject.toml b/pyproject.toml index 74ec5ed0..3858e80d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.6.2" +version = "4.6.3" description = "Backported and Experimental Type Hints for Python 3.7+" readme = "README.md" requires-python = ">=3.7"