Skip to content

Deprecate PermitException for removal in 4.0, warning only when it is read - #144

Draft
zeevmoney wants to merge 19 commits into
per-16177/wire-testsfrom
per-16331/deprecate-permit-exception
Draft

zeevmoney wants to merge 19 commits into
per-16177/wire-testsfrom
per-16331/deprecate-permit-exception

Conversation

@zeevmoney

@zeevmoney zeevmoney commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Linear issues

  • PER-16331: Deprecate PermitException for removal in permit 4.0, warning only when user code reads it.

Why

Base: #143 (stacked on #142 down to #128). This is the last 3.x PR of the stack.

permit 4.0 removes PermitException and makes PermitConnectionError a direct subclass of PermitError (PER-16236), and 3.x has to say so. On the base, PermitException carried a runtime @deprecated("Use PermitError instead") whose message does not mention 4.0. Reading the name did not warn. Instantiating or subclassing the class did, and so did subclassing PermitConnectionError, which is not deprecated. import permit was already silent, because the SDK created its own subclass under catch_warnings.

What changed

  • permit/exceptions.py: PermitException keeps its public name and carries the PEP 702 marker (typing_extensions.deprecated) with category=None and this message: "PermitException is deprecated and will be removed in permit 4.0; catch PermitConnectionError instead (in 4.0 it becomes a PermitError)." Type checkers flag it, and the class issues no warning of its own at runtime. The SDK refers to it as _PermitException. At runtime the public name is removed from the module dict and served by a PEP 562 __getattr__, which warns. The warning text is read from the marker's __deprecated__, so type checkers and the runtime show the same string.
  • permit/__init__.py: the same __getattr__ serves permit.PermitException and from permit import PermitException, and a TYPE_CHECKING re-export lets type checkers see the deprecated name on the package.
    • Neither __getattr__ is visible to type checkers, so a misspelled name on permit or permit.exceptions is still an error.
    • Neither module overrides __dir__. help(), inspect.getmembers() and mock.create_autospec() read every name dir() lists, so they never read PermitException.
  • permit/utils/deprecation.py: _warn_deprecated_name() attributes the warning to the reading line (stacklevel 3). It skips importlib's _handle_fromlist check, so from permit import PermitException warns once, not twice.
  • __all__: permit and permit.exceptions now define __all__, listing every SDK name a star import bound before, plus PermitException. So from permit import * and from permit.exceptions import * still bind PermitException, and a handler for it after a star import still catches PermitConnectionError. The star import reads the name through the module __getattr__, so it warns once, at the star-import line, even when the code never uses the name; importing the names the code uses avoids the warning. A test fails when a public name is missing from __all__.
  • Static marker: the PER-16331 plan declares PermitException under TYPE_CHECKING. Here the one runtime class carries the marker instead. mypy recognises the marker only when its message is a string literal, so this keeps one class and one message.
  • Typed surface:
    • tests/type_check/mypy.ini enables mypy's deprecated error code; strict alone does not report PEP 702 deprecations.
    • tests/type_check/consumer.py imports PermitException from permit.exceptions and reads permit.PermitException, each with a # type: ignore[deprecated] that must stay used. It also checks that missing names on both modules stay attr-defined errors.
    • The sync stub declares no exceptions and is unchanged.
  • Docs:
    • README Deprecations lists PermitException as the next 4.0 removal. The entry says which reads warn, what a star import does, and the filter for the new message.
    • MIGRATION.md and the migration skill drop ignore:Use PermitError instead from their -W error commands. They keep it as a note for the releases where import permit itself warns: permit 2.7.0 to 3.0.0 in MIGRATION.md, permit 3.0.0 in the skill.
    • MIGRATION.md's filter list gains the new message.
    • No CI workflow, pyproject setting or test had the old filter on the base.
  • Tests: tests/test_fix_permit_exception_deprecation.py (26 tests) replaces 5 tests in tests/test_offline_regressions.py that were written for the runtime decorator.

