From aed03e6a4fb5cfd48e21f8c7640263cc7bea3aa2 Mon Sep 17 00:00:00 2001 From: Adrian Chaves Date: Thu, 8 Oct 2026 07:28:30 -0500 Subject: [PATCH 1/2] Add suggest_new, and skip suggesting private replacements by default --- README.rst | 4 ++++ formerly/__init__.py | 31 +++++++++++++++++++++++++------ tests/test_formerly.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 8f94194..7f08d7a 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,10 @@ involved. Both are configurable, along with the messages themselves: subclass_message="{cls} inherits from {old}, which is going away in 3.0.", ) +Messages only tell users to switch to the new class when its path, or +``new_path`` if given, has no ``_``-prefixed part. Pass ``suggest_new=True`` or +``suggest_new=False`` to decide that yourself. + By default only the first subclass warns, since one warning is enough to tell users to migrate. Pass ``warn_once=False`` to warn on every subclass. diff --git a/formerly/__init__.py b/formerly/__init__.py index 457d5d0..77458ed 100644 --- a/formerly/__init__.py +++ b/formerly/__init__.py @@ -18,10 +18,9 @@ def deprecated_class( warn_once: bool = True, old_path: str | None = None, new_path: str | None = None, - subclass_message: str = ( - "{cls} inherits from deprecated class {old}, please inherit from {new}." - ), - instance_message: str = "{cls} is deprecated, instantiate {new} instead.", + suggest_new: bool | None = None, + subclass_message: str | None = None, + instance_message: str | None = None, ) -> type[_T]: """Return a deprecated alias of *new_class* named *name*. @@ -44,6 +43,10 @@ class NewName(SomeClass): ... warning messages report, which is useful when either class is reexported from a different module. + The warning messages tell users to use *new_class* instead, unless its + path, or *new_path* if given, has a ``_``-prefixed part. Set + *suggest_new* to ``True`` or ``False`` to decide that yourself. + *subclass_message* and *instance_message* replace the warning messages. Both may use the ``{cls}``, ``{old}`` and ``{new}`` fields, filled with the path of the subclass or instantiated class, of the alias, and of @@ -52,6 +55,16 @@ class NewName(SomeClass): ... Set *warn_once* to ``False`` to warn on every subclass instead of only on the first one. """ + if suggest_new is None: + suggest_new = not _is_private(new_path or _path(new_class)) + subclass_template = subclass_message or ( + "{cls} inherits from deprecated class {old}" + + (", please inherit from {new}." if suggest_new else ".") + ) + instance_template = instance_message or ( + "{cls} is deprecated" + (", instantiate {new} instead." if suggest_new else ".") + ) + alias: type[_T] | None = None warned = False @@ -75,7 +88,7 @@ def __init__( ): assert alias is not None warned = True - message = subclass_message.format( + message = subclass_template.format( cls=_path(cls), old=old_path or _path(alias), new=new_path or _path(new_class), @@ -89,7 +102,7 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: if cls is alias: assert alias is not None warnings.warn( - instance_message.format( + instance_template.format( cls=old_path or _path(alias), new=new_path or _path(new_class), ), @@ -126,3 +139,9 @@ def __subclasscheck__(cls, subclass: type) -> bool: def _path(cls: type) -> str: return f"{cls.__module__}.{cls.__name__}" + + +def _is_private(path: str) -> bool: + return any( + part.startswith("_") and not part.endswith("__") for part in path.split(".") + ) diff --git a/tests/test_formerly.py b/tests/test_formerly.py index dca8a2f..ac8f4cb 100644 --- a/tests/test_formerly.py +++ b/tests/test_formerly.py @@ -94,6 +94,39 @@ class UserClass(Deprecated): ): Deprecated() + @pytest.mark.parametrize( + ("new_path", "suggest_new", "suggested"), + [ + (None, None, False), + ("foo._bar.Baz", None, False), + ("__main__.Baz", None, True), + ("foo.Baz", None, True), + (None, True, True), + ("foo.Baz", False, False), + ], + ) + def test_suggest_new(self, new_path, suggest_new, suggested): + class _Private(NewName): + pass + + Deprecated: Any = deprecated_class( + "Deprecated", + _Private, + new_path=new_path, + suggest_new=suggest_new, + category=MyWarning, + ) + + with pytest.warns(MyWarning) as record: + + class UserClass(Deprecated): + pass + + Deprecated() + + new = new_path or "_Private" + assert [new in str(r.message) for r in record] == [suggested] * 2 + def test_only_direct_subclasses_warn(self): Deprecated: Any = deprecated_class( "Deprecated", NewName, warn_once=False, category=MyWarning From a7525fbacbfc0bdf0838f0936bf7e65ba88eccde Mon Sep 17 00:00:00 2001 From: Adrian Chaves Date: Thu, 8 Oct 2026 07:34:19 -0500 Subject: [PATCH 2/2] Upgrade pinned tool versions --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish.yml | 4 ++-- .pre-commit-config.yaml | 6 +++--- tox.ini | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9dbf76..ddfac8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: cache-dependency-glob: | pyproject.toml @@ -79,11 +79,11 @@ jobs: - name: Upload coverage report if: ${{ matrix.coverage }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - name: Upload test results if: ${{ matrix.coverage && !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: report_type: test_results diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5db311..e5b389c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,7 +26,7 @@ jobs: - run: | python -m pip install --upgrade build python -m build - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: python-package-distributions path: dist/ @@ -42,7 +42,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: python-package-distributions path: dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63fe4f7..993fba7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.20 + rev: v0.16.10 hooks: - id: ruff-check args: [ --fix ] @@ -10,7 +10,7 @@ repos: hooks: - id: blacken-docs additional_dependencies: - - black==26.5.1 + - black==26.10.0 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: @@ -21,7 +21,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.28.0 + rev: v1.30.1 hooks: - id: zizmor args: [--no-progress, --fix] diff --git a/tox.ini b/tox.ini index 002742c..7d74424 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ commands = basepython = python3.10 deps = {[testenv]deps} - mypy==2.3.0 + mypy==2.4.0 commands = mypy {posargs:formerly tests} @@ -35,8 +35,8 @@ skip_install = true [testenv:twinecheck] deps = - build==1.5.0 - twine==6.2.0 + build==1.6.1 + twine==7.0.0 commands = python -m build --sdist twine check dist/*