@@ -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
0 commit comments