diff --git a/poetry.lock b/poetry.lock index cefa2c6..9a2e23b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "astroid" version = "2.11.7" description = "An abstract syntax tree for Python with inference support." +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -22,6 +23,7 @@ wrapt = ">=1.11,<2" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -33,6 +35,7 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -105,6 +108,7 @@ toml = ["tomli"] name = "dill" version = "0.3.6" description = "serialize all of python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -119,6 +123,7 @@ graph = ["objgraph (>=1.7.2)"] name = "isort" version = "5.11.5" description = "A Python utility / library to sort Python imports." +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -136,6 +141,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "joblib" version = "1.2.0" description = "Lightweight pipelining with Python functions" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -147,6 +153,7 @@ files = [ name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -192,6 +199,7 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -199,21 +207,11 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] -[[package]] -name = "optional-py" -version = "1.3.2" -description = "An implementation of the Optional object in Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "optional.py-1.3.2-py2.py3-none-any.whl", hash = "sha256:41b575c96377b6f8fedc4aef0d0bcc172bbf7f143089f149dc2b4c0812fe2984"}, - {file = "optional.py-1.3.2.tar.gz", hash = "sha256:e0040b29d8e671245b92fffe3abc5aa3bd8870e9a804d7ac7ad377a370cccadc"}, -] - [[package]] name = "parameterized" version = "0.9.0" description = "Parameterized testing with any Python test framework" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -226,26 +224,28 @@ dev = ["jinja2"] [[package]] name = "platformdirs" -version = "3.5.1" +version = "3.8.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, - {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, ] [package.dependencies] -typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] [[package]] name = "pylint" version = "2.13.9" description = "python code static checker" +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -268,13 +268,14 @@ testutil = ["gitpython (>3)"] [[package]] name = "setuptools" -version = "67.8.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, - {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] @@ -286,6 +287,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -297,6 +299,7 @@ files = [ name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -328,19 +331,21 @@ files = [ [[package]] name = "typing-extensions" -version = "4.6.1" +version = "4.6.3" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.6.1-py3-none-any.whl", hash = "sha256:6bac751f4789b135c43228e72de18637e9a6c29d12777023a703fd1a6858469f"}, - {file = "typing_extensions-4.6.1.tar.gz", hash = "sha256:558bc0c4145f01e6405f4a5fdbd82050bd221b119f4bf72a961a1cfd471349d6"}, + {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, + {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, ] [[package]] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -424,4 +429,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "bc19628cf2f534e9ce2bb75506ce23bb5add8a3203605b5542384e49f7ab9df0" +content-hash = "cd51f0d06664ffbb0492e2ea1fa9c88eaf558d7c7d5ef10827ee15ecb56bbb9f" diff --git a/pyproject.toml b/pyproject.toml index 74af2f0..ec3bcf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ packages = [ [tool.poetry.dependencies] python = ">=3.7,<4.0" joblib = "~1.2.0" -optional-py = "^1.3.2" [tool.poetry.group.test.dependencies] parameterized = "*" diff --git a/pystreamapi/__optional.py b/pystreamapi/__optional.py new file mode 100644 index 0000000..0032d3f --- /dev/null +++ b/pystreamapi/__optional.py @@ -0,0 +1,114 @@ +class Optional: + """ + A container object which may or may not contain a non-none value. + + If a value is present, `is_present()` will return `True` and `get()` will return the value. + If a value is not present, `is_present()` will return `False` + and `get()` will raise a `ValueError`. + Additional methods provide ways to handle the presence or absence of a contained value. + + This class is inspired by Java's `Optional` class. + """ + + def __init__(self, value=None): + """ + Constructs an Optional with the given value. + + If the value is None, the Optional is considered empty. + """ + self._value = value + self._is_present = value is not None + + @staticmethod + def of(value): + """ + Returns an Optional with the given non-none value. + + Raises a ValueError if the value is None. + """ + if value is None: + raise ValueError("Value cannot be None") + return Optional(value) + + @staticmethod + def empty(): + """Returns an empty Optional.""" + return Optional() + + def is_present(self): + """Returns `True` if the Optional contains a non-none value, `False` otherwise.""" + return self._is_present + + def get(self): + """Returns the value if present, otherwise raises a `ValueError`.""" + if not self._is_present: + raise ValueError("Value is not present") + return self._value + + def or_else(self, default_value): + """Returns the value if present, otherwise returns the given default value.""" + return self._value if self._is_present else default_value + + def or_else_get(self, supplier): + """ + Returns the value if present, otherwise calls the given supplier function to get a + default value. + """ + return self._value if self._is_present else supplier() + + def map(self, mapper): + """ + Applies the given mapper function to the value if present, returning a new Optional + with the result. + + If the Optional is empty, returns an empty Optional. + """ + if not self._is_present: + return Optional() + mapped_value = mapper(self._value) + return Optional(mapped_value) + + def flat_map(self, mapper): + """ + Applies the given mapper function to the value if present, returning the result. + + If the Optional is empty, returns an empty Optional. + If the mapper function does not return an Optional, raises a TypeError. + """ + if not self._is_present: + return Optional() + optional_result = mapper(self._value) + if not isinstance(optional_result, Optional): + raise TypeError("Mapper function must return an Optional") + return optional_result + + def filter(self, predicate): + """ + Returns an Optional containing the value if present and the predicate is true, + otherwise an empty Optional. + """ + return self if self._is_present and predicate(self._value) else Optional() + + def if_present(self, consumer): + """Calls the given consumer function with the value if present, otherwise does nothing.""" + if self._is_present: + consumer(self._value) + + def __str__(self): + """Returns a string representation of the Optional.""" + return f"Optional({self._value if self._is_present else ''})" + + def __repr__(self): + """Returns a string representation of the Optional.""" + return self.__str__() + + def __eq__(self, other): + """ + Returns `True` if the other object is an Optional with the same value, + `False` otherwise. + """ + return self._value == other._value if isinstance(other, Optional) else False + + def __hash__(self): + """Returns the hash of the Optional's value.""" + return hash(self._value) diff --git a/pystreamapi/_streams/__base_stream.py b/pystreamapi/_streams/__base_stream.py index 78a6595..9a7df55 100644 --- a/pystreamapi/_streams/__base_stream.py +++ b/pystreamapi/_streams/__base_stream.py @@ -2,14 +2,11 @@ from abc import abstractmethod from builtins import reversed from functools import cmp_to_key -from typing import Iterable, Callable, Any, TypeVar, Iterator, Union - -from optional import Optional -from optional.nothing import Nothing -from optional.something import Something +from typing import Iterable, Callable, Any, TypeVar, Iterator from pystreamapi._lazy.process import Process from pystreamapi._lazy.queue import ProcessQueue +from pystreamapi.__optional import Optional K = TypeVar('K') _V = TypeVar('_V') @@ -320,7 +317,7 @@ def max(self): @abstractmethod def reduce(self, predicate: Callable[[K, K], K], identity=_identity_missing, - depends_on_state=False) -> Union[K, Something, Nothing]: + depends_on_state=False) -> Optional: """ Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value. diff --git a/pystreamapi/_streams/__parallel_stream.py b/pystreamapi/_streams/__parallel_stream.py index 1b4d0aa..91099ef 100644 --- a/pystreamapi/_streams/__parallel_stream.py +++ b/pystreamapi/_streams/__parallel_stream.py @@ -2,7 +2,7 @@ from typing import Callable, Any, Iterable from joblib import Parallel, delayed -from optional import Optional +from pystreamapi.__optional import Optional import pystreamapi._streams.__base_stream as stream from pystreamapi._parallel.fork_and_join import Parallelizer @@ -15,7 +15,7 @@ class ParallelStream(stream.BaseStream): def __init__(self, source: Iterable[stream.K]): super().__init__(source) - self.parallelizer = Parallelizer() + self._parallelizer = Parallelizer() def all_match(self, predicate: Callable[[Any], bool]): self._trigger_exec() @@ -24,7 +24,7 @@ def all_match(self, predicate: Callable[[Any], bool]): def _filter(self, predicate: Callable[[Any], bool]): self._set_parallelizer_src() - self._source = self.parallelizer.filter(predicate) + self._source = self._parallelizer.filter(predicate) def find_any(self): self._trigger_exec() @@ -77,11 +77,11 @@ def reduce(self, predicate: Callable[[Any, Any], Any], identity=_identity_missin return identity if identity is not _identity_missing else Optional.empty() def __reduce(self, pred, _): - return self.parallelizer.reduce(pred) + return self._parallelizer.reduce(pred) def to_dict(self, key_mapper: Callable[[Any], Any]) -> dict: self._trigger_exec() return dict(self._group_to_dict(key_mapper)) def _set_parallelizer_src(self): - self.parallelizer.set_source(self._source) + self._parallelizer.set_source(self._source) diff --git a/pystreamapi/_streams/__sequential_stream.py b/pystreamapi/_streams/__sequential_stream.py index f49026b..099ba18 100644 --- a/pystreamapi/_streams/__sequential_stream.py +++ b/pystreamapi/_streams/__sequential_stream.py @@ -1,7 +1,7 @@ from functools import reduce from typing import Callable, Any -from optional import Optional +from pystreamapi.__optional import Optional import pystreamapi._streams.__base_stream as stream diff --git a/pystreamapi/_streams/numeric/__parallel_numeric_stream.py b/pystreamapi/_streams/numeric/__parallel_numeric_stream.py index d8f2c2f..b54ecdd 100644 --- a/pystreamapi/_streams/numeric/__parallel_numeric_stream.py +++ b/pystreamapi/_streams/numeric/__parallel_numeric_stream.py @@ -21,4 +21,4 @@ def sum(self) -> Union[float, int, None]: def __sum(self): """Parallel sum method""" self._set_parallelizer_src() - return self.parallelizer.reduce(lambda x, y: x + y) + return self._parallelizer.reduce(lambda x, y: x + y) diff --git a/tests/test_base_stream.py b/tests/test_base_stream.py index 60ee144..35fc2de 100644 --- a/tests/test_base_stream.py +++ b/tests/test_base_stream.py @@ -1,6 +1,6 @@ import unittest -from optional import Optional +from pystreamapi.__optional import Optional from pystreamapi.__stream import Stream from pystreamapi._streams.__parallel_stream import ParallelStream diff --git a/tests/test_optional.py b/tests/test_optional.py new file mode 100644 index 0000000..0112f3c --- /dev/null +++ b/tests/test_optional.py @@ -0,0 +1,143 @@ +import unittest +from pystreamapi.__optional import Optional + +class TestOptional(unittest.TestCase): + def test_of(self): + # Test that creating an Optional with a non-None value works + optional = Optional.of(5) + self.assertTrue(optional.is_present()) + self.assertEqual(optional.get(), 5) + + # Test that creating an Optional with None raises a ValueError + with self.assertRaises(ValueError): + Optional.of(None) + + def test_empty(self): + # Test that creating an empty Optional works + optional = Optional.empty() + self.assertFalse(optional.is_present()) + + def test_get(self): + # Test that get returns the Optional's value if present + optional = Optional.of(5) + self.assertEqual(optional.get(), 5) + + # Test that get raises a ValueError if the Optional is empty + optional = Optional.empty() + with self.assertRaises(ValueError): + optional.get() + + def test_or_else(self): + # Test that or_else returns the Optional's value if present + optional = Optional.of(5) + self.assertEqual(optional.or_else(10), 5) + + # Test that or_else returns the default value if the Optional is empty + optional = Optional.empty() + self.assertEqual(optional.or_else(10), 10) + + def test_or_else_get(self): + # Test that or_else_get returns the Optional's value if present + optional = Optional.of(5) + self.assertEqual(optional.or_else_get(lambda: 10), 5) + + # Test that or_else_get returns the supplier's value if the Optional is empty + optional = Optional.empty() + self.assertEqual(optional.or_else_get(lambda: 10), 10) + + def test_map(self): + # Test that map applies the mapper function to the Optional's value + optional = Optional.of(5) + mapped_optional = optional.map(lambda x: x * 2) + self.assertTrue(mapped_optional.is_present()) + self.assertEqual(mapped_optional.get(), 10) + + # Test that map returns an empty Optional if the original Optional is empty + optional = Optional.empty() + mapped_optional = optional.map(lambda x: x * 2) + self.assertFalse(mapped_optional.is_present()) + + def test_flat_map(self): + # Test that flat_map applies the mapper function to the + # Optional's value and returns the result + optional = Optional.of(5) + mapped_optional = optional.flat_map(lambda x: Optional.of(x * 2)) + self.assertTrue(mapped_optional.is_present()) + self.assertEqual(mapped_optional.get(), 10) + + # Test that flat_map returns an empty Optional if the original Optional is empty + optional = Optional.empty() + mapped_optional = optional.flat_map(lambda x: Optional.of(x * 2)) + self.assertFalse(mapped_optional.is_present()) + + # Test that flat_map raises a TypeError if the mapper function doesn't return an Optional + optional = Optional.of(5) + with self.assertRaises(TypeError): + optional.flat_map(lambda x: x * 2) + + def test_filter(self): + # Test that filter returns the Optional if the predicate is true + optional = Optional.of(5) + filtered_optional = optional.filter(lambda x: x > 3) + self.assertTrue(filtered_optional.is_present()) + self.assertEqual(filtered_optional.get(), 5) + + # Test that filter returns an empty Optional if the predicate is false + optional = Optional.of(5) + filtered_optional = optional.filter(lambda x: x > 10) + self.assertFalse(filtered_optional.is_present()) + + # Test that filter returns an empty Optional if the original Optional is empty + optional = Optional.empty() + filtered_optional = optional.filter(lambda x: x > 3) + self.assertFalse(filtered_optional.is_present()) + + def test_if_present(self): + # Test that if_present calls the consumer function if the Optional is present + optional = Optional.of(5) + result = [] + optional.if_present(result.append) + self.assertEqual(result, [5]) + + # Test that if_present doesn't call the consumer function if the Optional is empty + optional = Optional.empty() + result = [] + optional.if_present(result.append) + self.assertEqual(result, []) + + def test_str(self): + # Test that str returns the string representation of the Optional's value + optional = Optional.of(5) + self.assertEqual(str(optional), "Optional(5)") + + # Test that str returns "Optional()" if the Optional is empty + optional = Optional.empty() + self.assertEqual(str(optional), "Optional()") + + def test_repr(self): + # Test that repr returns the string representation of the Optional's value + optional = Optional.of(5) + self.assertEqual(repr(optional), "Optional(5)") + + # Test that repr returns "Optional()" if the Optional is empty + optional = Optional.empty() + self.assertEqual(repr(optional), "Optional()") + + def test_eq(self): + # Test that eq returns True if the two Optionals have the same value + optional1 = Optional.of(5) + optional2 = Optional.of(5) + self.assertEqual(optional1, optional2) + + # Test that eq returns False if the two Optionals have different values + optional1 = Optional.of(5) + optional2 = Optional.of(10) + self.assertNotEqual(optional1, optional2) + + # Test that eq returns False if the other object is not an Optional + optional = Optional.of(5) + self.assertNotEqual(optional, 5) + + def test_hash(self): + optional = Optional(5) + self.assertEqual(hash(5), hash(optional)) diff --git a/tests/test_stream_implementation.py b/tests/test_stream_implementation.py index 006a124..9326954 100644 --- a/tests/test_stream_implementation.py +++ b/tests/test_stream_implementation.py @@ -1,13 +1,12 @@ import unittest -from optional import Optional -from optional.something import Something from parameterized import parameterized_class from pystreamapi._streams.__base_stream import BaseStream from pystreamapi._streams.__parallel_stream import ParallelStream from pystreamapi._streams.__sequential_stream import SequentialStream from pystreamapi._streams.numeric.__sequential_numeric_stream import SequentialNumericStream +from pystreamapi.__optional import Optional @parameterized_class("stream", [ @@ -104,8 +103,7 @@ def test_limit_empty(self): def test_reduce_no_identity(self): src = [1, 2, 3, 4, 5] result = self.stream(src).reduce(lambda x, y: x + y) - self.assertEqual(type(result), Something) - self.assertEqual(result.get_or_default("Empty"), sum(src)) + self.assertEqual(result.or_else("Empty"), sum(src)) def test_reduce_with_identity(self): src = [1, 2, 3, 4, 5] @@ -116,7 +114,6 @@ def test_reduce_with_identity(self): def test_reduce_depends_on_state(self): src = [4, 3, 2, 1] result = self.stream(src).reduce(lambda x, y: x - y, depends_on_state=True) - self.assertEqual(type(result), Something) self.assertEqual(result.get(), -2) def test_reduce_empty_stream_no_identity(self):