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
Copy file name to clipboardExpand all lines: .github/CONTRIBUTING.md
+11-1Lines changed: 11 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -374,7 +374,7 @@ Tests live in `tests/` and follow a **three-layer separation**:
374
374
> [!IMPORTANT]
375
375
> **README examples must be runnable.** Every Python code block in `README.md` is extracted by `phmdoctest` and executed as a test. Follow these rules when writing README examples:
376
376
>
377
-
> - Use `print()` for values you want to verify, paired with a `<details><summary>Output: ...</summary>` block immediately after the code block.
377
+
> - 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.
378
378
> - 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.
379
379
> - Do **not** use bare `assert` statements — they crash the test with an unhelpful `AssertionError` if the value changes.
380
380
> - Regenerate `test_readme.py` after any README change: `phmdoctest README.md --outfile tests/integration/test_readme.py`
-**One behavior per test** — each test method should verify one specific aspect.
487
487
-**Prefer parametrization for repetitive shapes** — when the setup/assertion flow is the same and only inputs/expected outputs differ, use `pytest.mark.parametrize(...)` to reduce duplication while keeping one behavioral intent per case.
488
488
-**Assertions on warnings:** Use `pytest.warns(FutureWarning|DeprecationWarning)` to verify deprecation warnings are emitted correctly.
489
+
-**Scenario description in docstrings** — every non-trivial test method must include a prose paragraph after the one-line summary that describes the real-world situation being tested. A one-line summary alone is not sufficient for complex tests.
490
+
491
+
```python
492
+
deftest_warns_on_read(self) -> None:
493
+
"""FutureWarning fires on property read access.
494
+
495
+
A user accesses a deprecated property on an existing object and
496
+
expects a FutureWarning with the original value still returned.
Copy file name to clipboardExpand all lines: AGENTS.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -75,6 +75,7 @@ Write a clear explanation linking to both sources, then let maintainers decide o
75
75
- Help onboard new contributors
76
76
- Ensure deprecation examples are documented in both README and `docs/guide/use-cases.md`
77
77
- Keep `docs/troubleshooting.md` and its FAQPage JSON-LD in `docs/overrides/main.html` in sync
78
+
- Every `docs/**/*.md` code block containing `print()` must be followed by a `<details><summary>Output: <code>…</code></summary>` block with verified output (see [Docs examples](.github/CONTRIBUTING.md#test-organization))
78
79
79
80
**Guidelines**:
80
81
@@ -121,6 +122,7 @@ Write a clear explanation linking to both sources, then let maintainers decide o
121
122
122
123
-**Three-layer separation**: targets in `collection_targets.py`, deprecated wrappers in `collection_deprecate.py`, test logic in `test_*.py`
123
124
-**Do not** define targets or `@deprecated` wrappers directly in test files
125
+
-**Scenario description required** — every non-trivial test method docstring must include a prose paragraph describing the real-world situation being tested; a one-line summary alone is not sufficient (see [Test Requirements](.github/CONTRIBUTING.md#-tests-and-quality-assurance))
124
126
- See [Test Organization](.github/CONTRIBUTING.md#test-organization) for details
Copy file name to clipboardExpand all lines: CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,8 @@
4
4
5
5
### Added
6
6
7
+
-**`@deprecated @property` now wraps `fset` and `fdel` with `FutureWarning`.** Applying `@deprecated` on the outside of `@property` (outer order, or explicit `deprecated(...)(property(fget, fset, fdel))`) now wraps all three accessors. Previously, only `fget` emitted a warning; `fset` and `fdel` were silently passed through. Consumers running `filterwarnings=error::FutureWarning` that wrote to or deleted a deprecated property will now see `FutureWarning` errors — use inner-order (`@property @deprecated`) or decorate only `fget` directly if you want a silent setter/deleter. Chain-style rebinding via `@value.setter` / `@value.deleter` is fully supported through the new `_DeprecatedProperty` subclass. ([#190](https://github.com/Borda/pyDeprecate/pull/190))
Copy file name to clipboardExpand all lines: README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -71,6 +71,7 @@ Another good aspect is not overwhelming users with too many warnings, so per fun
71
71
- 🚫 The deprecated function body is never executed when using `target`
72
72
- ⚡ Minimal runtime overhead with zero dependencies (Python standard library only)
73
73
- 🛠️ Supports deprecating callables: functions, methods, class constructors, **generator functions** (`def gen(): yield` — warning fires eagerly at call time, before iteration begins), and **`async def` coroutine functions** (wrapper is `async def`, `inspect.iscoroutinefunction(wrapper)` returns `True`; warning fires when the coroutine is awaited)
74
+
- 🏠 Supports deprecating `@property` — all three accessors (`fget`, `fset`, `fdel`) fire `FutureWarning`; chain-style `.setter()` / `.deleter()` rebinding preserved via `_DeprecatedProperty` subclass. Also supports `@cached_property` (getter-only — no `fset`/`fdel`).
74
75
- 📦 Supports deprecating classes, Enums, dataclasses, and module-level constants/objects via transparent proxies (`deprecated_class` for types; `deprecated_instance` for objects, with optional read-only enforcement — blocks standard collection mutators: `append`, `pop`, `update`, `clear`, etc.; custom mutator names bypass this guard)
75
76
- 📝 Optionally, docstrings can be updated automatically in RST/Sphinx or MkDocs/Markdown format (auto-detected); ships bundled Griffe and Sphinx doc-engine extensions
76
77
- 🔍 Preserves original function signature, annotations and metadata for introspection
@@ -976,7 +976,7 @@ Both decorator orders produce `classmethod(deprecated_wrapper)` or `staticmethod
976
976
977
977
## Properties and cached properties
978
978
979
-
`@deprecated` works with `@property` and `@cached_property` in either decorator order — the deprecation warning fires correctly at access time regardless of which order the decorators were applied.
979
+
`@deprecated` works with `@property` and `@cached_property`. The decorator only adds a `FutureWarning` at access time — it does **not** forward reads or writes to another property. For a getter-only property, either decorator order is valid. To add a warning to all three accessors (`fget`, `fset`, `fdel`) so that read, write, **and** delete each fire `FutureWarning`, place `@deprecated` on the **outside** (`@deprecated @property` order, or explicit `deprecated(...)(property(fget, fset, fdel))`). The inner-first order (`@property @deprecated`) only adds a warning to `fget` — apply `@deprecated` to setter and deleter separately if you also need them to warn.
980
980
981
981
```python
982
982
from functools import cached_property
@@ -985,48 +985,201 @@ from deprecate import deprecated
985
985
986
986
987
987
classConfig:
988
-
# @deprecated inside @property — conventional order
989
988
@property
990
989
@deprecated(deprecated_in="1.0", remove_in="2.0")
991
990
deftimeout(self) -> int:
992
991
return30
993
992
994
-
# @deprecated outside @property — also works
995
-
@deprecated(deprecated_in="1.0", remove_in="2.0")
996
-
@property
997
-
defretries(self) -> int:
998
-
return3
999
-
1000
-
# @deprecated inside @cached_property — conventional order
1001
993
@cached_property
1002
994
@deprecated(deprecated_in="1.0", remove_in="2.0")
1003
995
defbase_url(self) -> str:
1004
996
return"https://example.com"
1005
997
1006
-
# @deprecated outside @cached_property — also works
The `FutureWarning` fires on **attribute access** (`obj.timeout`), not on a call. For `@cached_property`, the warning fires on **first access only** — subsequent accesses return the cached value without emitting another warning.
1012
+
1013
+
!!! tip "Decorator order for getter-only properties"
1014
+
1015
+
Either `@property @deprecated` (inner) or `@deprecated @property` (outer) order works for getter-only properties. Inner order is conventional — the deprecated decorator is closer to the `def`. For properties with a setter or deleter, use outer order; see the next section.
1016
+
1017
+
### Deprecating a property with a setter or deleter
1018
+
1019
+
When the property being deprecated has a setter or deleter, all three accessors (`fget`, `fset`, `fdel`) are wrapped automatically — each fires a `FutureWarning`. Both the chain-style decorator pattern and the explicit construction pattern work:
1020
+
1021
+
```python
1022
+
from deprecate import deprecated
1023
+
1024
+
1025
+
classConfig:
1026
+
def__init__(self) -> None:
1027
+
self._timeout: int=30
1028
+
1029
+
# Outer order required: @deprecated @property wraps fget, fset, and fdel
1007
1030
@deprecated(deprecated_in="1.0", remove_in="2.0")
1008
-
@cached_property
1009
-
deflegacy_url(self) -> str:
1010
-
return"https://old.example.com"
1031
+
@property
1032
+
deftimeout(self) -> int:
1033
+
returnself._timeout
1034
+
1035
+
@timeout.setter
1036
+
deftimeout(self, value: int) -> None:
1037
+
self._timeout = value
1011
1038
1039
+
@timeout.deleter
1040
+
deftimeout(self) -> None:
1041
+
delself._timeout
1012
1042
1013
-
print(Config().legacy_url)
1043
+
1044
+
cfg = Config()
1045
+
cfg.timeout =10# FutureWarning: write
1046
+
print(cfg.timeout) # FutureWarning: read; prints 10
The `FutureWarning` fires on **attribute access** (`obj.timeout`), not on a call. For `@cached_property`, the warning fires on **first access only** — subsequent accesses return the cached value without emitting another warning.
1059
+
`obj.timeout` fires `FutureWarning` on **read**, `obj.timeout = value` fires on **write**, and `del obj.timeout` fires on **delete**.
1060
+
1061
+
!!! tip "Want only the getter to warn?"
1062
+
1063
+
If you want the setter or deleter to remain silent, apply `@deprecated` directly to `fget` using inner order (`@property @deprecated`) instead of wrapping the full `property` object.
1064
+
1065
+
!!! tip "Testing each accessor independently"
1066
+
1067
+
Each accessor (`fget`, `fset`, `fdel`) has its own warning counter — assert read, write, and delete warnings in separate `pytest.warns` blocks, or use `num_warns=-1` to disable per-accessor deduplication.
1068
+
1069
+
The explicit `property(fget, fset[, fdel])` construction also works:
!!! note "Audit discoverability with explicit construction"
1095
+
1096
+
`find_deprecation_wrappers` discovers explicit-construction properties via the accessor that carries `__deprecated__` metadata. For setter-only properties (`property(None, fset)`), it discovers via `fset`; if `fget` is plain (not deprecated), it falls through to `fset` or `fdel`.
1097
+
1098
+
### Deprecated property alias on a dataclass
1099
+
1100
+
When a dataclass field is renamed, define a property with the old name that delegates to the new field in its accessor body. `@deprecated` adds a `FutureWarning` to each accessor — the delegation itself is plain Python in the method body, not something the library provides.
1101
+
1102
+
**Read-only alias (warns on read only):**
1103
+
1104
+
```python
1105
+
from dataclasses import dataclass
1106
+
1107
+
from deprecate import deprecated
1108
+
1109
+
1110
+
@dataclass
1111
+
classConfig:
1112
+
timeout_ms: int=30_000# renamed from `timeout`
1113
+
1114
+
@property
1115
+
@deprecated(deprecated_in="1.0", remove_in="2.0")
1116
+
deftimeout(self) -> int:
1117
+
"""Deprecated — use ``timeout_ms`` instead."""
1118
+
returnself.timeout_ms //1000
1119
+
1120
+
1121
+
cfg = Config(timeout_ms=5_000)
1122
+
print(cfg.timeout) # FutureWarning fired; prints 5
`cfg.timeout` fires `FutureWarning` (from `@deprecated`) and the getter body returns `cfg.timeout_ms // 1000`. `cfg.timeout = 5` fires `FutureWarning` and the setter body assigns `cfg.timeout_ms = 5000`.
1173
+
1174
+
!!! warning "Do not shadow a dataclass field"
1175
+
1176
+
Do **not** use the same name as an existing dataclass field for the deprecated property. The `@dataclass`-generated `__init__` performs `self.field = value`, which conflicts with a property descriptor of the same name. Use a different name for the deprecated alias and keep the dataclass field under its new name.
1177
+
1178
+
The same pattern works on regular (non-dataclass) classes — replace field access with `self._attr` lookups in the accessor body. `@deprecated` only adds the warning in either case.
1026
1179
1027
-
!!! tip "Prefer `@property @deprecated` (deprecated closer to `def`)"
1180
+
!!! note "`target=<callable>` not supported on properties"
1028
1181
1029
-
The inner-first order is the conventional Python style. Follow this pattern for consistency if your team has no existing convention.
1182
+
`@deprecated` rejects `target=<callable>` on a `property` with `TypeError`. Properties have three independent accessors (`fget`, `fset`, `fdel`); there is no single callable to forward to. Delegate in each accessor body as shown above.
0 commit comments