Repository: briancurtin/deprecation

Issue:
Allow removed_in to be a date
Depending on the versioning scheme used by a project, it can be difficult to tell in advance the version number at which code can be removed. Sometimes, though, it is easier to tell *when* it should be removed. Google Guava, for example, guarantees all methods marked deprecated will be kept around for two years post-deprecation.

It would be convenient to handle this by allowing a date (in some TBD format) to be passed for the `removed_in` argument.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=h1) Report
> Merging [#47](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=desc) into [master](https://codecov.io/gh/briancurtin/deprecation/commit/264e8cd469cdcfc793afedb80d224f48d350a360?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/briancurtin/deprecation/pull/47/graphs/tree.svg?width=650&token=6w8zqu2CEQ&height=150&src=pr)](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #47   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           1      1           
  Lines          83     92    +9     
  Branches       16     20    +4     
=====================================
+ Hits           83     92    +9
```


| [Impacted Files](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deprecation.py](https://codecov.io/gh/briancurtin/deprecation/pull/47/diff?src=pr&el=tree#diff-ZGVwcmVjYXRpb24ucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=footer). Last update [264e8cd...85738a1](https://codecov.io/gh/briancurtin/deprecation/pull/47?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

gabbard: @aakum03 : I have not yet reviewed the tests. I will look at them on Monday.
aakum03: Made the requested changes to `deprecation.py`
briancurtin: Thanks a lot for this PR, and thanks @gabbard for having a look as well. I'll give it a full review in the next day or so.
briancurtin: Thanks for the PR! I'm going to merge it and then will put together a release probably later today or early tomorrow, then I'll close out #46 once it's available.

Patch:
diff --git a/.gitignore b/.gitignore
index 72364f9..0627077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,3 +87,6 @@ ENV/
 
 # Rope project settings
 .ropeproject
+
+#Pycharm Files
+.idea/
diff --git a/deprecation.py b/deprecation.py
index ccb7f66..e7903b4 100644
--- a/deprecation.py
+++ b/deprecation.py
@@ -15,6 +15,7 @@ import textwrap
 import warnings
 
 from packaging import version
+from datetime import date
 
 __version__ = "2.0.7"
 
@@ -42,7 +43,8 @@ class DeprecatedWarning(DeprecationWarning):
 
     :param function: The function being deprecated.
     :param deprecated_in: The version that ``function`` is deprecated in
-    :param removed_in: The version that ``function`` gets removed in
+    :param removed_in: The version or :class:`datetime.date` specifying
+                       when ``function`` gets removed.
     :param details: Optional details about the deprecation. Most often
                     this will include directions on what to use instead
                     of the now deprecated code.
@@ -68,7 +70,8 @@ class DeprecatedWarning(DeprecationWarning):
         if self.deprecated_in:
             parts["deprecated"] = " as of %s" % self.deprecated_in
         if self.removed_in:
-            parts["removed"] = " and will be removed in %s" % self.removed_in
+            parts["removed"] = " and will be removed {} {}".format("on" if isinstance(self.removed_in, date) else "in",
+                                                                   self.removed_in)
         if any([self.deprecated_in, self.removed_in, self.details]):
             parts["period"] = "."
         if self.details:
@@ -121,10 +124,11 @@ def deprecated(deprecated_in=None, removed_in=None, current_version=None,
                           means immediate deprecation. If this is not
                           specified, then the `removed_in` and
                           `current_version` arguments are ignored.
-    :param removed_in: The version when the decorated method will be removed.
-                       The default is **None**, specifying that the function
-                       is not currently planned to be removed.
-                       Note: This cannot be set to a value if
+    :param removed_in: The version or :class:`datetime.date` when the decorated
+                       method will be removed. The default is **None**,
+                       specifying that the function is not currently planned
+                       to be removed.
+                       Note: This parameter cannot be set to a value if
                        `deprecated_in=None`.
     :param current_version: The source of version information for the
                             currently running code. This will usually be
@@ -155,8 +159,14 @@ def deprecated(deprecated_in=None, removed_in=None, current_version=None,
     is_unsupported = False
 
     # StrictVersion won't take a None or a "", so make whatever goes to it
-    # is at least *something*.
-    if current_version:
+    # is at least *something*. Compare versions only if removed_in is not
+    # of type datetime.date
+    if isinstance(removed_in, date):
+        if date.today() >= removed_in:
+            is_unsupported = True
+        else:
+            is_deprecated = True
+    elif current_version:
         current_version = version.parse(current_version)
 
         if (removed_in
@@ -183,12 +193,15 @@ def deprecated(deprecated_in=None, removed_in=None, current_version=None,
             # a number of ways the deprecation notice could go. The following
             # makes for a nicely constructed sentence with or without any
             # of the parts.
+
+            # If removed_in is a date, use "removed on"
+            # If removed_in is a version, use "removed in"
             parts = {
                 "deprecated_in":
                     " %s" % deprecated_in if deprecated_in else "",
                 "removed_in":
-                    "\n   This will be removed in %s." %
-                    removed_in if removed_in else "",
+                    "\n   This will be removed {} {}.".format("on" if isinstance(removed_in, date) else "in",
+                                                              removed_in) if removed_in else "",
                 "details":
                     " %s" % details if details else ""}
 


Test patch:
diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py
index 4d294ba..0fd29b3 100644
--- a/tests/test_deprecation.py
+++ b/tests/test_deprecation.py
@@ -16,6 +16,7 @@ import unittest2
 import warnings
 
 import deprecation
+from datetime import date
 
 
 class Test_deprecated(unittest2.TestCase):
@@ -41,6 +42,14 @@ class Test_deprecated(unittest2.TestCase):
                                "details": "some details"},
                       "__doc__": "docstring\n\n.. deprecated:: 1.0"
                                  "\n   This will be removed in 2.0. "
+                                 "some details"},
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2200, 5, 20)},
+                      "__doc__": "docstring\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2200-05-20."},
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2100, 3, 15),
+                               "details": "some details"},
+                      "__doc__": "docstring\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2100-03-15. "
                                  "some details"}]:
             with self.subTest(**test):
                 @deprecation.deprecated(**test["args"])
@@ -62,6 +71,14 @@ class Test_deprecated(unittest2.TestCase):
                                "details": "some details"},
                       "__doc__": "%s\n\n.. deprecated:: 1.0"
                                  "\n   This will be removed in 2.0. "
+                                 "some details"},
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2200, 11, 20)},
+                      "__doc__": "%s\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2200-11-20."},
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2100, 3, 15),
+                               "details": "some details"},
+                      "__doc__": "%s\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2100-03-15. "
                                  "some details"}]:
             with self.subTest(**test):
                 @deprecation.deprecated(**test["args"])
@@ -88,6 +105,14 @@ class Test_deprecated(unittest2.TestCase):
                                "details": "some details"},
                       "__doc__": "%s\n\n.. deprecated:: 1.0"
                                  "\n   This will be removed in 2.0. "
+                                 "some details%s"},#####
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2200, 11, 20)},
+                      "__doc__": "%s\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2200-11-20.%s"},
+                     {"args": {"deprecated_in": "1.0", "removed_in": date(2100, 3, 15),
+                               "details": "some details"},
+                      "__doc__": "%s\n\n.. deprecated:: 1.0"
+                                 "\n   This will be removed on 2100-03-15. "
                                  "some details%s"}]:
             with self.subTest(**test):
                 deprecation.message_location = "top"
@@ -150,6 +175,24 @@ class Test_deprecated(unittest2.TestCase):
                                "details": "do something else."},
                       "warning": deprecation.UnsupportedWarning,
                       "message": ("method is unsupported as of 2.0. "
+                                  "do something else.")},
+                     {"args": {"deprecated_in": "1.0",
+                               "removed_in": date(2100, 4, 19),
+                               "current_version": "2.0"},
+                      "warning": deprecation.DeprecatedWarning,
+                      "message": ("method is deprecated as of 1.0 "
+                                  "and will be removed on 2100-04-19.")},
+                     {"args": {"deprecated_in": "1.0",
+                               "removed_in": date.today(),
+                               "current_version": "2.0"},
+                      "warning": deprecation.UnsupportedWarning,
+                      "message": "method is unsupported as of %s." % date.today()},
+                     {"args": {"deprecated_in": "1.0",
+                               "removed_in": date(2020, 1, 30),
+                               "current_version": "2.0",
+                               "details": "do something else."},
+                      "warning": deprecation.UnsupportedWarning,
+                      "message": ("method is unsupported as of 2020-01-30. "
                                   "do something else.")}]:
             with self.subTest(**test):
                 class Test(object):