Skip to content

Commit dde8597

Browse files
committed
New wrapper
1 parent 6dfeaf8 commit dde8597

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

pvlib/_deprecation.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,193 @@ def wrapper(*args, **kwargs):
389389
return wrapper
390390

391391
return deprecate
392+
393+
394+
def renamed_key_items_warning(since, old_to_new_keys_map, removal=""):
395+
"""
396+
Decorator to mark a possible key item (e.g. ``df["key"]``) of an object as
397+
deprecated and replaced with other attribute.
398+
399+
Raises a warning when the deprecated attribute is used, and uses the new
400+
attribute instead, by wrapping the ``__getattr__`` method of the object.
401+
See [1]_.
402+
403+
While this implementation is decorator-like, Python syntax won't allow
404+
``@decorator`` for applying it. Two sets of parenthesis are required:
405+
the first one configures the wrapper and the second one applies it.
406+
This leaves room for reusability too.
407+
408+
Code is inspired by [2]_, thou it has been generalized to arbitrary data
409+
types.
410+
411+
.. warning::
412+
Ensure ``removal`` date with a ``fail_on_pvlib_version`` decorator in
413+
the test suite.
414+
415+
.. note::
416+
This works for any object that implements a ``__getitem__`` method,
417+
such as dictionaries, DataFrames, and other collections.
418+
419+
Parameters
420+
----------
421+
since : str
422+
The release at which this API became deprecated.
423+
old_to_new_keys_map : dict
424+
A dictionary mapping old keys to new keys.
425+
removal : str, optional
426+
The expected removal version, in order to compose the Warning message.
427+
428+
Returns
429+
-------
430+
object
431+
A new object that behaves like the original, but raises a warning
432+
when accessing deprecated keys and returns the value of the new key.
433+
434+
Examples
435+
--------
436+
>>> dict_obj = {"new_key": "renamed_value", "another_key": "another_value"}
437+
>>> dict_obj = renamed_key_items_warning(
438+
... "1.4.0", {"old_key": "new_key"}
439+
... )(dict_obj)
440+
>>> dict_obj["old_key"]
441+
pvlibDeprecationWarning: Please use `new_key` instead of `old_key`. \
442+
Deprecated since 1.4.0 and will be removed soon.
443+
'renamed_value'
444+
>>> isinstance(d, dict)
445+
True
446+
>>> type(dict_obj)
447+
<class 'pvlib._deprecation.DeprecatedKeyItems'>
448+
449+
>>> dict_obj = {"new_key": "renamed_value", "new_key2": "another_value"}
450+
>>> dict_obj = renamed_key_items_warning(
451+
... "1.4.0", {"old_key": "new_key", "old_key2": "new_key2"}, "1.6.0"
452+
... )(dict_obj)
453+
>>> dict_obj["old_key2"]
454+
pvlibDeprecationWarning: Please use `new_key2` instead of `old_key2`. \
455+
Deprecated since 1.4.0 and will be removed in 1.6.0.
456+
'another_value'
457+
458+
You can even chain the decorator to rename multiple keys at once:
459+
460+
>>> dict_obj = {"new_key1": "value1", "new_key2": "value2"}
461+
>>> dict_obj = renamed_key_items_warning(
462+
... "0.1.0", {"old_key1": "new_key1"}, "0.2.0"
463+
... )(dict_obj)
464+
>>> dict_obj = renamed_key_items_warning(
465+
... "0.3.0", {"old_key2": "new_key2"}, "0.4.0"
466+
... )(dict_obj)
467+
>>> dict_obj["old_key1"]
468+
pvlibDeprecationWarning: Please use `new_key1` instead of `old_key1`. \
469+
Deprecated since 0.1.0 and will be removed in 0.4.0.
470+
'value1'
471+
>>> dict_obj["old_key2"]
472+
pvlibDeprecationWarning: Please use `new_key2` instead of `old_key2`. \
473+
Deprecated since 0.3.0 and will be removed in 0.4.0.
474+
'value2'
475+
476+
Reusing the object wrapper factory:
477+
478+
>>> dict_obj1 = {"new_key": "renamed_value", "another_key": "another_value"}
479+
>>> dict_obj2 = {"new_key": "just_another", "yet_another_key": "yet_another_value"}
480+
>>> wrapper_renames_old_key_to_new_key = renamed_key_items_warning("1.4.0", {"old_key": "new_key"}, "2.0.0")
481+
>>> new_dict_obj1 = wrapper_renames_old_key_to_new_key(dict_obj1)
482+
>>> new_dict_obj2 = wrapper_renames_old_key_to_new_key(dict_obj2)
483+
>>> new_dict_obj1["old_key"]
484+
<stdin>:1: pvlibDeprecationWarning: Please use `new_key` instead of `old_key`. Deprecated since 1.4.0 and will be removed in 2.0.0.
485+
'renamed_value'
486+
>>> new_dict_obj2["old_key"]
487+
<stdin>:1: pvlibDeprecationWarning: Please use `new_key` instead of `old_key`. Deprecated since 1.4.0 and will be removed in 2.0.0.
488+
'just_another'
489+
490+
Notes
491+
-----
492+
This decorator does not modify the way you access methods on the original
493+
type. For example, dictionaries can only be accessed with bracketed
494+
indexes, ``dictionary["key"]``. After decoration, ``"old_key"`` can only
495+
be used as follows: ``dictionary["old_key"]``. Both ``dictionary.key`` and
496+
``dictionary.old_key`` won't become available after wrapping.
497+
498+
>>> from pvlib._deprecation import renamed_key_items_warning
499+
>>> dict_base = {"a": [1]}
500+
>>> dict_depre = renamed_key_items_warning("0.0.1", {"b": "a"})(dict_base)
501+
>>> dict_depre["a"]
502+
[1]
503+
>>> dict_depre["b"]
504+
<stdin>:1: pvlibDeprecationWarning: Please use `a` instead of `b`. \
505+
Deprecated since 0.0.1 and will be removed soon.
506+
[1]
507+
>>> dict_depre.a
508+
Traceback (most recent call last):
509+
File "<stdin>", line 1, in <module>
510+
AttributeError: 'DeprecatedKeyItems' object has no attribute 'a'
511+
>>> dict_depre.b
512+
Traceback (most recent call last):
513+
File "<stdin>", line 1, in <module>
514+
AttributeError: 'DeprecatedKeyItems' object has no attribute 'b'
515+
516+
On the other hand, ``pandas.DataFrame`` and other types may also expose
517+
indexes as attributes on the object instance. In a ``DataFrame`` you can
518+
either use ``df.a`` or ``df["a"]``. An old key ``b`` that maps to ``a``
519+
through the decorator, can either be accessed with ``df.b`` or ``df["b"]``.
520+
521+
>>> from pvlib._deprecation import renamed_key_items_warning
522+
>>> import pandas as pd
523+
>>> df_base = pd.DataFrame({"a": [1]})
524+
>>> df_base.a
525+
0 1
526+
Name: a, dtype: int64
527+
>>> df_depre = renamed_key_items_warning("0.0.1", {"b": "a"})(df_base)
528+
>>> df_depre.a
529+
0 1
530+
Name: a, dtype: int64
531+
>>> df_depre.b
532+
Traceback (most recent call last):
533+
File "<stdin>", line 1, in <module>
534+
File "...", line 6299, in __getattr__
535+
return object.__getattribute__(self, name)
536+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
537+
AttributeError: 'DeprecatedKeyItems' object has no attribute 'b'
538+
539+
References
540+
----------
541+
.. [1] `Python docs on __getitem__
542+
<https://docs.python.org/3/reference/datamodel.html#object.__getitem__>`_
543+
.. [2] `StackOverflow thread on deprecating dict keys
544+
<https://stackoverflow.com/questions/54095279/how-to-make-a-dict-key-deprecated>`_
545+
""" # noqa: E501
546+
547+
def deprecated(obj, old_to_new_keys_map=old_to_new_keys_map, since=since):
548+
obj_type = type(obj)
549+
550+
class DeprecatedKeyItems(obj_type):
551+
"""Handles deprecated key-indexed elements in a collection."""
552+
553+
def __getitem__(self, old_key):
554+
if old_key in old_to_new_keys_map:
555+
new_key = old_to_new_keys_map[old_key]
556+
msg = (
557+
f"Please use `{new_key}` instead of `{old_key}`. "
558+
f"Deprecated since {since} and will be removed "
559+
+ (f"in {removal}." if removal else "soon.")
560+
)
561+
with warnings.catch_warnings():
562+
# by default, only first ocurrence is shown
563+
# remove limitation to show on multiple uses
564+
warnings.simplefilter("always")
565+
warnings.warn(
566+
msg, category=_projectWarning, stacklevel=2
567+
)
568+
old_key = new_key
569+
return super().__getitem__(old_key)
570+
571+
wrapped_obj = DeprecatedKeyItems(obj)
572+
573+
wrapped_obj.__class__ = type(
574+
wrapped_obj.__class__.__name__,
575+
(DeprecatedKeyItems, obj.__class__),
576+
{},
577+
)
578+
579+
return wrapped_obj
580+
581+
return deprecated

