Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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/
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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 ]
Expand All @@ -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:
Expand All @@ -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]
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
31 changes: 25 additions & 6 deletions formerly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*.

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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),
Expand All @@ -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),
),
Expand Down Expand Up @@ -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(".")
)
33 changes: 33 additions & 0 deletions tests/test_formerly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ commands =
basepython = python3.10
deps =
{[testenv]deps}
mypy==2.3.0
mypy==2.4.0
commands =
mypy {posargs:formerly tests}

Expand All @@ -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/*
Loading