diff --git a/README.md b/README.md index e69de29..67827ab 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# python_js + +`python_js` is a simple Python module which ports some JS functions to Python. + +This is not production safe and it's made for learning purposes. I know this is not Pythonic's stylish but it's just a playground to learn some basics about this language. + +## Features + +- Some array methods +- Type checking +- Linter +- Local publishing using `Pypi` test environment. +- Automatic publish using `Github Actions` + `semantic-release` +- Local & CI testing using `tox` + +## Requirements + +- `python >= 3.10` +- `pipenv` + +## Install + +```bash +pipenv install +``` + +## Running linters + +```bash +pipenv run lint +``` + +## Running tests + +Run all tests using `tox`: + +```bash +tox +``` + +Using the current python version: + +```bash +pipenv run test +``` + +## Publish + +Test `Pypi` from local: + +```bash +pipenv run publish_raw +``` diff --git a/python_js/__init__.py b/python_js/__init__.py index a88d0e8..81b91eb 100644 --- a/python_js/__init__.py +++ b/python_js/__init__.py @@ -4,4 +4,8 @@ This is not production safe and it's made for learning purposes. """ -__author__ = 'Jose Luis Represa' +from .array import Array + +__all__ = [ + "Array", +] diff --git a/python_js/array.py b/python_js/array.py new file mode 100644 index 0000000..4d74674 --- /dev/null +++ b/python_js/array.py @@ -0,0 +1,23 @@ +# from .array.map import map, CallbackType, ReturnValue +from __future__ import annotations + +from typing import List, TypeVar, Callable, Any + +from .array_methods import concat, filter, map, reduce + +T = TypeVar("T") +K = TypeVar("K") + + +class Array(List[T]): + def concat(self, *args: List[Any]) -> Array[Any]: + return Array(concat(self, *args)) + + def filter(self, func: Callable[[T, int], bool]) -> Array[Any]: + return Array(filter(self, func)) + + def map(self, func: Callable[[T, int], K]) -> Array[K]: + return Array(map(self, func)) + + def reduce(self, func: Callable[[Any, T, int], Any], initialValue: K | None = None) -> Any: + return reduce(self, func, initialValue=initialValue) diff --git a/python_js/array/__init__.py b/python_js/array/__init__.py deleted file mode 100644 index 8a43a4d..0000000 --- a/python_js/array/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Array utils. - -Export all things -""" - -from .map import map diff --git a/python_js/array/map.py b/python_js/array/map.py deleted file mode 100644 index c1e9140..0000000 --- a/python_js/array/map.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -from typing import Iterable, List, Protocol, TypeVar # Protocol - -# P = ParamSpec('P') -T = TypeVar('T', contravariant=True) -K = TypeVar('K') - -T_contra = TypeVar('T_contra', contravariant=True) -K_co = TypeVar('K_co', covariant=True) - - -class Callback(Protocol[T_contra, K_co]): - """Definition of a callback function protocol.""" - - def __call__( - self, - value: T_contra, - index: int, - ) -> K_co: - """ - Definition of a callback function. - - Args: - ---- - value: An element of the iterable. - index: The index of the element. - - """ - pass - - -def map(iterable: Iterable[T], func: Callback[T, K]) -> List[K]: - """ - Return a new array with the results of calling func on each element. - - Args: - iterable: The iterable to map. - func: The callback which transforms each value. - - Return: - A List with each value result. - - Examples - -------- - map([1, 2, 3], lambda x: x * 2) # [2, 4, 6] - - """ - return [func(value, index) for index, value in enumerate(iterable)] diff --git a/python_js/array_methods/__init__.py b/python_js/array_methods/__init__.py new file mode 100644 index 0000000..e901649 --- /dev/null +++ b/python_js/array_methods/__init__.py @@ -0,0 +1,13 @@ +from .concat import concat +from .filter import filter +from .map import map +from .reduce import reduce + +# __all__ = tuple(k for k in locals() if not k.startswith("_")) + +__all__ = [ + "concat", + "filter", + "map", + "reduce" +] diff --git a/python_js/array_methods/concat.py b/python_js/array_methods/concat.py new file mode 100644 index 0000000..7665810 --- /dev/null +++ b/python_js/array_methods/concat.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any, List + + +def concat(*args: List[Any]) -> List[Any]: + """ + Merge two or more arrays. + This method does not change the existing arrays, but instead returns a new array. + + Args: + *args: The iterables to mergle. + + Return: + A new element. + + Examples + -------- + concat( + ["a", "b", "c"], + [1, 2, 3] + ) # ["a", "b", "c", 1, 2, 3] + + """ + result: List[Any] = [] + + for x in args: + result = [*result, *x] + + return result diff --git a/python_js/array_methods/filter.py b/python_js/array_methods/filter.py new file mode 100644 index 0000000..d84810f --- /dev/null +++ b/python_js/array_methods/filter.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from typing import Iterable, TypeVar, List, Callable, Any + +T = TypeVar('T') + + +def filter(iterable: Iterable[T], func: Callable[[T, int], bool]) -> List[Any]: + """ + Return a new array with the the elements which passes the callback. + + Args: + iterable: The iterable to map. + func: The callback which filters the values. + + Return: + A filtered List. + + Examples + -------- + filter([1, 2, 3], lambda x: x > 2) # [3] + + """ + return [ + value for index, value in enumerate(iterable) if func(value, index) + ] diff --git a/python_js/array_methods/map.py b/python_js/array_methods/map.py new file mode 100644 index 0000000..b0f767f --- /dev/null +++ b/python_js/array_methods/map.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from typing import Iterable, TypeVar, List, Callable + +T = TypeVar('T') +K = TypeVar('K') + + +def map(iterable: Iterable[T], func: Callable[[T, int], K]) -> List[K]: + """ + Return a new array with the results of calling func on each element. + + Args: + iterable: The iterable to map. + func: The callback which transforms each value. + + Return: + A List with each value result. + + Examples + -------- + map([1, 2, 3], lambda x: x * 2) # [2, 4, 6] + + """ + return [func(value, index) for index, value in enumerate(iterable)] diff --git a/python_js/array_methods/reduce.py b/python_js/array_methods/reduce.py new file mode 100644 index 0000000..6e5b0c8 --- /dev/null +++ b/python_js/array_methods/reduce.py @@ -0,0 +1,36 @@ +from __future__ import annotations +from typing import Iterable, TypeVar, Callable, Any + +T = TypeVar('T') +K = TypeVar('K') + + +def reduce(iterable: Iterable[T], func: Callable[[Any, T, int], Any], initialValue: K | None = None) -> Any: + """ + Executes a user-supplied “reducer” callback function on each element of + the array, in order, passing in the return value from the calculation on + the preceding element. The final result of running the reducer across all + elements of the array is a single value. + + Args: + iterable: The iterable to map. + func: The callback which transforms all the values. + + Return: + A new element. + + Examples + -------- + reduce( + ["a", "b", "c"], + lambda acc, x, i: (acc[x] = i) and acc, + {} + ) # { 'a': 0, 'b': 1, 'c': 2 } + + """ + result = initialValue + + for i, x in enumerate(iterable): + result = func(result, x, i) + + return result diff --git a/setup.cfg b/setup.cfg index 738fa4c..a747276 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [semantic_release] version_variable = setup.py:__version__ + +[flake8] +max-line-length = 120 diff --git a/tests/array/test_concat.py b/tests/array/test_concat.py new file mode 100644 index 0000000..5385923 --- /dev/null +++ b/tests/array/test_concat.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import List + +from python_js.array_methods.concat import concat + +seasons: List[str] = ['Winter', 'Summer', 'Fall', 'Spring'] +numbers: List[int] = [1, 2] +seasons_and_numbers: List[int | str] = [*seasons, *numbers] + + +def test_returned_value_instance() -> None: + formatted_seasons: List[str | int] = concat(seasons, numbers) + + # Then + assert isinstance(formatted_seasons, List) + + +def test_input_argument_iterable() -> None: + result: List[str | int] = concat(seasons, numbers) + + # Then + assert result == seasons_and_numbers diff --git a/tests/array/test_filter.py b/tests/array/test_filter.py new file mode 100644 index 0000000..37d7681 --- /dev/null +++ b/tests/array/test_filter.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Callable, List + +from python_js.array_methods.filter import filter + +seasons: List[str] = ['Winter', 'Summer', 'Fall', 'Spring'] +numbers: List[int] = [1, 2] +seasons_and_numbers: List[int | str] = [*seasons, *numbers] + + +def test_returned_value_instance() -> None: + formatted_seasons: List[str] = filter(seasons, lambda x, i: bool(x)) + + # Then + assert isinstance(formatted_seasons, List) + + +def test_input_argument_iterable() -> None: + format_str: Callable[[str | int, int], + bool] = lambda value, index: isinstance(value, str) + + formatted_seasons_and_numbers: List[str] = filter( + seasons_and_numbers, format_str) + + # Then + assert formatted_seasons_and_numbers == [ + 'Winter', 'Summer', 'Fall', 'Spring' + ] diff --git a/tests/array/test_map.py b/tests/array/test_map.py index d5288f7..824500d 100644 --- a/tests/array/test_map.py +++ b/tests/array/test_map.py @@ -1,20 +1,29 @@ from __future__ import annotations + from typing import Callable, List -from python_js.array import map +from python_js.array_methods.map import map seasons: List[str] = ['Winter', 'Summer', 'Fall', 'Spring'] -numbers: List[int] = [1, 2, 3, 4] -seasond_and_numbers: List[int | str] = [*seasons, *numbers] +numbers: List[int] = [1, 2] +seasons_and_numbers: List[int | str] = [*seasons, *numbers] + + +def test_returned_value_instance() -> None: + formatted_seasons: List[str] = map(seasons, lambda x, i: x) + + # Then + assert isinstance(formatted_seasons, List) -def test_map(): - format_str: Callable[[str, int], +def test_input_argument_iterable() -> None: + format_str: Callable[[str | int, int], str] = lambda value, index: f"{value} ({index})" - formatted_seasons = map(seasons, format_str) + formatted_seasons_and_numbers: List[str] = map( + seasons_and_numbers, format_str) # Then - assert formatted_seasons == [ - 'Winter (0)', 'Summer (1)', 'Fall (2)', 'Spring (3)' + assert formatted_seasons_and_numbers == [ + 'Winter (0)', 'Summer (1)', 'Fall (2)', 'Spring (3)', '1 (4)', '2 (5)' ] diff --git a/tests/array/test_reduce.py b/tests/array/test_reduce.py new file mode 100644 index 0000000..a245f8f --- /dev/null +++ b/tests/array/test_reduce.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Callable, Dict, List + +from python_js.array_methods.reduce import reduce + +seasons: List[str] = ['Winter', 'Summer', 'Fall', 'Spring'] +numbers: List[int] = [1, 2] +seasons_and_numbers: List[int | str] = [*seasons, *numbers] + + +def test_returned_value_instance() -> None: + formatted_seasons: str = reduce(seasons, lambda x, y, i: y) + + # Then + assert isinstance(formatted_seasons, str) + + +def test_input_argument_iterable() -> None: + concat_str: Callable[[str, str | int, int], + str] = lambda prev, next, index: f"{prev}, {next}" + + formatted_seasons_and_numbers: str = reduce( + seasons_and_numbers, concat_str) + + # Then + assert formatted_seasons_and_numbers == 'None, Winter, Summer, Fall, Spring, 1, 2' + + +def test_initialValue() -> None: + accumulator: Dict[str, int] = {} + + def reduce_seasons(acc: Dict[str, int], season: str, index: int) -> Dict[str, int]: + acc[season] = index + return acc + + seasons_dict: Dict[str, int] = reduce( + seasons, reduce_seasons, initialValue=accumulator) + + # Then + assert seasons_dict == { + 'Winter': 0, + 'Summer': 1, + 'Fall': 2, + 'Spring': 3, + } + assert seasons_dict == accumulator + assert id(seasons_dict) == id(accumulator) diff --git a/tests/test_array.py b/tests/test_array.py new file mode 100644 index 0000000..4324dc6 --- /dev/null +++ b/tests/test_array.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import List + +from python_js.array import Array + +seasons: List[str] = ['Winter', 'Summer', 'Fall', 'Spring'] +numbers: List[int] = [1, 2] +seasons_and_numbers: List[int | str] = [*seasons, *numbers] + + +def test_constructor() -> None: + # Then + assert Array(seasons_and_numbers) == seasons_and_numbers + + +def test_concat() -> None: + array = Array(seasons) + result = array.concat(numbers) + + # Then + assert result == seasons_and_numbers + + +def test_concat_chainable() -> None: + array = Array(seasons) + result: List[str | int] = array.concat(numbers).concat(numbers) + + # Then + assert result == [*seasons_and_numbers, *numbers] + + +def test_filter() -> None: + array = Array(seasons_and_numbers) + result = array.filter(lambda x, i: isinstance(x, str)) + + # Then + assert result == ["Winter", "Summer", "Fall", "Spring"] + + +def test_filter_chainable() -> None: + array = Array(seasons_and_numbers) + result: List[str] = array.filter( + lambda x, i: isinstance(x, str) + ).filter( + lambda x, i: x.startswith('W') + ) + + # Then + assert result == ["Winter"] + + +def test_map() -> None: + array = Array(seasons_and_numbers) + result = array.map(lambda x, i: f"{x} ({i})") + + # Then + assert result == [ + 'Winter (0)', 'Summer (1)', 'Fall (2)', 'Spring (3)', '1 (4)', '2 (5)' + ] + + +def test_map_chainable() -> None: + array = Array(seasons_and_numbers) + result = array.map(lambda x, i: f"{x} ({i})").map( + lambda x, i: f"({i}) {x}") + + # Then + assert result == [ + '(0) Winter (0)', + '(1) Summer (1)', + '(2) Fall (2)', + '(3) Spring (3)', + '(4) 1 (4)', + '(5) 2 (5)' + ] + + +def test_reduce() -> None: + array = Array(seasons_and_numbers) + result = array.reduce(lambda prev, next, index: f"{prev}, {next}") + + # Then + assert result == 'None, Winter, Summer, Fall, Spring, 1, 2'