Behaviour changes

  • Each read of PermitException warns once, at the reading line. That covers from permit import PermitException, from permit.exceptions import PermitException, permit.PermitException, permit.exceptions.PermitException, getattr and hasattr.
    • A read warns only when its line runs: an except permit.PermitException: clause only when an exception reaches it, and an annotation never on Python 3.14, which does not evaluate it.
    • Later uses of an imported name do not warn.
  • Instantiating, raising or subclassing the class no longer warns by itself (the base warned "Use PermitError instead"). Subclassing PermitConnectionError no longer warns.
  • A star import of permit or permit.exceptions warns once (the PermitException deprecation) and binds the same SDK names as before. It no longer binds names the modules import for their own use: typing, datetime, enum, uuid, http, functools, pydantic and aiohttp names, permit.exceptions' type variables P and R, PYDANTIC_VERSION, sdk_logger, and the package's submodules.
  • dir(permit) and dir(permit.exceptions) no longer list PermitException.
  • Warning filters for "Use PermitError instead" no longer match. The new filter is ignore:PermitException is deprecated:DeprecationWarning.
  • PermitException.__deprecated__, which PermitConnectionError inherits, holds the new message.
  • Pickling or unpickling a bare PermitException instance warns, because pickle looks the class up by its public name; the pickle bytes are unchanged. PermitConnectionError pickles without a warning.
  • Unchanged:
    • except PermitException still catches PermitConnectionError, and isinstance still holds.
    • PermitConnectionError.__mro__, reprs and pickle bytes are the same.
    • import permit emits no warning. On pydantic 1 it still issues only "Support for pydantic 1 is deprecated".

Release notes

PermitException is deprecated and will be removed in permit 4.0: catch PermitConnectionError instead. Importing PermitException, or reading permit.PermitException or permit.exceptions.PermitException, now warns at that line, and mypy (--enable-error-code deprecated) and pyright (strict) flag it. import permit does not warn. A star import of permit or permit.exceptions still binds PermitException and warns once at the star-import line. Filters for the old message, "Use PermitError instead", no longer match; use ignore:PermitException is deprecated:DeprecationWarning. permit 4.0 moves PermitConnectionError under PermitError and removes PermitException.

How it was tested

  • Offline suite (pytest -m "not e2e"): 1912 passed, 3 skipped, 0 warnings on each pydantic lane. The base had 1891 passed and 3 skipped.
  • tests/test_typing_surface.py: 4 passed on each lane. mypy: no issues in 122 source files on each lane.
  • The PR's test files (test_fix_permit_exception_deprecation.py, test_typing_surface.py, test_offline_regressions.py): 125 passed on Python 3.10, 3.12 and 3.14, on both lanes.
  • python -W error::DeprecationWarning -c 'import permit' on Python 3.10, 3.12 and 3.14: silent on pydantic 2. On pydantic 1 the only warning recorded is "Support for pydantic 1 is deprecated".
  • pyright 1.1.411 in strict mode on a probe file:
    • flags both import lines, permit.PermitException, permit.exceptions.PermitException and an except PermitException clause;
    • does not flag subclassing PermitConnectionError;
    • still reports permit.NoSuchName as an error.
      pyright is not a repo check.
  • Mutation check: 21 mutations of the runtime code, the helper and the type-check config, all caught by the tests. They cover:
    • the import warning returning;
    • a wrong message, or a marker message that is not a literal;
    • each guard of the helper, and stacklevel;
    • the name cached after the first read, or kept in the module dict;
    • either __getattr__ serving nothing, not warning, or visible to type checkers;
    • __dir__ added back;
    • PermitConnectionError moved under PermitError;
    • the package re-export removed;
    • the consumer's deprecated error code removed.
  • Repo checks:
    • pre-commit (all hooks), uv lock --check, actionlint and zizmor (no findings) pass.
    • CI script tests: 223 passed.
    • Migration skill tests: 86 passed, 1 skipped on each lane.
    • API coverage report, run as the API Coverage job runs it with the offline record: exit 0.
  • e2e: 45 tests collected and type-checked, not run locally; CI runs them.

Owner actions before merge

  • Let CI run the e2e, compatibility and floor lanes.
  • In the test-harnesses repository, drop the python-sdk harness's ignore:Use PermitError instead:DeprecationWarning filter (separate PR there).
  • Decide whether the "Use PermitError instead" filter notes stay in MIGRATION.md (permit 2.7.0 to 3.0.0) and the migration skill (permit 3.0.0).

🤖 Generated with Claude Code

zeevmoney and others added 11 commits October 2, 2026 08:32
permit 4.0 removes PermitException (PER-16331). It carried typing_extensions'
@deprecated("Use PermitError instead"), which did not mention 4.0 and warns at
runtime on every subclass: the SDK silenced it for PermitConnectionError, and
code that subclassed PermitConnectionError still got it.

The class now carries the PEP 702 marker with category=None, so type checkers
flag every use, with the new message, and the class issues no warning itself.
permit.exceptions binds it under the private name _PermitException, which the
SDK uses. A module __getattr__ in permit and in permit.exceptions serves the
public name, and each read warns once, at the line that reads it:

    PermitException is deprecated and will be removed in permit 4.0; catch
    PermitConnectionError instead (in 4.0 it becomes a PermitError).

`from package import name` reads the name twice; the first read, importlib's
_handle_fromlist checking that the package has it, does not warn. Type
checkers do not see the __getattr__ functions, which would make them accept
any name on the two modules.

