Repository: seequent/pure_interface

Issue:
Add optional_adapt() function
This idiom is quite common
`obj = Interface.adapt(obj) if obj is not None else None`

create `optional_adapt(obj)` to replace it.

`optional_adapt(obj)` should adapt obj if it is not None.



Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 14a8b6b..3fa08d2 100644
--- a/README.rst
+++ b/README.rst
@@ -290,6 +290,14 @@ using ``filter_adapt(objects)``::
 
    list(ISpeaker.filter_adapt([None, Talker(), a_speaker, 'text']) --> [TalkerToSpeaker, a_speaker]
 
+To adapt an object only if it is not ``None`` then use::
+
+    ISpeaker.optional_adapt(optional_talker)
+
+This is equivalent to::
+
+    ISpeaker.adapt(optional_talker) if optional_talker is not None else None
+
 By default the adaption functions will return an object which provides **only**
 the functions and properties specified by the interface.  For example given the
 following implementation of the ``ISpeaker`` interface above::
@@ -485,6 +493,10 @@ Classes
     **adapt_or_none** *(obj, allow_implicit=False, interface_only=None)*
         As per **adapt()** except returns ``None`` instead of raising a ``ValueError``
 
+    **optional_adapt** *(obj, allow_implicit=False, interface_only=None)*
+        Adapts obj to this interface if it is not ``None`` returning ``None`` otherwise.
+        Short-cut for ``adapt(obj) if obj is not None else None``
+
     **can_adapt** *(obj, allow_implicit=False)*
         Returns ``True`` if ``adapt(obj, allow_implicit)`` will succeed.  Short-cut for
         ``adapt_or_none(obj) is not None``
diff --git a/pure_interface.py b/pure_interface.py
index 9ecb7fd..4ecb744 100644
--- a/pure_interface.py
+++ b/pure_interface.py
@@ -38,7 +38,7 @@ else:
             super(abstractstaticmethod, self).__init__(callable)
 
 
-__version__ = '3.2.0'
+__version__ = '3.3.0'
 
 
 is_development = not hasattr(sys, 'frozen')
@@ -616,6 +616,14 @@ class PureInterface(ABC):
             adapted = cls.interface_only(adapted)
         return adapted
 
+    @classmethod
+    def optional_adapt(cls, obj, allow_implicit=False, interface_only=None):
+        # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI]
+        """ Adapt obj to to_interface or return None if adaption fails """
+        if obj is None:
+            return None
+        return cls.adapt(obj, allow_implicit=allow_implicit, interface_only=interface_only)
+
     @classmethod
     def adapt_or_none(cls, obj, allow_implicit=False, interface_only=None):
         # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI]
diff --git a/setup.py b/setup.py
index af5d976..e4ef140 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
 
 setup(
     name='pure_interface',
-    version='3.2.0',
+    version='3.3.0',
     py_modules=['pure_interface', 'pure_contracts'],
     url='https://github.com/aranzgeo/pure_interface',
     install_requires=['six', 'typing'],


Test patch:
diff --git a/tests/test_adaption.py b/tests/test_adaption.py
index d9541cc..5611957 100644
--- a/tests/test_adaption.py
+++ b/tests/test_adaption.py
@@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import pure_interface
 
 import unittest
+import mock
 
 
 class ISpeaker(pure_interface.PureInterface):
@@ -310,3 +311,15 @@ class TestAdaptionToInterfaceOnly(unittest.TestCase):
         a = IA.adapt_or_none(4, interface_only=False)
         self.assertIsInstance(a, IntToA)
 
+    def test_optional_adapt(self):
+        a_speaker = Speaker()
+        allow = object()
+        interface_only = object()
+        with mock.patch('pure_interface.PureInterface.adapt') as adapt:
+            # act
+            s = ISpeaker.optional_adapt(a_speaker, allow_implicit=allow, interface_only=interface_only)
+            none = ISpeaker.optional_adapt(None, allow_implicit=allow, interface_only=interface_only)
+            # assert
+            adapt.assert_called_once_with(a_speaker, allow_implicit=allow, interface_only=interface_only)
+            self.assertIs(s, adapt.return_value)
+            self.assertIsNone(none, 'optional_adapt(None) did not return None')