@@ -389,3 +389,193 @@ def wrapper(*args, **kwargs):
389
389
return wrapper
390
390
391
391
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