Unchanged: PermitConnectionError still subclasses the class, so `except
PermitException` and isinstance checks hold, and the MRO, repr and pickles are
the same. dir() still lists the name. `import permit` and star imports do not
warn, so a star import no longer binds PermitException.

The new tests replace the ones written for the runtime decorator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The consumer type check now imports PermitException, a line that must stay a
deprecation error, and catches it, which still type-checks. mypy reports PEP
702 deprecations only under the deprecated error code, which strict does not
enable, so the consumer's mypy.ini enables it: it is the strictest setting a
user can run.

Two more lines must stay errors: a name that permit, or permit.exceptions,
does not have. Type checkers that saw the modules' __getattr__ would accept
any name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It is the next removal in permit 4.0, which makes PermitConnectionError a
direct subclass of PermitError. The entry says what to catch instead, where
the warning appears, which type checkers flag it, and that a star import no
longer binds the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`import permit` no longer warns, so the guide's and the migration skill's
`pytest -W error::DeprecationWarning` run needs no
`ignore:Use PermitError instead` filter. Both now say that the run also fails
on each line that names PermitException, and keep the filter only for
projects on permit 3.0.0 or a 2.x release that deprecated PermitException,
where `import permit` itself warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The consumer type check read PermitException only from
permit.exceptions. permit re-exports it for type checkers under
TYPE_CHECKING, since its __getattr__ is hidden from them; dropping that
re-export would turn `from permit import PermitException` into an
attr-defined error, and the check still passed. Read it through the
package too, as a line that must stay a deprecation error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MIGRATION.md said "permit 3.0.0 and the later 2.x releases" warn on
`import permit`; the subclass that warns has been there since 2.7.0, so
name the range. The README's PermitException entry said importing permit
does not warn, right below the entry that says it does on pydantic 1:
say that `import permit` does not issue this warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
help(), inspect.getmembers() and mock.create_autospec() read every name
dir() lists. With PermitException listed, introspecting permit or
permit.exceptions issued its deprecation warning in code that never
names it, and raised under -W error. Neither module overrides __dir__
now, and a test checks that introspecting them does not warn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fresh-interpreter tests reach _warn_deprecated_name's importlib
check only as importlib._bootstrap, because importing permit imports
importlib, which renames _frozen_importlib. A direct test now runs the
helper under each bootstrap module name, and checks that a function
named _handle_fromlist elsewhere, or other importlib code, still warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs said every line that names PermitException warns. Only an
import of it, or a read of permit.PermitException or
permit.exceptions.PermitException, warns, and only when that line runs:
later uses of an imported name do not. The README and MIGRATION.md also
give the filter for the new message, since filters for "Use PermitError
instead" do not match it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Binding the name in a star import would read it, and the read warns, so
from permit import * no longer binds it. Code that star-imports permit
and catches PermitException raises NameError when an exception reaches
that handler. The README and MIGRATION.md now say so, and what to catch
instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 2, 2026

Copy link
Copy Markdown

PER-16331

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Dependency Security Audit

Scanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major)

✅ No known vulnerabilities found.

Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL.

zeevmoney and others added 8 commits October 2, 2026 17:49
permit and permit.exceptions serve PermitException through a module
__getattr__ (PER-16331), so the name is not in their globals, and a
star import of either module stopped binding it. Code that catches
PermitException after `from permit import *` raised NameError when an
exception reached the handler.

Both modules now have an __all__ that lists the names a star import
bound before, plus PermitException, so the star import binds it
through __getattr__ and warns once, at the star-import line. It
leaves out what the modules import for their own use: standard
library, typing, pydantic and aiohttp names, permit.exceptions' type
variables, PYDANTIC_VERSION and sdk_logger, and the submodules the
import system binds in the package.

The tests check that each module lists every public name it binds
and nothing it cannot serve, and that a star import binds the name,
warns once at its line and catches a PermitConnectionError, in a
fresh interpreter, with or without either client imported first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With __all__ in permit/__init__.py, mypy and pyright take the
package's public names from it, so the redundant `X as X` aliases no
longer mark anything. mypy strict and pyright strict report the same
diagnostics on the consumer probes with and without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A star import of permit or permit.exceptions binds PermitException
again (PER-16331), so a handler for it after a star import no longer
raises NameError. The README, the migration guide and the migration
skill now say that the star import warns once, at its line, even
where the code never uses the name, and that importing the names the
code uses avoids it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After regenerating permit/api/models.py, a new model reaches a star
import of permit, and type checkers, only once __all__ lists it. The
guide's regeneration steps now say so, and where a name the models
import for their own use goes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zeevmoney
zeevmoney added this pull request to stack #145 October 2, 2026 18:24

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant