Repository: briancurtin/deprecation

Issue:
DeprecatedWarning.__str__ should handle None arguments better
`DeprecatedWarning.__str__`doesn't play nicely with `None` values, such as what happens when you call `deprecation.deprecated()` with no arguments. This ends up showing up in the interpreter messages when run with `python -Wd`, though it doesn't affect docstrings.

    /.../deprecation.py:183: DeprecatedWarning: fn is deprecated as of None and will be removed in None. 
      warnings.warn(the_warning)

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/14?src=pr&el=h1) Report
> Merging [#14](https://codecov.io/gh/briancurtin/deprecation/pull/14?src=pr&el=desc) into [master](https://codecov.io/gh/briancurtin/deprecation/commit/3ad94c2de7c313d0ca20a8ce4c9247a0faaa8018?src=pr&el=desc) will **decrease** coverage by `7.69%`.
> The diff coverage is `66.66%`.

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

```diff
@@           Coverage Diff            @@
##           master     #14     +/-   ##
========================================
- Coverage     100%   92.3%   -7.7%     
========================================
  Files           1       1             
  Lines          62      78     +16     
  Branches       10      15      +5     
========================================
+ Hits           62      72     +10     
- Misses          0       3      +3     
- Partials        0       3      +3
```


| [Impacted Files](https://codecov.io/gh/briancurtin/deprecation/pull/14?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deprecation.py](https://codecov.io/gh/briancurtin/deprecation/pull/14/diff?src=pr&el=tree#diff-ZGVwcmVjYXRpb24ucHk=) | `92.3% <66.66%> (-7.7%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/14?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/14?src=pr&el=footer). Last update [3ad94c2...7304a70](https://codecov.io/gh/briancurtin/deprecation/pull/14?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/deprecation.py b/deprecation.py
index 00598db..a64b5b7 100644
--- a/deprecation.py
+++ b/deprecation.py
@@ -9,6 +9,7 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations
 # under the License.
+import collections
 import functools
 import textwrap
 import warnings
@@ -51,9 +52,22 @@ class DeprecatedWarning(DeprecationWarning):
         super(DeprecatedWarning, self).__init__()
 
     def __str__(self):
-        return ("%s is deprecated as of %s and will "
-                "be removed in %s. %s" % (self.function, self.deprecated_in,
-                                          self.removed_in, self.details))
+        # Use a defaultdict to give us the empty string
+        # when a part isn't included.
+        parts = collections.defaultdict(str)
+        parts["function"] = self.function
+
+        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
+        if any([self.deprecated_in, self.removed_in, self.details]):
+            parts["period"] = "."
+        if self.details:
+            parts["details"] = " %s" % self.details
+
+        return ("%(function)s is deprecated%(deprecated)s%(removed)s"
+                "%(period)s%(details)s" % (parts))
 
 
 class UnsupportedWarning(DeprecatedWarning):
@@ -67,9 +81,15 @@ class UnsupportedWarning(DeprecatedWarning):
     """
 
     def __str__(self):
-        return ("%s is unsupported as of %s. %s" % (self.function,
-                                                    self.removed_in,
-                                                    self.details))
+        parts = collections.defaultdict(str)
+        parts["function"] = self.function
+        parts["removed"] = self.removed_in
+
+        if self.details:
+            parts["details"] = " %s" % self.details
+
+        return ("%(function)s is unsupported as of %(removed)s."
+                "%(details)s" % (parts))
 
 
 def deprecated(deprecated_in=None, removed_in=None, current_version=None,


Test patch:
diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py
index cf9d420..55e05bd 100644
--- a/tests/test_deprecation.py
+++ b/tests/test_deprecation.py
@@ -71,14 +71,33 @@ class Test_deprecated(unittest2.TestCase):
         ret_val = "lololol"
 
         for test in [{"args": {},  # No args just means deprecated
-                      "warning": deprecation.DeprecatedWarning},
+                      "warning": deprecation.DeprecatedWarning,
+                      "message": "method is deprecated"},
+                     {"args": {"details": "do something else."},
+                      "warning": deprecation.DeprecatedWarning,
+                      "message": "method is deprecated. do something else."},
                      {"args": {"deprecated_in": "1.0",
                                "current_version": "2.0"},
-                      "warning": deprecation.DeprecatedWarning},
+                      "warning": deprecation.DeprecatedWarning,
+                      "message": "method is deprecated as of 1.0."},
+                     {"args": {"deprecated_in": "1.0",
+                               "removed_in": "3.0",
+                               "current_version": "2.0"},
+                      "warning": deprecation.DeprecatedWarning,
+                      "message": ("method is deprecated as of 1.0 "
+                                  "and will be removed in 3.0.")},
                      {"args": {"deprecated_in": "1.0",
                                "removed_in": "2.0",
                                "current_version": "2.0"},
-                      "warning": deprecation.UnsupportedWarning}]:
+                      "warning": deprecation.UnsupportedWarning,
+                      "message": "method is unsupported as of 2.0."},
+                     {"args": {"deprecated_in": "1.0",
+                               "removed_in": "2.0",
+                               "current_version": "2.0",
+                               "details": "do something else."},
+                      "warning": deprecation.UnsupportedWarning,
+                      "message": ("method is unsupported as of 2.0. "
+                                  "do something else.")}]:
             with self.subTest(**test):
                 class Test(object):
                     @deprecation.deprecated(**test["args"])
@@ -93,6 +112,8 @@ class Test_deprecated(unittest2.TestCase):
 
                 self.assertEqual(len(caught_warnings), 1)
                 self.assertEqual(caught_warnings[0].category, test["warning"])
+                self.assertEqual(str(caught_warnings[0].message),
+                                 test["message"])
 
     def test_DeprecatedWarning_not_raised(self):
         ret_val = "lololol"