Skip to content

Commit

Permalink
Merge pull request #84 from keotl/feature/nullable
Browse files Browse the repository at this point in the history
nullable class
  • Loading branch information
keotl committed Mar 6, 2019
2 parents 2f05c82 + 8127496 commit 58c0e7d
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 23 deletions.
4 changes: 3 additions & 1 deletion jivago/inject/service_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from jivago.inject.exception.non_injectable_constructor_exception import NonInjectableConstructorException
from jivago.inject.scope_cache import ScopeCache
from jivago.lang.annotations import Inject
from jivago.lang.nullable import Nullable
from jivago.lang.registry import Registry
from jivago.lang.stream import Stream

Expand Down Expand Up @@ -109,4 +110,5 @@ def __is_injectable(self, constructor_function: Callable) -> bool:
return True

def __get_scope(self, component: type) -> Optional[ScopeCache]:
return Stream(self.scopeCaches).firstMatch(lambda scope: scope.handles_component(component))
found_scope = Stream(self.scopeCaches).firstMatch(lambda scope: scope.handles_component(component))
return found_scope.get() if found_scope.isPresent() else None
97 changes: 97 additions & 0 deletions jivago/lang/nullable.py
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
21 changes: 12 additions & 9 deletions jivago/lang/stream.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import inspect
import itertools
from typing import Iterable, Callable, Iterator, Optional, Tuple, Any, TypeVar, Generic, Set, List
from typing import Iterable, Callable, Iterator, Tuple, Any, TypeVar, Generic, Set, List

from jivago.lang.nullable import Nullable

T = TypeVar('T')
S = TypeVar('S')


class Stream(Generic[T]):
"""Stream class to perform functional-style operations in an aesthetically-pleasing manner.
Expand Down Expand Up @@ -50,17 +53,17 @@ def noneMatch(self, fun: Callable[[T], bool]) -> bool:
"""Returns True if no element of the stream matches the criteria."""
return not self.anyMatch(fun)

def firstMatch(self, fun: Callable[[T], bool]) -> Optional[T]:
"""Returns the first element matching the criteria. If none exist, returns None."""
def firstMatch(self, fun: Callable[[T], bool]) -> Nullable[T]:
"""Returns a Nullable of the first element matching the criteria. If none exist, returns an empty Nullable."""
if self.__should_expand(fun):
for i in self:
if fun(*i):
return i
return Nullable(i)
else:
for i in self:
if fun(i):
return i
return None
return Nullable(i)
return Nullable.empty()

def flat(self) -> "Stream[T]":
"""When iterating over lists, flattens the stream by concatenating all lists."""
Expand Down Expand Up @@ -163,6 +166,6 @@ def range(*args) -> "Stream[int]":
else:
return Stream(range(*args))

def first(self) -> Optional[T]:
"""Returns the first element of the stream. If the stream is empty, returns None."""
return next(self.iterable, None)
def first(self) -> Nullable[T]:
"""Returns a nullable containing the first element of the stream. If the stream is empty, returns an empty nullable."""
return Nullable(next(self.iterable, None))
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def doFilter(self, request: Request, response: Response, chain: FilterChain):
chain.doFilter(request, response)
except Exception as e:
exception_mapper = Stream(self.exception_mappers).firstMatch(lambda mapper: mapper.handles(e))
if exception_mapper is not None:
response.copy(exception_mapper.create_response(e))
if exception_mapper.isPresent():
response.copy(exception_mapper.get().create_response(e))
else:
raise e
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import inspect
from typing import Callable, Optional

from jivago.lang.nullable import Nullable
from jivago.lang.stream import Stream
from jivago.serialization.dto_serialization_handler import DtoSerializationHandler
from jivago.wsgi.invocation.missing_route_invocation_argument import MissingRouteInvocationArgument
Expand Down Expand Up @@ -33,12 +34,12 @@ def get_parameters(self, request: Request, method: Callable) -> list:
parameters = []
for name, parameter in inspect.signature(method).parameters.items():
parameter_type = parameter._annotation
selector: Optional[ParameterSelector] = Stream(self.parameter_selectors) \
selector: Nullable[ParameterSelector] = Stream(self.parameter_selectors) \
.filter(lambda s: s.matches(parameter_type)) \
.first()

if selector:
parameters.append(selector.format_parameter(name, parameter_type, request))
if selector.isPresent():
parameters.append(selector.get().format_parameter(name, parameter_type, request))
else:
raise MissingRouteInvocationArgument(name)

Expand Down
2 changes: 1 addition & 1 deletion jivago/wsgi/request/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __getitem__(self, item: str) -> str:
found_header = Stream(self.content.items()).firstMatch(
lambda key, value: key == item or _format_camel_case(key) == _format_camel_case(item))
if found_header:
return found_header[1]
return found_header.get()[1]
return None

def values(self):
Expand Down
4 changes: 2 additions & 2 deletions jivago/wsgi/routing/table/reflective_routing_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ def __init__(self, registry: Registry, resources: List[Registration]):
route_sub_path = Stream(sub_paths).firstMatch(lambda r: r.registered == route_function.registered)
resource_path = resource.arguments['value']

if route_sub_path is None:
if not route_sub_path.isPresent():
self.register_route(primitive, resource_path, resource.registered,
route_function.registered)
else:
sub_path = route_sub_path.arguments['value']
sub_path = route_sub_path.get().arguments['value']
path = resource_path + sub_path if \
resource_path.endswith('/') or sub_path.startswith('/') \
else resource_path + '/' + sub_path
Expand Down
99 changes: 99 additions & 0 deletions test/lang/test_nullable.py
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)
10 changes: 5 additions & 5 deletions test/lang/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_givenFunctionWithTwoParameters_whenFiltering_thenExpandTuplesWhenCallin
def test_givenFunctionWithTwoParameters_whenFindingFirstMatch_thenExpandTuplesWhenCallingFunction(self):
result = self.stream.map(lambda x: (x, x)).firstMatch(lambda x, y: x == y)

self.assertEqual((self.COLLECTION[0], self.COLLECTION[0]), result)
self.assertEqual((self.COLLECTION[0], self.COLLECTION[0]), result.get())

def test_givenFunctionWithTwoParameters_whenIteratingOverScalars_thenThrowTypeError(self):
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -236,15 +236,15 @@ def test_givenParameters_whenGettingRange_thenPassParametersToBuiltinRange(self)

self.assertEqual([x for x in range(0, 5)], first5)

def test_whenGettingFirst_thenReturnFirstElementInIterable(self):
first = Stream.range().first()
def test_whenGettingFirst_thenReturnNullableContainingFirstElementInIterable(self):
first = Stream.range().first().get()

self.assertEqual(0, first)

def test_givenEmptyStream_whenGettingFirst_thenReturnNone(self):
def test_givenEmptyStream_whenGettingFirst_thenReturnEmptyNullable(self):
first = Stream([]).first()

self.assertIsNone(first)
self.assertFalse(first.isPresent())


class AClassWithAMethod(object):
Expand Down

0 comments on commit 58c0e7d

Please sign in to comment.