tests/test__deprecation.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import pytest
6+
import pandas as pd
67

78
from pvlib import _deprecation
89
from .conftest import fail_on_pvlib_version
@@ -95,3 +96,65 @@ def test_renamed_kwarg_warning(renamed_kwarg_func):
9596
TypeError, match="missing 1 required positional argument"
9697
):
9798
renamed_kwarg_func()
99+
100+
101+
def test_renamed_key_items_warning():
102+
"""Test the renamed_key_items_warning decorator."""
103+
# Test on a dictionary
104+
data_dict = {
105+
"new_key1": [1, 2, 3],
106+
"new_key2": [4, 5, 6],
107+
"another_key": [7, 8, 9],
108+
}
109+
data_dict_wrapped = _deprecation.renamed_key_items_warning(
110+
"0.1.0", {"old_key1": "new_key1"}, "0.2.0"
111+
)(data_dict)
112+
113+
# Check that the new key is present in the wrapped object
114+
assert "new_key1" in data_dict_wrapped
115+
assert "new_key2" in data_dict_wrapped
116+
assert "another_key" in data_dict_wrapped
117+
assert "old_key1" not in data_dict_wrapped
118+
# Check that the old key still exists in the wrapped object
119+
assert data_dict_wrapped["new_key1"] == [1, 2, 3]
120+
assert data_dict_wrapped["new_key2"] == [4, 5, 6]
121+
assert data_dict_wrapped["another_key"] == [7, 8, 9]
122+
with pytest.warns(Warning, match="use `new_key1` instead of `old_key1`."):
123+
assert data_dict_wrapped["old_key1"] == [1, 2, 3]
124+
# check yet again, to ensure there is no weird persistences
125+
with pytest.warns(Warning, match="use `new_key1` instead of `old_key1`."):
126+
assert data_dict_wrapped["old_key1"] == [1, 2, 3]
127+
128+
# Test on a DataFrame
129+
data_df = pd.DataFrame(data_dict)
130+
data_df = _deprecation.renamed_key_items_warning(
131+
"0.1.0", {"old_key1": "new_key1", "old_key2": "new_key2"}, "0.2.0"
132+
)(data_df)
133+
134+
assert "new_key1" in data_df.columns
135+
assert data_df.new_key1 is not None # ensure attribute access works
136+
assert "new_key2" in data_df.columns
137+
assert "old_key1" not in data_df.columns
138+
assert "old_key2" not in data_df.columns
139+
# Check that the old key still exists in the DataFrame
140+
assert data_df["new_key1"].tolist() == [1, 2, 3]
141+
with pytest.warns(Warning, match="use `new_key1` instead of `old_key1`."):
142+
assert data_df["old_key1"].tolist() == [1, 2, 3]
143+
with pytest.warns(Warning, match="use `new_key1` instead of `old_key1`."):
144+
assert data_df["old_key1"].tolist() == [1, 2, 3]
145+
146+
# Test chaining decorators, on a dict, first new_key1, then new_key2
147+
data_dict_wrapped = _deprecation.renamed_key_items_warning(
148+
"0.1.0", {"old_key1": "new_key1"}, "0.2.0"
149+
)(data_dict)
150+
data_dict_wrapped = _deprecation.renamed_key_items_warning(
151+
"0.3.0", {"old_key2": "new_key2"}, "0.4.0"
152+
)(data_dict_wrapped)
153+
# Check that the new keys are present in the wrapped object
154+
assert "new_key1" in data_dict_wrapped
155+
assert "new_key2" in data_dict_wrapped
156+
157+
with pytest.warns(Warning, match="use `new_key1` instead of `old_key1`."):
158+
assert data_dict_wrapped["old_key1"] == [1, 2, 3]
159+
with pytest.warns(Warning, match="use `new_key2` instead of `old_key2`."):
160+
assert data_dict_wrapped["old_key2"] == [4, 5, 6]

0 commit comments

Comments
 (0)