-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from keotl/feature/nullable
nullable class
- Loading branch information
Showing
9 changed files
with
225 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from typing import Generic, TypeVar, Optional, Callable, Union | ||
|
||
T = TypeVar('T') | ||
S = TypeVar('S') | ||
|
||
|
||
class Nullable(Generic[T]): | ||
"""Nullable class which wraps Optional types. | ||
Args: | ||
nullable (Optional[object)) : Item which can be None. | ||
""" | ||
|
||
def __init__(self, nullable: Optional[T]): | ||
self._item: Optional[T] = nullable | ||
|
||
def isPresent(self) -> bool: | ||
"""Returns True if item is not None.""" | ||
return self._item is not None | ||
|
||
def get(self) -> Optional[T]: | ||
"""Gets the item if present. | ||
Raises: | ||
EmptyNullableException: Attempting to get a missing item. | ||
""" | ||
if self.isPresent(): | ||
return self._item | ||
raise EmptyNullableException() | ||
|
||
def orElse(self, default_value: T) -> T: | ||
"""Returns the item if present, else return the supplied default value. | ||
Args: | ||
default_value: Value to return instead of a None value. | ||
""" | ||
return self._item if self.isPresent() else default_value | ||
|
||
def orElseThrow(self, exception: Union[Exception, Callable[[], Exception]]) -> T: | ||
"""Returns if present, raises exception if missing. | ||
Args: | ||
exception: Either an exception, or a callable which returns an exception. | ||
""" | ||
if self.isPresent(): | ||
return self._item | ||
if isinstance(exception, Exception): | ||
raise exception | ||
raise exception() | ||
|
||
def orElseFetch(self, supplier: Callable[[], T]) -> T: | ||
"""Returns if present, invoke callable if missing. | ||
Args: | ||
supplier (Callable): Supplied return value will be return in place of a None value. Should not require parameters. | ||
""" | ||
if self.isPresent(): | ||
return self._item | ||
return supplier() | ||
|
||
def ifPresent(self, consumer: Callable[[T], None]) -> None: | ||
"""Invoke function if value is present; otherwise does nothing. | ||
Args: | ||
consumer (Callable): Function to be invoked with a non-nil parameter. | ||
""" | ||
if self.isPresent(): | ||
consumer(self._item) | ||
|
||
def filter(self, predicate: Callable[[T], bool]) -> "Nullable[T]": | ||
"""Filters item given a criterion. | ||
Args: | ||
predicate (Callable): Invoked with a non-nil parameter. Should return a boolean. | ||
""" | ||
if self.isPresent(): | ||
return self if predicate(self._item) else Nullable.empty() | ||
return Nullable.empty() | ||
|
||
def map(self, callable: Callable[[T], S]) -> "Nullable[S]": | ||
"""Maps the item when present. | ||
Args: | ||
callable (Callable): Invoked with a non-nil parameter. | ||
""" | ||
if self.isPresent(): | ||
return Nullable(callable(self._item)) | ||
return Nullable.empty() | ||
|
||
def __bool__(self) -> bool: | ||
return self.isPresent() | ||
|
||
@staticmethod | ||
def empty() -> "Nullable": | ||
return _empty | ||
|
||
|
||
_empty = Nullable(None) | ||
|
||
|
||
class EmptyNullableException(Exception): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import unittest | ||
from unittest.mock import MagicMock | ||
|
||
from jivago.lang.nullable import Nullable, EmptyNullableException | ||
|
||
ITEM = object() | ||
|
||
|
||
class NullableTest(unittest.TestCase): | ||
|
||
def setUp(self): | ||
self.null = Nullable(None) | ||
self.non_null = Nullable(ITEM) | ||
|
||
def test_whenCheckingIsPresent_thenChecksIfItemValueIsNone(self): | ||
self.assertFalse(self.null.isPresent()) | ||
self.assertTrue(self.non_null.isPresent()) | ||
|
||
def test_whenGettingValue_thenReturnStoredItemValueOrThrowException(self): | ||
self.assertEqual(ITEM, self.non_null.get()) | ||
|
||
with self.assertRaises(EmptyNullableException): | ||
self.null.get() | ||
|
||
def test_whenUsingOrElse_thenReturnsDefaultValueForMissingItem(self): | ||
default_value = object() | ||
|
||
self.assertEqual(ITEM, self.non_null.orElse(default_value)) | ||
self.assertEqual(default_value, self.null.orElse(default_value)) | ||
|
||
def test_whenUsingOrElseThrow_thenRaiseExceptionForMissingItem(self): | ||
an_exception = Exception | ||
|
||
self.assertEqual(ITEM, self.non_null.orElseThrow(an_exception)) | ||
with self.assertRaises(an_exception): | ||
self.null.orElseThrow(an_exception) | ||
|
||
def test_givenExceptionFactory_whenUsingOrElseThrow_thenInvokeTheCallableToGetTheThrownException(self): | ||
callable_which_returns_an_exception = MagicMock() | ||
callable_which_returns_an_exception.return_value = Exception() | ||
|
||
with self.assertRaises(Exception): | ||
self.null.orElseThrow(callable_which_returns_an_exception) | ||
|
||
self.assertTrue(callable_which_returns_an_exception.called) | ||
|
||
def test_whenUsingOrElseFetch_thenInvokeTheCallableForMissingValues(self): | ||
default_value = object() | ||
callable = lambda: default_value | ||
|
||
self.assertEqual(ITEM, self.non_null.get()) | ||
self.assertEqual(default_value, self.null.orElseFetch(callable)) | ||
|
||
def test_whenFiltering_thenCreateANullableWithFilteredItem(self): | ||
criterion = lambda x: x is ITEM | ||
unsatisfied_criterion = lambda x: False | ||
|
||
filtered = self.non_null.filter(criterion) | ||
unsatisfied_filter = self.non_null.filter(unsatisfied_criterion) | ||
|
||
self.assertEqual(ITEM, filtered.get()) | ||
self.assertFalse(unsatisfied_filter.isPresent()) | ||
|
||
def test_givenMissingItem_whenFiltering_thenReturnEmptyNullable(self): | ||
criterion = lambda x: False | ||
|
||
filtered = self.null.filter(criterion) | ||
|
||
self.assertFalse(filtered.isPresent()) | ||
|
||
def test_whenMapping_thenCreateANullableIfValueIsPresent(self): | ||
mapped_value = object() | ||
mapping_function = lambda x: mapped_value | ||
|
||
mapped_null = self.null.map(mapping_function) | ||
mapped_non_null = self.non_null.map(mapping_function) | ||
|
||
self.assertTrue(mapped_non_null.isPresent()) | ||
self.assertFalse(mapped_null.isPresent()) | ||
|
||
def test_givenMissingItem_whenMapping_thenCallbackIsNeverInvoked(self): | ||
mapping_function = MagicMock() | ||
|
||
self.null.map(mapping_function) | ||
|
||
self.assertFalse(mapping_function.called) | ||
|
||
def test_whenEvaluatingToBoolean_thenReturnIfItemIsPresent(self): | ||
self.assertTrue(self.non_null) | ||
self.assertFalse(self.null) | ||
|
||
def test_whenUsingIfPresent_thenInvokeOnlyWhenItemIsPresent(self): | ||
callback = MagicMock() | ||
|
||
self.null.ifPresent(callback) | ||
self.assertFalse(callback.called) | ||
|
||
self.non_null.ifPresent(callback) | ||
self.assertTrue(callback.called) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters