From 041dc166e7c09ec2228f88cb72c2a4e6e4d8b882 Mon Sep 17 00:00:00 2001 From: Derek Passen Date: Fri, 7 Sep 2018 18:29:02 -0500 Subject: [PATCH] Rework to use an abstract base class. - Introduce separate (private) classes, _Something and _Nothing that inherit from _AbstracrOptional. - Introduce Optional class with two static functions, of and empty to simplify usage. - Remove the tracking of check before get. Use of get should be heavily discouraged. --- README.md | 50 ++-------- optional.py | 83 ---------------- optional/__init__.py | 1 + optional/compatible_abc.py | 12 +++ optional/optional.py | 109 ++++++++++++++++++++++ test/__init__.py | 0 test_optional.py => test/test_optional.py | 17 ++-- 7 files changed, 138 insertions(+), 134 deletions(-) delete mode 100644 optional.py create mode 100644 optional/__init__.py create mode 100644 optional/compatible_abc.py create mode 100644 optional/optional.py create mode 100644 test/__init__.py rename test_optional.py => test/test_optional.py (91%) diff --git a/README.md b/README.md index 381c035..ef71c1c 100644 --- a/README.md +++ b/README.md @@ -82,25 +82,7 @@ So we present you with an **Optional** object as an alternative. print(thing) ``` -6. But you **can't** get the value without first checking for presence: - ```python - thing = some_func_returning_an_optional() - print(thing.get()) # **will raise an exception** - - ``` - but: - ```python - thing = some_func_returning_an_optional() - if thing.is_present(): # could use is_empty() as alternative - print(thing.get()) # **does not throw** - ``` - instead of: - ```python - if thing is not None: - print(thing) - ``` - -7. You **can't** get the value if its empty: +6. You **can't** get the value if its empty: ```python thing = some_func_returning_an_optional() if thing.is_empty(): @@ -112,7 +94,7 @@ So we present you with an **Optional** object as an alternative. print(None) # very odd ``` -8. **__Best Usage:__** You can chain on presence: +7. **__Best Usage:__** You can chain on presence: ```python thing = some_func_returning_an_optional() thing.if_present(lambda thing: print(thing)) @@ -123,7 +105,7 @@ So we present you with an **Optional** object as an alternative. print(thing) ``` -9. **__Best Usage:__** You can chain on non presence: +8. **__Best Usage:__** You can chain on non presence: ```python thing = some_func_returning_an_optional() thing.if_present(lambda thing: print(thing)).or_else(lambda _: print("PANTS!")) @@ -136,45 +118,33 @@ So we present you with an **Optional** object as an alternative. print("PANTS!") ``` -10. **__Best Usage:__** You can map a function: +9. **__Best Usage:__** You can map a function: ```python def mapping_func(thing): return thing + "PANTS" - + thing_to_map = Optional.of("thing") mapped_thing = thing_to_map.map(mapping_func) # returns Optional.of("thingPANTS") ``` Note that if the mapping function returns `None` then the map call will return `Optional.empty()`. Also if you call `map` on an empty optional it will return `Optional.empty()`. - -11. **__Best Usage:__** You can flat map a function which returns an Optional. + +10. **__Best Usage:__** You can flat map a function which returns an Optional. ```python def flat_mapping_func(thing): return Optional.of(thing + "PANTS") - + thing_to_map = Optional.of("thing") mapped_thing = thing_to_map.map(mapping_func) # returns Optional.of("thingPANTS") ``` - Note that this does not return an Optional of an Optional. __Use this for mapping functions which return optionals.__ + Note that this does not return an Optional of an Optional. __Use this for mapping functions which return optionals.__ If the mapping function you use with this does not return an Optional, calling `flat_map` will raise a `FlatMapFunctionDoesNotReturnOptionalException`. -12. You can compare two optionals: +11. You can compare two optionals: ```python Optional.empty() == Optional.empty() # True Optional.of("thing") == Optional.of("thing") # True Optional.of("thing") == Optional.empty() # False Optional.of("thing") == Optional.of("PANTS") # False ``` - - - - - - - - - - - - diff --git a/optional.py b/optional.py deleted file mode 100644 index 4fed4f9..0000000 --- a/optional.py +++ /dev/null @@ -1,83 +0,0 @@ - - -class Optional: - - def __init__(self, thing): - self._thing = thing - self._presence_checked = False - - @staticmethod - def of(thing): - if thing is None: - return Optional.empty() - return Optional(thing) - - @staticmethod - def empty(): - return Optional(None) - - def __eq__(self, other): - if not isinstance(other, Optional): - return False - - if self.is_empty() and other.is_empty(): - return True - - if self.is_empty() or other.is_empty(): - return False - - return other.get() == self.get() - - def is_present(self): - self._presence_checked = True - return self._thing is not None - - def is_empty(self): - return not self.is_present() - - def get(self): - if not self._presence_checked: - raise OptionalAccessWithoutCheckingPresenceException( - "You cannot access the contents of an optional without first checking for its presence.") - - if self._thing is None: - raise OptionalAccessOfEmptyException("You cannot call get on an empty optional") - - return self._thing - - def map(self, func): - if self.is_empty(): - return Optional.empty() - return Optional.of(func(self.get())) - - def flat_map(self, func): - if self.is_empty(): - return Optional.empty() - - res = func(self.get()) - if not isinstance(res, Optional): - raise FlatMapFunctionDoesNotReturnOptionalException("Mapping function to flat_map must return Optional.") - - return res - - def if_present(self, consumer): - if self._thing is not None: - consumer(self._thing) - return Optional._NotPresent() - - class _NotPresent: - - def or_else(self, procedure): - procedure() - - -class FlatMapFunctionDoesNotReturnOptionalException(Exception): - pass - - -class OptionalAccessWithoutCheckingPresenceException(Exception): - pass - - -class OptionalAccessOfEmptyException(Exception): - pass \ No newline at end of file diff --git a/optional/__init__.py b/optional/__init__.py new file mode 100644 index 0000000..8f5c1a5 --- /dev/null +++ b/optional/__init__.py @@ -0,0 +1 @@ +from .optional import * diff --git a/optional/compatible_abc.py b/optional/compatible_abc.py new file mode 100644 index 0000000..16bc850 --- /dev/null +++ b/optional/compatible_abc.py @@ -0,0 +1,12 @@ +"""compatible_abc + +This module exports a single class, CompatibleABC. +It is necessary to provide the same behavior in +Python 2 and Python 3. + +The implementation was taken from https://stackoverflow.com/a/38668373 +""" +from abc import ABCMeta + + +CompatibleABC = ABCMeta('ABC', (object,), {'__slots__': ()}) diff --git a/optional/optional.py b/optional/optional.py new file mode 100644 index 0000000..036f1e7 --- /dev/null +++ b/optional/optional.py @@ -0,0 +1,109 @@ +from abc import abstractmethod + +from .compatible_abc import CompatibleABC + + +class Optional(object): + @staticmethod + def of(thing): + return _Nothing() if thing is None else _Something(thing) + + @staticmethod + def empty(): + return _Nothing() + + +class _AbstractOptional(CompatibleABC): + + @abstractmethod + def is_present(self): + pass + + def is_empty(self): + return not self.is_present() + + @abstractmethod + def get(self): + pass + + @abstractmethod + def if_present(self, consumer): + pass + + @abstractmethod + def or_else(self, procedure): + pass + + @abstractmethod + def map(self, func): + pass + + @abstractmethod + def flat_map(self, func): + pass + + +class _Nothing(_AbstractOptional): + def is_present(self): + return False + + def get(self): + raise OptionalAccessOfEmptyException( + "You cannot call get on an empty optional" + ) + + def if_present(self, consumer): + return self + + def or_else(self, procedure): + return procedure() + + def map(self, func): + return self + + def flat_map(self, func): + return self + + def __eq__(self, other): + return isinstance(other, _Nothing) + + +class _Something(_AbstractOptional): + def __init__(self, value): + self.__value = value + + def is_present(self): + return True + + def get(self): + return self.__value + + def if_present(self, consumer): + consumer(self.get()) + return self + + def or_else(self, procedure): + return self + + def map(self, func): + return Optional.of(func(self.get())) + + def flat_map(self, func): + res = func(self.get()) + if not isinstance(res, _AbstractOptional): + raise FlatMapFunctionDoesNotReturnOptionalException( + "Mapping function to flat_map must return Optional." + ) + + return res + + def __eq__(self, other): + return isinstance(other, _Something) and self.get() == other.get() + + +class OptionalAccessOfEmptyException(Exception): + pass + + +class FlatMapFunctionDoesNotReturnOptionalException(Exception): + pass diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_optional.py b/test/test_optional.py similarity index 91% rename from test_optional.py rename to test/test_optional.py index 3a311f9..4f54d1e 100644 --- a/test_optional.py +++ b/test/test_optional.py @@ -1,12 +1,16 @@ import unittest -from optional import Optional, OptionalAccessWithoutCheckingPresenceException, OptionalAccessOfEmptyException, FlatMapFunctionDoesNotReturnOptionalException +from optional import ( + Optional, + OptionalAccessOfEmptyException, + FlatMapFunctionDoesNotReturnOptionalException +) class TestOptional(unittest.TestCase): def test_can_instantiate(self): - Optional(None) + Optional.of(None) def test_instantiate_empty(self): optional = Optional.empty() @@ -28,11 +32,6 @@ def test_is_not_present_with_empty(self): optional = Optional.of(None) self.assertFalse(optional.is_present()) - def test_cannot_get_without_checking_presence(self): - optional = Optional.of("thing") - with self.assertRaises(OptionalAccessWithoutCheckingPresenceException): - optional.get() - def test_cannot_get_from_empty_even_after_checking(self): optional = Optional.empty() self.assertTrue(optional.is_empty()) @@ -156,7 +155,3 @@ def test_non_empty_optionals_with_non_equal_content_are_not_equal(self): def test_non_empty_optionals_with_equal_content_are_equal(self): self.assertEqual(Optional.of("PANTS"), Optional.of("PANTS")) - - -if __name__ == '__main__': - unittest.main()