Feature
Add a way to report [deprecated] only for deprecated symbols defined in selected packages, the inverse of deprecated_calls_exclude:
[tool.mypy]
enable_error_code = ["deprecated"]
deprecated_calls_include = ["mylib"]
With this, using mylib.old_func anywhere (in mylib itself, its tests, its examples) is an error, while deprecations defined in third-party packages or the stdlib are not reported. Matching would work exactly like deprecated_calls_exclude (prefix on the fully qualified name of the definition), and deprecated_calls_exclude would still apply on top of it, so mylib can be included while mylib.compat is excluded.
Pitch
This is the use case of a library that deprecates its own API:
- Its own code must never use its own deprecated symbols. Otherwise it ships code that emits
DeprecationWarnings that downstream users can do nothing about. This should fail CI.
- Deprecations in its dependencies must NOT fail CI. Libraries are tested against dependency ranges, so a new upstream release that adds a
@deprecated breaks every unrelated PR on the next CI run. Often the fix is not even possible yet: when the replacement only exists in the release that deprecated the old name, switching to it means raising the lower bound of the dependency, which is a decision of its own.
At runtime we already get exactly this split with a pytest filterwarnings = ["error:.*mylib\..* is deprecated:DeprecationWarning"] entry. Statically, none of the current options can express it:
report_deprecated_as_note is global: it makes our own deprecations non-fatal too.
- Per-module overrides select the module being checked, not the module that defines the deprecated symbol, so they can't tell the two cases apart.
deprecated_calls_exclude can, but only by listing every dependency. That list has to include transitive packages reached through inheritance (a deprecated method inherited from a base class in another package is reported with that package's fully qualified name), test-only dependencies, and one entry per distribution when several share a namespace package (in our case a family of libraries sharing the frequenz.* namespace, so excluding frequenz would hide our own deprecations as well). The list has to be updated whenever dependencies change, and forgetting an entry brings back the exact CI break it is meant to prevent. An include list, on the other hand, is a single entry that never changes.
deprecated_calls_exclude was added (#18641) for the opposite use case: a library excluding its own package because its tests exercise its deprecated API (#18435, and this comment in #18192). Both cases seem common for libraries, and only this one has no reasonable configuration today.
The change should be small: it touches the two places that check deprecated_calls_exclude, TypeChecker.warn_deprecated and TypeAnalyser.check_and_warn_deprecated, plus the option, docs and tests. I'd be happy to send a PR if this is acceptable.
A possible generalization is a per-package severity. An include list makes dependency deprecations silent, but what would fit even better is keeping them visible but non-fatal, for example with report_deprecated_as_note = true plus a way to report mylib as an error anyway. That is close to what @JelleZijlstra suggested in #18435 (comment) (a pattern mapped to a behavior, such as ignore, note or error); deprecated_calls_include would then be the special case "everything else is ignored". I know notes without errors were questioned in #18192, so I'm not attached to this part and it could be split into a separate issue; the include list alone already solves the CI problem.
Having something like this might also make it easier to enable deprecated by default, as announced in the 1.14 release notes, since projects could keep deprecations they don't control from failing their checks.
Feature
Add a way to report
[deprecated]only for deprecated symbols defined in selected packages, the inverse ofdeprecated_calls_exclude:With this, using
mylib.old_funcanywhere (inmylibitself, its tests, its examples) is an error, while deprecations defined in third-party packages or the stdlib are not reported. Matching would work exactly likedeprecated_calls_exclude(prefix on the fully qualified name of the definition), anddeprecated_calls_excludewould still apply on top of it, somylibcan be included whilemylib.compatis excluded.Pitch
This is the use case of a library that deprecates its own API:
DeprecationWarnings that downstream users can do nothing about. This should fail CI.@deprecatedbreaks every unrelated PR on the next CI run. Often the fix is not even possible yet: when the replacement only exists in the release that deprecated the old name, switching to it means raising the lower bound of the dependency, which is a decision of its own.At runtime we already get exactly this split with a pytest
filterwarnings = ["error:.*mylib\..* is deprecated:DeprecationWarning"]entry. Statically, none of the current options can express it:report_deprecated_as_noteis global: it makes our own deprecations non-fatal too.deprecated_calls_excludecan, but only by listing every dependency. That list has to include transitive packages reached through inheritance (a deprecated method inherited from a base class in another package is reported with that package's fully qualified name), test-only dependencies, and one entry per distribution when several share a namespace package (in our case a family of libraries sharing thefrequenz.*namespace, so excludingfrequenzwould hide our own deprecations as well). The list has to be updated whenever dependencies change, and forgetting an entry brings back the exact CI break it is meant to prevent. An include list, on the other hand, is a single entry that never changes.deprecated_calls_excludewas added (#18641) for the opposite use case: a library excluding its own package because its tests exercise its deprecated API (#18435, and this comment in #18192). Both cases seem common for libraries, and only this one has no reasonable configuration today.The change should be small: it touches the two places that check
deprecated_calls_exclude,TypeChecker.warn_deprecatedandTypeAnalyser.check_and_warn_deprecated, plus the option, docs and tests. I'd be happy to send a PR if this is acceptable.A possible generalization is a per-package severity. An include list makes dependency deprecations silent, but what would fit even better is keeping them visible but non-fatal, for example with
report_deprecated_as_note = trueplus a way to reportmylibas an error anyway. That is close to what @JelleZijlstra suggested in #18435 (comment) (a pattern mapped to a behavior, such asignore,noteorerror);deprecated_calls_includewould then be the special case "everything else is ignored". I know notes without errors were questioned in #18192, so I'm not attached to this part and it could be split into a separate issue; the include list alone already solves the CI problem.Having something like this might also make it easier to enable
deprecatedby default, as announced in the 1.14 release notes, since projects could keep deprecations they don't control from failing their checks.