diff --git a/docs/.pages b/docs/.pages new file mode 100644 index 0000000..51aa747 --- /dev/null +++ b/docs/.pages @@ -0,0 +1,3 @@ +nav: + - ... + - package: package diff --git a/docs/index.md b/docs/index.md index 0b888b0..9601eff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,13 +4,13 @@ ![versions](https://img.shields.io/pypi/pyversions/pycommons-base.svg) ![PyPI - License](https://img.shields.io/pypi/l/pycommons-base) ![PyPI - Downloads](https://img.shields.io/pypi/dw/pycommons-base) +[![codecov](https://codecov.io/gh/pycommons/pycommons-base/branch/main/graph/badge.svg?token=uXZGA4h4sH)](https://codecov.io/gh/pycommons/pycommons-base) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -Python Commons Base package that serves as the base package for all the other - -Visit the [documentation](https://pycommons.github.io/pycommons-base) to view -the usage of each and every class and feature provided by this library. +Python Commons Base package that serves as the base package for all the other pycommons libraries like +[pycommons-lang](https://github.com/pycommons/pycommons-lang) +and [pycommons-collections](https://github.com/pycommons/pycommons-collections). ## Get Started @@ -28,9 +28,13 @@ project poetry add pycommons-base ``` +!!! Note + To install alpha, beta and release candidate versions of the package use the package + published to [https://test.pypi.org](https://test.pypi.org/project/pycommons-base/) + ## License -Apache-2.0 (See [License](LICENSE)) +Apache-2.0 (See [License](https://github.com/pycommons/pycommons-base/blob/main/LICENSE)) ## Author diff --git a/docs/package/.pages b/docs/package/.pages new file mode 100644 index 0000000..b3888a7 --- /dev/null +++ b/docs/package/.pages @@ -0,0 +1,4 @@ +nav: + - ... + +title: Package Documentation diff --git a/docs/package/atomic/.pages b/docs/package/atomic/.pages new file mode 100644 index 0000000..54845ef --- /dev/null +++ b/docs/package/atomic/.pages @@ -0,0 +1,2 @@ +nav: + - pycommons.base.atomic: atomic.md diff --git a/docs/package/atomic/atomic.md b/docs/package/atomic/atomic.md new file mode 100644 index 0000000..e87e3cb --- /dev/null +++ b/docs/package/atomic/atomic.md @@ -0,0 +1 @@ +::: pycommons.base.atomic diff --git a/docs/package/container/.pages b/docs/package/container/.pages new file mode 100644 index 0000000..c4a3dc8 --- /dev/null +++ b/docs/package/container/.pages @@ -0,0 +1,2 @@ +nav: + - pycommons.base.container: container.md diff --git a/docs/package/container/container.md b/docs/package/container/container.md new file mode 100644 index 0000000..cbafc02 --- /dev/null +++ b/docs/package/container/container.md @@ -0,0 +1 @@ +::: pycommons.base.container diff --git a/docs/package/function/.pages b/docs/package/function/.pages new file mode 100644 index 0000000..50b57c9 --- /dev/null +++ b/docs/package/function/.pages @@ -0,0 +1,2 @@ +nav: + - pycommons.base.function: function.md diff --git a/docs/package/function/function.md b/docs/package/function/function.md new file mode 100644 index 0000000..cddcf2a --- /dev/null +++ b/docs/package/function/function.md @@ -0,0 +1 @@ +::: pycommons.base.function diff --git a/mkdocs.yml b/mkdocs.yml index de2ad47..7dac2d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ --- # Project Information -site_name: PyCommons Lang +site_name: pycommons-base site_author: Shashank Sharma -site_description: Python Commons Lang +site_description: Python Commons Base remote_branch: gh-pages remote_name: origin site_url: https://pycommons.github.io/pycommons-base @@ -38,6 +38,9 @@ markdown_extensions: - pymdownx.mark - pymdownx.smartsymbols - pymdownx.superfences + - pymdownx.highlight + - pymdownx.snippets + - pymdownx.superfences - pymdownx.emoji: emoji_generator: !!python/name:pymdownx.emoji.to_png - pymdownx.tasklist: @@ -59,7 +62,21 @@ theme: language: en base_url: https://github.com/pycommons/pycommons-base palette: - primary: 'white' + # Palette toggle for light mode + - scheme: default + primary: blue grey + accent: red + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: blue grey + accent: red + toggle: + icon: material/brightness-4 + name: Switch to light mode # Footer extra: config_override: false @@ -71,6 +88,15 @@ extra: - icon: fontawesome/brands/github link: https://github.com/shashankrnr32 name: shashankrnr32 on GitHub + - icon: fontawesome/brands/twitter + link: https://twitter.com/shashankrnr32 + name: shashankrnr32 on Twitter + - icon: fontawesome/brands/linkedin + link: https://linkedin.com/in/shashankrnr32 + name: shashankrnr32 on LinkedIn + - icon: fontawesome/brands/mastodon + link: https://fosstodon.org/@shashankrnr32 + name: shashankrnr32 on fosstodon plugins: - search diff --git a/poetry.lock b/poetry.lock index b1e0abf..cb656ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -19,21 +19,6 @@ wrapt = [ {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, ] -[[package]] -name = "astunparse" -version = "1.6.3" -description = "An AST unparser for Python" -optional = false -python-versions = "*" -files = [ - {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, - {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, -] - -[package.dependencies] -six = ">=1.6.1,<2.0" -wheel = ">=0.23.0,<1.0" - [[package]] name = "black" version = "22.3.0" @@ -328,6 +313,20 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffe" +version = "0.29.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.7" +files = [ + {file = "griffe-0.29.0-py3-none-any.whl", hash = "sha256:e62ff34b04630c2382e2e277301cb2c29221fb09c04028e62ef35afccc64344b"}, + {file = "griffe-0.29.0.tar.gz", hash = "sha256:6fc892aaa251b3761e3a8d2f5893758e1850ec5d81d4605c4557be0666202a0b"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "idna" version = "3.4" @@ -639,13 +638,13 @@ wcmatch = ">=7" [[package]] name = "mkdocs-material" -version = "9.1.15" +version = "9.1.16" description = "Documentation that simply works" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.1.15-py3-none-any.whl", hash = "sha256:b49e12869ab464558e2dd3c5792da5b748a7e0c48ee83b4d05715f98125a7a39"}, - {file = "mkdocs_material-9.1.15.tar.gz", hash = "sha256:8513ab847c9a541ed3d11a3a7eed556caf72991ee786c31c5aac6691a121088a"}, + {file = "mkdocs_material-9.1.16-py3-none-any.whl", hash = "sha256:f9e62558a6b01ffac314423cbc223d970c25fbc78999860226245b64e64d6751"}, + {file = "mkdocs_material-9.1.16.tar.gz", hash = "sha256:1021bfea20f00a9423530c8c2ae9be3c78b80f5a527b3f822e6de3d872e5ab79"}, ] [package.dependencies] @@ -688,7 +687,7 @@ Markdown = ">=3.3" MarkupSafe = ">=1.1" mkdocs = ">=1.2" mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python-legacy = {version = ">=0.2.1", optional = true, markers = "extra == \"python-legacy\""} +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} pymdown-extensions = ">=6.3" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} @@ -698,19 +697,19 @@ python = ["mkdocstrings-python (>=0.5.2)"] python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] -name = "mkdocstrings-python-legacy" -version = "0.2.3" -description = "A legacy Python handler for mkdocstrings." +name = "mkdocstrings-python" +version = "1.1.2" +description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.7" files = [ - {file = "mkdocstrings-python-legacy-0.2.3.tar.gz", hash = "sha256:3fb58fdabe19c6b52b8bb1d3bb1540b1cd527b562865468d6754e8cd1201050c"}, - {file = "mkdocstrings_python_legacy-0.2.3-py3-none-any.whl", hash = "sha256:1b04d71a4064b0bb8ea9448debab89868a752c7e7bfdd11de480dfbcb9751a00"}, + {file = "mkdocstrings_python-1.1.2-py3-none-any.whl", hash = "sha256:c2b652a850fec8e85034a9cdb3b45f8ad1a558686edc20ed1f40b4e17e62070f"}, + {file = "mkdocstrings_python-1.1.2.tar.gz", hash = "sha256:f28bdcacb9bcdb44b6942a5642c1ea8b36870614d33e29e3c923e204a8d8ed61"}, ] [package.dependencies] -mkdocstrings = ">=0.19" -pytkdocs = ">=0.14" +griffe = ">=0.24" +mkdocstrings = ">=0.20" [[package]] name = "mockito" @@ -985,23 +984,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "pytkdocs" -version = "0.16.1" -description = "Load Python objects documentation." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytkdocs-0.16.1-py3-none-any.whl", hash = "sha256:a8c3f46ecef0b92864cc598e9101e9c4cf832ebbf228f50c84aa5dd850aac379"}, - {file = "pytkdocs-0.16.1.tar.gz", hash = "sha256:e2ccf6dfe9dbbceb09818673f040f1a7c32ed0bffb2d709b06be6453c4026045"}, -] - -[package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} - -[package.extras] -numpy-style = ["docstring_parser (>=0.7)"] - [[package]] name = "pyyaml" version = "6.0" @@ -1328,20 +1310,6 @@ files = [ [package.dependencies] bracex = ">=2.1.1" -[[package]] -name = "wheel" -version = "0.40.0" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, - {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)"] - [[package]] name = "wrapt" version = "1.15.0" @@ -1444,4 +1412,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "580cb9c0119004475c6a93e099ff659cb870a76cc9e00e5f01072a2230cb275e" +content-hash = "b2f36bb8fafe4b7029a374ea2b10e40bd0cc8fc628deabcf8cdc0be2f280f8d5" diff --git a/pycommons/base/atomic/atomic.py b/pycommons/base/atomic/atomic.py index 603b57d..23fb995 100644 --- a/pycommons/base/atomic/atomic.py +++ b/pycommons/base/atomic/atomic.py @@ -1,12 +1,26 @@ from typing import TypeVar, Generic, Optional -from pycommons.base.synchronized import RLockSynchronized, synchronized from pycommons.base.container import Container +from pycommons.base.synchronized import RLockSynchronized, synchronized _T = TypeVar("_T") class Atomic(Container[_T], RLockSynchronized, Generic[_T]): + """ + Atomic mutable container, that holds a value in the object + and only allows synchronized read and write. + This implementation is thread-safe and can be used across multiple threads. + If the container is used only on a single thread, consider + using the [Container][pycommons.base.container], and it's derived classes. + + The object is held on a re-entrant lock during reads and writes + and is unlocked after the operation is complete. + + References: + https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReference.html + """ + def __init__(self, t: Optional[_T] = None): super().__init__(t) RLockSynchronized.__init__(self) diff --git a/pycommons/base/atomic/boolean.py b/pycommons/base/atomic/boolean.py index 4fc7d7e..bc4a3db 100644 --- a/pycommons/base/atomic/boolean.py +++ b/pycommons/base/atomic/boolean.py @@ -1,11 +1,20 @@ from __future__ import annotations from pycommons.base.atomic.atomic import Atomic -from pycommons.base.synchronized import synchronized from pycommons.base.container.boolean import BooleanContainer +from pycommons.base.synchronized import synchronized class AtomicBoolean(BooleanContainer, Atomic[bool]): # pylint: disable=R0901 + """ + Atomic Boolean Container that allows atomic update of the container value. + The object is synchronized for read and write operations so that only one read/write + happens at a time. This is ensured using re-entrant locks. Provides + all the functionalities provided by the + [BooleanContainer][pycommons.base.container.BooleanContainer] + + """ + @synchronized def true(self) -> bool: return super().true() diff --git a/pycommons/base/container/boolean.py b/pycommons/base/container/boolean.py index 3ca56b7..1b9d91f 100644 --- a/pycommons/base/container/boolean.py +++ b/pycommons/base/container/boolean.py @@ -6,27 +6,103 @@ class BooleanContainer(Container[bool]): + """ + A mutable container extends the [Container][pycommons.base.container.Container] and stores + a boolean value. Custom helper methods are provided in the class to manipulate + with the value present in the container. If the value of the boolean is not passed during + initialization, it is set to `False` by default. + + The class also implements the `__bool__` magic method, so that the container object can directly + be used in the conditional expressions. + + Warning: + This class is not thread safe. If you need a thread safe BooleanContainer, checkout + [AtomicBoolean][pycommons.base.atomic.AtomicBoolean] + + Examples: + ```python + from pycommons.base.container import BooleanContainer + + boolean_container = BooleanContainer.with_true() + + assert boolean_container.get() + assert bool(boolean_container) + + boolean_container.false() + + assert boolean_container.get() is False + assert boolean_container.compliment() + ``` + This script is complete, it should run as is. + + References: + https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/mutable/MutableBoolean.html + """ + def __init__(self, flag: bool = False): + """ + Initialize the container with a value, False by default + Args: + flag: value of the container + """ super().__init__(flag) def true(self) -> bool: + """ + Set the value of the container to `True` regardless + of the previous value. + Returns: + True + """ return typing.cast(bool, self.set_and_get(True)) def false(self) -> bool: + """ + Set the value of the container to `False` regardless + of the previous value. + + Returns: + False + """ return typing.cast(bool, self.set_and_get(False)) def compliment(self) -> bool: + """ + Compliment the current value present in the container. If the value + of the container is True, the container value is set to False and False is returned + and vice-versa + Returns: + + """ return typing.cast(bool, self.set_and_get(not self.get())) @classmethod def with_true(cls) -> BooleanContainer: + """ + Class method to initialize a new BooleanContainer with the value `True`. + + Returns: + A new container with value set to `True` + """ return cls(True) @classmethod def with_false(cls) -> BooleanContainer: + """ + Class method to initialize a new BooleanContainer with the value `False`. + + Returns: + A new container with value set to `False` + """ return cls(False) def get(self) -> bool: + """ + Gets the value in the container. + + Returns: + The boolean value in the container. + """ return typing.cast(bool, super().get()) def __bool__(self) -> bool: diff --git a/pycommons/base/container/container.py b/pycommons/base/container/container.py index 9b76afd..39a3e28 100644 --- a/pycommons/base/container/container.py +++ b/pycommons/base/container/container.py @@ -8,13 +8,83 @@ class Container(Generic[_T], Supplier[Optional[_T]]): + """ + Mirrors the functionalities provided by the MutableObject in the Apache Commons-Lang package. + Holds an object if present, None if not. The read and write methods are not thread-safe + and are supposed to be used within the same thread. + + Examples: + ```python + from pycommons.base.container import Container + from pydantic import BaseModel + + class MyModel(BaseModel): + foo: int + bar: str + + model = MyModel(foo=2, bar="test") + container: Container[MyModel] = Container(model) + + print(container.get()) + # MyModel(foo=2, bar='test') + + print(model in container) + # True + + print(container.unset()) + # MyModel(foo=2, bar='test') + + print(container.get()) + # None + + print(container.set_and_get(model)) + # MyModel(foo=2, bar='test') + + print(container.get_and_set(MyModel(foo=3, bar="test2"))) + # MyModel(foo=2, bar='test') + + container.get() + # MyModel(foo=3, bar='test2') + + ``` + This script is complete, it should run as is. + + References: + https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/mutable/MutableObject.html + """ + def get(self) -> Optional[_T]: + """ + Get the value held by the container. + + Returns: + The object held by the container object + """ return self._object def __init__(self, t: Optional[_T] = None): + """ + Initialize the container + + Args: + t: value to be held by container + """ self._object: Optional[_T] = t def set(self, t: Optional[_T]) -> None: + """ + Set the value held by the container. This removes the current + value held by the container and holds the new value sent in + the arguments. The reference to the old is lost after + the execution of this method. To get the old value after setting + a new value, use [`get_and_set`][pycommons.base.container.Container.get_and_set] + + Args: + t: The new value + + Returns: + None + """ self._object = t def set_and_get(self, t: Optional[_T]) -> Optional[_T]: @@ -22,6 +92,16 @@ def set_and_get(self, t: Optional[_T]) -> Optional[_T]: return self._object def get_and_set(self, t: Optional[_T]) -> Optional[_T]: + """ + Stores the current value in a temporary object, sets the new value with + the argument sent in the methods and returns the old value. + + Args: + t: The new value + + Returns: + The old value of the container + """ old_object = self._object self._object = t return old_object diff --git a/pycommons/base/container/integer.py b/pycommons/base/container/integer.py index a7466c2..3a7f580 100644 --- a/pycommons/base/container/integer.py +++ b/pycommons/base/container/integer.py @@ -4,37 +4,139 @@ class IntegerContainer(Container[int]): + """ + A mutable container that holds an integer value. Provides the functionalities + of Commons-Lang's MutableInt. Provides helper methods to modify the values + by adding/subtracting another integer from the existing value. + By default, the value is initialized to 0 if not passed during initialization. + The class implements multiple magic methods that makes it easy to use in conditional + expressions and other type conversions + + Warning: + This container implementation is not thread-safe. Use + [AtomicInteger][pycommons.base.atomic.AtomicInteger] for synchronized + read/write operations + """ + def __init__(self, value: int = 0): + """ + Initialize the container with a value, zero by default + + Args: + value: The value the container holds + """ super().__init__(value) def add(self, val: int) -> None: + """ + Add an integer to the container value and set the result + as the container value. + + Args: + val: Value to be added to the container value + + Returns: + None + """ self.set(self.get() + val) def add_and_get(self, val: int) -> int: + """ + Add a value to the current value and get the result + + Args: + val: The value to be added to the container value + + Returns: + + """ return typing.cast(int, self.set_and_get(self.get() + val)) def get_and_add(self, val: int) -> int: + """ + Returns the current value and adds the current value with the + value passed in the argument + + Args: + val: The value to be added to the container value + + Returns: + The current value before performing addition + """ return typing.cast(int, self.get_and_set(self.get() + val)) def increment(self) -> None: + """ + Increments the container value by one. + + Returns: + None + """ return self.add(1) def increment_and_get(self) -> int: + """ + Increments the container value by one and returns the resulting value. + + Returns: + The container value after increment operation + """ return self.add_and_get(1) def get_and_increment(self) -> int: + """ + Gets the current value and then increments the value by one. + + Returns: + The container value before increment operation. + """ return self.get_and_add(1) def subtract(self, val: int) -> None: + """ + Subtract a value from the container value + + Args: + val: The value to be subtracted from container value + + Returns: + None + """ return self.add(-val) def subtract_and_get(self, val: int) -> int: + """ + Subtracts the value from container value and returns + the resulting operation after setting it in the container. + + Args: + val: value to be subtracted from the container value + + Returns: + The resulting value after subtraction operation + """ return self.add_and_get(-val) def get_and_subtract(self, val: int) -> int: + """ + Gets the current value of the container and then subtracts the value + from the container value + + Args: + val: The value to be subtracted from the container value + + Returns: + The value before the subtraction operation + """ return self.get_and_add(-val) def get(self) -> int: + """ + The current value of the container + + Returns: + The current value of the container + """ return typing.cast(int, super().get()) def __int__(self) -> int: diff --git a/pycommons/base/container/optional.py b/pycommons/base/container/optional.py index d365378..263e527 100644 --- a/pycommons/base/container/optional.py +++ b/pycommons/base/container/optional.py @@ -6,8 +6,8 @@ from pycommons.base.function.consumer import Consumer from pycommons.base.function.function import Function from pycommons.base.function.predicate import Predicate -from pycommons.base.function.runnable import Runnable -from pycommons.base.function.supplier import Supplier +from pycommons.base.function.runnable import RunnableType, Runnable +from pycommons.base.function.supplier import Supplier, SupplierType from pycommons.base.utils.objectutils import ObjectUtils _T = TypeVar("_T", Any, None) @@ -18,15 +18,20 @@ class OptionalContainer(Generic[_T]): """ Identical implementation of Java's Optional. - A container object which may or may not contain a non-null value + A container object which may or may not contain a non-null value. - See Also + References: https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html """ _value: _T def __init__(self, value: Optional[_T]): + """ + Initialize the container with a value if present, None if not. + Args: + value: The initializing value. Can be None or not-None + """ self._value = value def get(self) -> _T: @@ -76,7 +81,7 @@ def if_present(self, consumer: Consumer[_T]) -> None: if self.is_present(): consumer.accept(self._value) - def if_present_or_else(self, consumer: Consumer[_T], runnable: Runnable) -> None: + def if_present_or_else(self, consumer: Consumer[_T], runnable: RunnableType) -> None: """ Runs a consumer if the value is present, else runs the runnable Args: @@ -90,7 +95,7 @@ def if_present_or_else(self, consumer: Consumer[_T], runnable: Runnable) -> None if self.is_present(): consumer.accept(self._value) else: - runnable.run() + Runnable.of(runnable).run() def filter(self, predicate: Predicate[_T]) -> OptionalContainer[_T]: ObjectUtils.require_not_none(predicate) @@ -113,22 +118,22 @@ def flat_map(self, mapper: Function[_T, OptionalContainer[_U]]) -> OptionalConta return ObjectUtils.get_not_none(mapper.apply(self._value)) - def in_turn(self, supplier: Supplier[OptionalContainer[_T]]) -> OptionalContainer[_T]: + def in_turn(self, supplier: SupplierType[OptionalContainer[_T]]) -> OptionalContainer[_T]: if self.is_present(): return self - return ObjectUtils.get_not_none(supplier.get()) + return ObjectUtils.get_not_none(Supplier.of(supplier).get()) def or_else(self, other: _T) -> _T: return self._value if self.is_present() else other - def or_else_get(self, supplier: Supplier[_T]) -> _T: - return self._value if self.is_present() else supplier.get() + def or_else_get(self, supplier: SupplierType[_T]) -> _T: + return self._value if self.is_present() else Supplier.of(supplier).get() - def or_else_throw(self, supplier: Optional[Supplier[_E]] = None) -> _T: + def or_else_throw(self, supplier: Optional[SupplierType[_E]] = None) -> _T: if self.is_empty(): if supplier: - raise supplier.get() + raise Supplier.of(supplier).get() raise NoSuchElementError("No value present") return self._value diff --git a/pycommons/base/exception/__init__.py b/pycommons/base/exception/__init__.py index 5be56f6..3766689 100644 --- a/pycommons/base/exception/__init__.py +++ b/pycommons/base/exception/__init__.py @@ -1,3 +1,15 @@ -from .no_such_element_error import NoSuchElementError - __all__ = ["NoSuchElementError"] + + +class NoSuchElementError(RuntimeError): + """ + Raised when an object is expected to be present but is not in real. Extends RuntimeError as + this error happens during runtime. + """ + + +class IllegalStateException(RuntimeError): + """ + Runtime Error raised when the program's state is not in a expected state or the code execution + should never have reached this state + """ diff --git a/pycommons/base/exception/no_such_element_error.py b/pycommons/base/exception/no_such_element_error.py deleted file mode 100644 index e633ad5..0000000 --- a/pycommons/base/exception/no_such_element_error.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -no_such_element_error.py - -Consists of an Exception "NoSuchElementError" that is raised when an -element is expected to be present (in an Optional) but is not present. -""" - - -class NoSuchElementError(RuntimeError): - """ - Raised when an object is expected to be present but is not in real. Extends RuntimeError as - this error happens during runtime. - """ diff --git a/pycommons/base/function/__init__.py b/pycommons/base/function/__init__.py index 5ba2bbb..04d7ba2 100644 --- a/pycommons/base/function/__init__.py +++ b/pycommons/base/function/__init__.py @@ -2,8 +2,22 @@ from .consumer import Consumer, BiConsumer from .function import Function -from .predicate import Predicate, BiPredicate -from .runnable import Runnable -from .supplier import Supplier +from .predicate import Predicate, BiPredicate, PredicateType, PredicateCallableType +from .runnable import Runnable, RunnableType, RunnableCallableType +from .supplier import Supplier, SupplierType, SupplierCallableType -__all__ = ["BiConsumer", "Consumer", "Function", "Predicate", "Runnable", "Supplier", "BiPredicate"] +__all__ = [ + "BiConsumer", + "Consumer", + "Function", + "Predicate", + "PredicateType", + "PredicateCallableType", + "Runnable", + "Supplier", + "BiPredicate", + "SupplierCallableType", + "SupplierType", + "RunnableType", + "RunnableCallableType", +] diff --git a/pycommons/base/function/interface.py b/pycommons/base/function/interface.py new file mode 100644 index 0000000..320e607 --- /dev/null +++ b/pycommons/base/function/interface.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from typing import TypeVar, Any + + +class FunctionalInterface(ABC): + __TYPE__: TypeVar + + @abstractmethod + def __call__(self, *args: Any, **kwargs: Any) -> Any: + ... diff --git a/pycommons/base/function/predicate.py b/pycommons/base/function/predicate.py index 2830bb6..5ef263b 100644 --- a/pycommons/base/function/predicate.py +++ b/pycommons/base/function/predicate.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import TypeVar, Generic, Callable, Any +from typing import TypeVar, Generic, Callable, Any, Union from pycommons.base.utils.objectutils import ObjectUtils @@ -10,33 +10,104 @@ class Predicate(Generic[_T]): + """ + A functional interface that takes a value and returns a boolean result based on some + operation performed on the object passed. Similar to Java's Predicate. + + References: + https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html + """ + @classmethod - def of(cls, predicate: Callable[[_T], bool]) -> Predicate[_T]: + def of(cls, predicate: PredicateType[_T]) -> Predicate[_T]: + """ + If the passed argument is a callable, then wraps the callable in a Basic Predicate instance. + If the argument is already a predicate, then the method returns the passed argument. + Args: + predicate: Predicate Type (Callable/Instance) + + Returns: + Predicate Instance + """ ObjectUtils.require_not_none(predicate) class BasicPredicate(Predicate[_T]): def test(self, value: _T) -> bool: return predicate(value) + if isinstance(predicate, Predicate): + return predicate return BasicPredicate() @abstractmethod def test(self, value: _T) -> bool: - pass + """ + The functional interface method that takes a value and returns + a boolean result + + Args: + value: Value passed to the predicate + + Returns: + A boolean result + """ def negate(self) -> Predicate[_T]: + """ + Returns a predicate that results in the negation of the current predicate's + [`test`][pycommons.base.function.Predicate.test] result. The resulting predicate + is wrapped in a Local Anonymous Predicate object + Returns: + + """ return self.of(lambda _t: not self.test(_t)) def do_and(self, predicate: Predicate[_T]) -> Predicate[_T]: + """ + Returns a predicate that `and`s the result of the current predicate and the + argument predicate + + Args: + predicate: predicate + + Returns: + A wrapped predicate whose result is an `and` operation of the current predicate + and the argument predicate + """ + ObjectUtils.require_not_none(predicate) return self.of(lambda _t: self.test(_t) and predicate.test(_t)) def do_or(self, predicate: Predicate[_T]) -> Predicate[_T]: + """ + Returns a predicate that `or`s the result of the current predicate and the + argument predicate + + Args: + predicate: predicate + + Returns: + A wrapped predicate whose result is an `or` operation of the current predicate + and the argument predicate + """ + ObjectUtils.require_not_none(predicate) return self.of(lambda _t: self.test(_t) or predicate.test(_t)) def __call__(self, t: _T, *args: Any, **kwargs: Any) -> bool: return self.test(t) +PredicateCallableType = Callable[[_T], bool] +""" +A callable function that adheres to the signature of a predicate +""" + +PredicateType = Union[Predicate[_T], PredicateCallableType[_T]] +""" +The type variable that is either a callable or a Predicate type which can be passed on to the +[`Predicate.of`][pycommons.base.function.Predicate.of] to get an instance of predicate +""" + + class BiPredicate(Generic[_T, _U]): @classmethod def of(cls, predicate: Callable[[_T, _U], bool]) -> BiPredicate[_T, _U]: diff --git a/pycommons/base/function/runnable.py b/pycommons/base/function/runnable.py index d6dbb38..852ee57 100644 --- a/pycommons/base/function/runnable.py +++ b/pycommons/base/function/runnable.py @@ -1,23 +1,64 @@ from __future__ import annotations from abc import abstractmethod -from typing import TypeVar, Callable, Any +from typing import TypeVar, Callable, Any, Union _T = TypeVar("_T") class Runnable: + """ + Provides the functionalities of the Java's Runnable functional interface with the + interface method [`run`][pycommons.base.function.Runnable.run]. + The interface can be used with the threading operations. The interface + provides a wrapper classmethod named [`of`][pycommons.base.function.Runnable.of] that + wraps a callable in Runnable instance. + + References: + https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html + """ + @classmethod - def of(cls, runnable: Callable[[], None]) -> Runnable: + def of(cls, runnable: RunnableType) -> Runnable: + """ + Wraps a callable in a BasicRunnable instance. If the passed object is a runnable, then + the supplier is not runnable. + + Args: + runnable: Runnable object or a callable lambda + + Returns: + An instance of runnable. + """ + class BasicRunnable(Runnable): def run(self) -> None: runnable() + if isinstance(runnable, Runnable): + return runnable return BasicRunnable() @abstractmethod def run(self) -> None: - pass + """ + Run the interface method + + Returns: + None + """ def __call__(self, *args: Any, **kwargs: Any) -> None: self.run() + + +RunnableCallableType = Callable[[], None] +""" +A callable function that adheres the signature of a runnable +""" + +RunnableType = Union[Runnable, RunnableCallableType] +""" +The type variable that indicates a runnable type, a lambda or a runnable instance +that can be passed to the [`Runnable.of`][pycommons.base.function.Runnable.of] classmethod +""" diff --git a/pycommons/base/function/supplier.py b/pycommons/base/function/supplier.py index d3ba690..59aeed9 100644 --- a/pycommons/base/function/supplier.py +++ b/pycommons/base/function/supplier.py @@ -1,23 +1,67 @@ from __future__ import annotations from abc import abstractmethod -from typing import TypeVar, Generic, Callable, Any +from typing import TypeVar, Generic, Callable, Any, Union + +from pycommons.base.function.interface import FunctionalInterface _T = TypeVar("_T") -class Supplier(Generic[_T]): +class Supplier(FunctionalInterface, Generic[_T]): + """ + A functional interface that has a method `get` that returns a value. Mirrors the Java's Supplier + interface. A lambda or another supplier can be wrapped into a supplier by calling the + [`of`][pycommons.base.function.Supplier.of] classmethod. The method also implements the + `__call__` method so that an instance of supplier can be called like a function. + + References: + https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html + """ + @classmethod - def of(cls, supplier: Callable[[], _T]) -> Supplier[_T]: + def of(cls, supplier: SupplierType[_T]) -> Supplier[_T]: + """ + Wrap a lambda or a function in a Basic Supplier Implementation + that just calls the mentioned lambda. If the passed object is a supplier, + then it is returned without wrapping. + Args: + supplier: A supplier type object + + Returns: + A supplier object regardless of the input. + """ + class BasicSupplier(Supplier[_T]): def get(self) -> _T: return supplier() + if isinstance(supplier, Supplier): + return supplier return BasicSupplier() @abstractmethod def get(self) -> _T: - pass + """ + The interface abstract method. + + Returns: + The supplier return object + """ def __call__(self, *args: Any, **kwargs: Any) -> _T: return self.get() + + +SupplierCallableType = Callable[[], _T] +""" +A callable function that adheres the signature of a supplier +""" + +SupplierType = Union[Supplier[_T], SupplierCallableType[_T]] +""" +The generic supplier object that can be passed to the +[`Supplier.of`][pycommons.base.function.Supplier.of]. +Has the references to both Supplier and the type of lambdas +that can defined for it to be called a supplier lambda. +""" diff --git a/pyproject.toml b/pyproject.toml index 81e6b83..1cfb6e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,8 @@ importlib_metadata = "*" typing_extensions = "4.6.3" [tool.poetry.dev-dependencies] -mkdocs-material = ">7.0.0" -mkdocstrings = { version = ">0.18", extras = ["python-legacy"] } +mkdocs-material = ">7.1.0" +mkdocstrings = { version = ">0.18", extras = ["python"] } mkdocs-awesome-pages-plugin = "*" markdown-include = "*" livereload = "*" diff --git a/tests/pycommons/base/function/__init__.py b/tests/pycommons/base/function/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pycommons/base/function/test_supplier.py b/tests/pycommons/base/function/test_supplier.py new file mode 100644 index 0000000..02e828f --- /dev/null +++ b/tests/pycommons/base/function/test_supplier.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +from pycommons.base.function import Supplier + + +class TestSupplier(TestCase): + def test_supplier_with_lambda(self): + supplier = Supplier.of(lambda: 4**2) + self.assertEqual(16, supplier()) + self.assertEqual(16, supplier.get()) + self.assertTrue("BasicSupplier" in str(type(supplier))) + + def test_supplier_with_functional_interface(self): + class PowerSupplier(Supplier[int]): + def get(self) -> int: + return 4**2 + + supplier = Supplier.of(PowerSupplier()) + self.assertEqual(16, supplier()) + self.assertEqual(16, supplier.get()) + self.assertFalse("BasicSupplier" in str(type(supplier)))