You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Fixed audit scans to deduplicate re-exported wrappers without dropping wrappers from non-recursive scans or external defining modules
- Fixed audit report formatting so chained proxy targets are read without emitting warnings, consuming warning budget, or fabricating module paths
- Fixed CLI plain-directory scans so successful checks exit 0 and nested-file skip warnings are reported on stderr
- Fixed CLI aggregate status handling so status-rendering failures do not change the overall exit code
- Improved CLI version detection for packages whose import names differ from distribution names, with a note when no version can be resolved
- Fixed expiry checks to propagate user-package import failures instead of misreporting them as missing version metadata
- Updated CI workflow permissions and link-check handling for release artifact downloads and timeout responses
- Updated documentation for CLI/audit behavior, public exports, FAQ metadata, contribution examples, troubleshooting anchors, and audit examples
- Updated project policy to allow bare `assert` statements inside pytest test functions
- Updated changelog entries for the shipped CLI and audit behavior fixes
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@@ -393,13 +396,13 @@ Tests live in `tests/` and follow a **three-layer separation**:
393
396
> - Use `print()` for values you want to verify, paired with a `<details><summary>Output: <code>expression</code></summary>` block immediately after the code block. The `<summary>` label shows the **expression** being evaluated (e.g. `cfg.timeout`), not the `print()` wrapper.
394
397
> - Only import and use `pytest.raises` when an example intentionally raises an exception — this prevents the extracted test from crashing. Do **not** use `pytest.warns`; deprecation warnings are emitted to stderr and do not cause test failures.
395
398
> -**Never use `with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always")`** in any `.md` code block (README, docs, docstrings). Use direct calls annotated with `# warns: FutureWarning` or `# silent` instead. Output blocks show only return values — not warning counts or `w[0].category.__name__`.
396
-
> - Do **not** use bare `assert` statements — they crash the test with an unhelpful `AssertionError` if the value changes.
399
+
> - Do **not** use bare `assert` statements in top-level example code — they crash the test with an unhelpful `AssertionError` if the value changes. **Exception:** bare `assert` statements inside `def test_...` function bodies shown as pytest integration examples are allowed and idiomatic — the test function itself is the test.
397
400
> - Regenerate `test_readme.py` after any README change: `phmdoctest README.md --outfile tests/integration/test_readme.py`
398
401
399
402
> **Docs examples must use `print()` + output blocks — no `assert`.** For all `docs/**/*.md` blocks that execute code:
400
403
>
401
404
> - Use `print()` to display values; follow immediately with a `<details><summary>Output: <code>expression</code></summary>` block showing expected output.
402
-
> - Do **not** use bare `assert` statements (e.g. `assert pt.x == 1.0`, `assert isinstance(obj, MyClass)`) — use `print()` instead so the value is visible rather than crashing with `AssertionError`.
405
+
> - Do **not** use bare `assert` statements in top-level example code (e.g. `assert pt.x == 1.0`, `assert isinstance(obj, MyClass)`) — use `print()` instead so the value is visible rather than crashing with `AssertionError`. **Exception:** bare `assert` statements inside `def test_...` function bodies shown as pytest integration examples are allowed and idiomatic.
403
406
> - Avoid placeholders that do not validate behavior.
404
407
> -**Never import a fictional package name** in runnable examples — executable examples must import from actual test collection modules (`from tests import collection_deprecate`, `collection_misconfigured`, or `collection_chains`). For CI-template snippets that intentionally show a placeholder import, add `# phmdoctest:skip — CI template: replace my_package with your actual package` as the first line of the code block so phmdoctest skips execution.
405
408
@@ -469,7 +472,7 @@ Examples:
469
472
When adding a parametrized test that covers both forms, always add both fixtures and share the same `deprecated(...)` instance to guarantee identical configuration:
470
473
471
474
```python
472
-
from deprecate import deprecated, void
475
+
from deprecate importTargetMode, deprecated, void
473
476
474
477
475
478
# original_* is declared first — _deprecation_* refers to it immediately after.
>**Rule**: when a `Decorated<Name>`/`Wrapped<Name>` pair exists, both **must** share a single `_class_deprecation_<name>` instance. Duplicating the `deprecated_class(...)` kwargs is a bug — a silent config drift will cause the parametrized test to compare two different deprecations instead of the same one in two application forms.
528
531
529
-
**Unification pattern — shared version kwargs and hoisted instances:**
532
+
#### Unification pattern — shared version kwargs and hoisted instances
530
533
531
534
When three or more `@deprecated(...)`or`@deprecated_class(...)` call sites share the same `(deprecated_in, remove_in[, num_warns])` combination, extract the repeated kwargs into a named `dict` constant and splat it at each call site. This eliminates silent version drift and makes bulk version-bump changes a one-line edit.
532
535
@@ -566,16 +569,16 @@ Functions in `collection_deprecate.py`, `collection_misconfigured.py`, `collecti
566
569
Use a one-line summary of the deprecation pattern, then an `Examples:` section describing the user scenario:
Copy file name to clipboardExpand all lines: AGENTS.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,7 +90,7 @@ Write a clear explanation linking to both sources, then let maintainers decide o
90
90
-**New troubleshooting item**: add to `docs/troubleshooting.md` AND the FAQPage JSON-LD in `docs/overrides/main.html`
91
91
-**`docs/overrides/main.html`** is Jinja2 (prettier-excluded); do not put content files there
92
92
-**`docs/robots.txt`** — AI crawler access policy; add a `User-agent: <bot> / Allow: /` block when a new mainstream AI crawler is released. The comment referencing the `docs/llms.txt` URL must stay current.
93
-
- See [Documentation Site](.github/CONTRIBUTING.md#documentation-site) for the full consistency rules
93
+
- See [Documentation Site](.github/CONTRIBUTING.md#-documentation-site) for the full consistency rules
94
94
95
95
## 🚫 Critical Constraints
96
96
@@ -101,7 +101,7 @@ Write a clear explanation linking to both sources, then let maintainers decide o
101
101
- Use bare `except:` clauses
102
102
- Define deprecated wrappers inside test files
103
103
- Use `with warnings.catch_warnings(...)` in any `.md` documentation example in any form — neither `simplefilter("always")` for capturing nor `simplefilter("ignore", ...)` for suppressing; annotate the call with `# warns: FutureWarning` or `# warns: UserWarning` instead; output blocks show only return values
104
-
- Use bare `assert` statements in `.md` documentation examples (e.g. `assert pt.x == 1.0`, `assert isinstance(obj, MyClass)`) — use `print()` instead and follow with a `<details><summary>Output: <code>expression</code></summary>` block showing expected output
104
+
- Use bare `assert` statements in `.md` documentation examples outside of `def test_...` bodies (e.g. `assert pt.x == 1.0`, `assert isinstance(obj, MyClass)`) — use `print()` instead and follow with a `<details><summary>Output: <code>expression</code></summary>` block showing expected output. **Exception:** bare `assert` inside `def test_...` function bodies shown as pytest integration examples is allowed and idiomatic
105
105
- Import a fictional package name in runnable `.md` examples — executable examples must import from actual test collection modules (`from tests import collection_deprecate`, `collection_misconfigured`, or `collection_chains`). For CI-template snippets that intentionally show a placeholder import, add `# phmdoctest:skip — CI template: replace my_package with your actual package` as the first line so phmdoctest skips execution
106
106
- Skip test coverage for new features or bug fixes
107
107
- Implement features without maintainer approval
@@ -156,4 +156,4 @@ This file provides quick reference for agents. For complete, authoritative guide
Copy file name to clipboardExpand all lines: CHANGELOG.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,6 +22,14 @@
22
22
23
23
### Fixed
24
24
25
+
-**Audit scans no longer double-count re-exported wrappers.** Recursive `find_deprecation_wrappers` previously reported a wrapper once per importing module (e.g. once under the package root re-export and once under its defining submodule), inflating expiry counts and table rows; wrappers are now attributed to their defining module and deduplicated by identity across the scan. ([#215](https://github.com/Borda/pyDeprecate/pull/215))
26
+
27
+
-**Audit report formatting no longer triggers chained proxies.** Formatting a report for a wrapper whose `target` is itself a deprecated proxy previously emitted a spurious `FutureWarning` from inside the audit tooling, consumed the proxy's warn budget, and printed a fabricated module path; proxy targets are now read via static metadata access. ([#215](https://github.com/Borda/pyDeprecate/pull/215))
28
+
29
+
-**CLI: `all` and `status` no longer fail on plain directories.** Scanning a directory without `__init__.py` exited 1 from the status-table step even when every check passed; module-name resolution is now lazy and a status-rendering failure cannot change the aggregate exit code. ([#215](https://github.com/Borda/pyDeprecate/pull/215))
30
+
31
+
-**CLI: version auto-detection resolves distributions whose name differs from the import name.**`importlib.metadata` lookup previously failed for packages like pyDeprecate itself (import `deprecate`, distribution `pyDeprecate`); the import name is now mapped via `packages_distributions()`, and expiry prints an explicit note when it runs without a resolved version. ([#215](https://github.com/Borda/pyDeprecate/pull/215))
32
+
25
33
-**`num_warns` quota is now thread-safe.** Concurrent first calls to a shared wrapper could each pass the quota check before any counter increment, emitting up to one warning per thread instead of the configured budget; the warn path now synchronizes on a per-wrapper lock, so exactly `num_warns` warnings are emitted under concurrency. ([#214](https://github.com/Borda/pyDeprecate/pull/214))
26
34
27
35
-**Cross-class forwarding between staticmethods no longer raises a spurious `TypeError`.** The cross-class guard exists to prevent `self` carrying the wrong type, but staticmethods have no `self`; `@deprecated(target=NewCls.compute)` on a staticmethod forwarding to another class's staticmethod is now allowed. ([#214](https://github.com/Borda/pyDeprecate/pull/214))
Copy file name to clipboardExpand all lines: README.md
+6-4Lines changed: 6 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -996,7 +996,7 @@ red
996
996
997
997
</details>
998
998
999
-
Each deprecated attribute name has its own independent warning counter — with `num_warns=1`, both `color` and `size` each emit one warning, not one shared across all entries. See [Selective attribute deprecation](https://borda.github.io/pyDeprecate/stable/guide/use-cases.html#selective-attribute-deprecation) in the docs for write/delete redirect and Enum member alias examples.
999
+
Each deprecated attribute name has its own independent warning counter — with `num_warns=1`, both `color` and `size` each emit one warning, not one shared across all entries. See [Selective attribute deprecation](https://borda.github.io/pyDeprecate/stable/guide/classes.html#selective-attribute-deprecation) in the docs for write/delete redirect and Enum member alias examples.
1000
1000
1001
1001
**Dataclass auto-expand** — when the wrapped class is a `@dataclass`, a single `deprecated_class(attrs_mapping={"old": "new"})` call automatically warns on both attribute access (`obj.old`) and constructor kwargs (`DC(old=5)`). No need to set `args_mapping` separately for a field rename. For non-`@dataclass` targets, `attrs_mapping` covers attribute access only — also set `args_mapping` to cover constructor kwargs.
1002
1002
@@ -1972,7 +1972,7 @@ print(MyClass(42).x)
1972
1972
1973
1973
### ❗ TypeError: `Failed mapping`
1974
1974
1975
-
**Problem:**`TypeError: Failed mapping of 'my_func', arguments missing in target source: ['old_arg']`
1975
+
**Problem:**`TypeError: Failed mapping of 'my_func', arguments not accepted by target: ['old_arg']`
1976
1976
1977
1977
**Cause:** Your deprecated function has arguments that the target function doesn't accept.
1978
1978
@@ -2136,9 +2136,11 @@ True
2136
2136
2137
2137
### 📦 Deprecation Not Working Across Modules
2138
2138
2139
-
If you're moving functions to a different module or package, show the pattern rather than importing a non-existent package in the docs.
2139
+
**Problem:** The deprecation notice shows an unexpected path when a function is moved to a different module.
2140
2140
2141
-
The warning will correctly show the full path for real imports when used in your package.
2141
+
**Cause:** pyDeprecate resolves the target path from `target.__module__` and `target.__qualname__` at decoration time — not at call time. If the target is imported via an alias or re-export at the point where `@deprecated` is applied, the stored path reflects the alias, not the canonical location.
2142
+
2143
+
**Solution:** Import the target from its canonical module location before applying `@deprecated`. The deprecation notice will then show the fully-qualified path where the function actually lives.
0 commit comments