Skip to content

Commit

Permalink
chore: Update dependencies and setup for PyPi package publishing
Browse files Browse the repository at this point in the history
  • Loading branch information
Zozi96 committed Aug 16, 2024
1 parent 08f3aa3 commit a69d43f
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
Expand Down
136 changes: 100 additions & 36 deletions linq/linq.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import sys

from contextlib import suppress
from itertools import islice, groupby, takewhile, dropwhile, zip_longest
from typing import Generator, Iterable, Callable, Iterator, TypeVar, Generic, List, Optional, Tuple, Any, cast, Final

from more_itertools import first, interleave_longest, last, chunked, unique_everseen

PYTHON_VERSION: Final[Tuple[int, int]] = sys.version_info[:2]

T = TypeVar('T')
Expand Down Expand Up @@ -127,7 +128,7 @@ def to_list(self) -> List[T]:
"""
return list(self.iterable)

def first_or_default(self, default: Optional[T] = None) -> Optional[T]:
def first(self, default: Optional[T] = None) -> Optional[T]:
"""
Returns the first element of the iterable or the default value if the iterable is empty.
Expand All @@ -148,9 +149,9 @@ def first_or_default(self, default: Optional[T] = None) -> Optional[T]:
>>> print(result)
42
"""
return next(iter(self.iterable), default)
return first(iter(self.iterable), default=default)

def last_or_default(self, default: Optional[T] = None) -> Optional[T]:
def last(self, default: Optional[T] = None) -> Optional[T]:
"""
Returns the last element of the iterable or the default value if the iterable is empty.
Expand All @@ -171,9 +172,7 @@ def last_or_default(self, default: Optional[T] = None) -> Optional[T]:
>>> print(result)
42
"""
with suppress(StopIteration):
return next(reversed(list(self.iterable)), default)
return default
return last(iter(self.iterable), default=default)

def any(self, predicate: Callable[[T], bool] = lambda x: True) -> bool:
"""
Expand Down Expand Up @@ -376,43 +375,108 @@ def batch(self, size: int) -> 'Linq[Tuple[T, ...]]':
[(1, 2), (3, 4), (5, 6)]
"""

def batch_generator(iterable: Iterable[T], size: int) -> Generator[Tuple[T, ...], None, None]:
"""
Generates batches of elements from an iterable.
if PYTHON_VERSION < (3, 12):
from more_itertools import batched
else:
from itertools import batched

Args:
iterable (Iterable[T]): The iterable to generate batches from.
size (int): The size of each batch.
return Linq(batched(self.iterable, size))

Yields:
Generator[Tuple[T, ...], None, None]: A generator that yields batches of elements as tuples.
def chunk_into(self, size: int, strict: bool = False) -> 'Linq[List[T]]':
"""
Chunks the iterable into lists of the specified size.
Raises:
ValueError: If size is less than or equal to 0.
Args:
size (int): The size of each chunk.
strict (bool, optional): If True, raises an error if the iterable cannot be evenly divided into chunks of the specified size. Defaults to False.
Example:
>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> for batch in batch_generator(numbers, 3):
... print(batch)
(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)
"""
if size <= 0:
raise ValueError("Size must be greater than 0.")
it: Iterator[T] = iter(iterable)
while batch := tuple(islice(it, size)):
yield batch
Returns:
Linq[List[T]]: A new Linq object containing the chunked lists.
if PYTHON_VERSION < (3, 12):
batcher = batch_generator
"""
return Linq(chunked(self.iterable, size, strict))

def consecutive_pairs(self) -> 'Linq[Tuple[T, T]]':
"""
Returns an iterable of consecutive pairs of elements.
Returns:
Linq[Tuple[T, T]]: A new Linq object with consecutive pairs of elements.
Example:
>>> linq = Linq([1, 2, 3, 4])
>>> result = linq.consecutive_pairs().to_list()
>>> print(result)
[(1, 2), (2, 3), (3, 4)]
"""
if PYTHON_VERSION < (3, 10):
from more_itertools import pairwise
else:
from itertools import batched
from itertools import pairwise
return Linq(pairwise(self.iterable))

batcher = batched
def unique_seen(self, key: Optional[Callable[[T], Any]] = None) -> 'Linq[T]':
"""
Returns unique elements in the order they are first seen, based on a specified key function.
Args:
key (Optional[Callable[[T], Any]]): A function that takes an element as input and returns a value
to be compared for uniqueness. Defaults to None, meaning the elements themselves are compared.
return Linq(batcher(self.iterable, size))
Returns:
Linq[T]: A new Linq object with unique elements in the order they were first seen.
Examples:
# Example 1: Unique elements based on their length
>>> linq = Linq(['apple', 'banana', 'pear', 'apricot', 'peach'])
>>> result = linq.unique_seen(key=len).to_list()
>>> print(result)
['apple', 'banana', 'apricot']
# Example 2: Unique elements based on the first character
>>> linq = Linq(['apple', 'banana', 'avocado', 'blueberry', 'cherry'])
>>> result = linq.unique_seen(key=lambda x: x[0]).to_list()
>>> print(result)
['apple', 'banana', 'cherry']
# Example 3: Unique elements based on a dictionary attribute
>>> linq = Linq([
... {'name': 'apple', 'color': 'red'},
... {'name': 'banana', 'color': 'yellow'},
... {'name': 'cherry', 'color': 'red'},
... {'name': 'pear', 'color': 'green'}
... ])
>>> result = linq.unique_seen(key=lambda x: x['color']).to_list()
>>> print(result)
[{'name': 'apple', 'color': 'red'}, {'name': 'banana', 'color': 'yellow'}, {'name': 'pear', 'color': 'green'}]
# Example 4: Unique elements ignoring case sensitivity
>>> linq = Linq(['Apple', 'banana', 'apple', 'Banana', 'CHERRY'])
>>> result = linq.unique_seen(key=lambda x: x.lower()).to_list()
>>> print(result)
['Apple', 'banana', 'CHERRY']
"""
return Linq(unique_everseen(self.iterable, key=key))

def interleave_with(self, *others: Iterable) -> 'Linq[T]':
"""
Interleaves the elements of the iterable with the elements of other iterables, filling with None
if one iterable is shorter.
Args:
*others (Iterable[T]): Other iterables to interleave with.
Returns:
Linq[T]: A new Linq object with interleaved elements.
Example:
>>> linq = Linq([1, 2, 3])
>>> result = linq.interleave_with(['a', 'b'], ['x', 'y', 'z']).to_list()
>>> print(result)
[1, 'a', 'x', 2, 'b', 'y', 3, None, 'z']
"""
return Linq(interleave_longest(self.iterable, *others))

def __iter__(self) -> Iterator[T]:
"""
Expand Down
35 changes: 1 addition & 34 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,34 +1 @@
build==1.2.1
certifi==2024.7.4
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==43.0.0
docutils==0.21.2
idna==3.7
importlib_metadata==8.2.0
jaraco.classes==3.4.0
jaraco.context==5.3.0
jaraco.functools==4.0.2
jeepney==0.8.0
keyring==25.3.0
markdown-it-py==3.0.0
mdurl==0.1.2
more-itertools==10.3.0
nh3==0.2.18
packaging==24.1
pkginfo==1.10.0
pycparser==2.22
Pygments==2.18.0
pyproject_hooks==1.1.0
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.7.1
SecretStorage==3.3.3
setuptools==72.1.0
setuptools-scm==8.1.0
twine==5.1.1
urllib3==2.2.2
wheel==0.44.0
zipp==3.19.2
more-itertools==10.4.0
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
with open('README.md', 'r', encoding='utf-8') as fh:
long_description = fh.read()

DESCRIPTION: Final[str] = 'A LINQ-like library for Python inspired by C# LINQ using itertools internally.'
DESCRIPTION: Final[str] = (
'A LINQ-like library for Python inspired by C# LINQ using itertools and more-itertools internally.'
)

setup(
name='linq-tool',
Expand All @@ -22,5 +24,6 @@
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
],
python_requires='>=3.7',
python_requires='>=3.8',
install_requires=['more-itertools'],
)
28 changes: 24 additions & 4 deletions tests/test_linq.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ def test_skip(self) -> None:

def test_first_or_default(self) -> None:
linq = Linq([1, 2, 3])
result = linq.first_or_default()
result = linq.first()
self.assertEqual(result, 1)

empty_linq = Linq([])
result = empty_linq.first_or_default(42)
result = empty_linq.first(42)
self.assertEqual(result, 42)

def test_last_or_default(self) -> None:
linq = Linq([1, 2, 3])
result = linq.last_or_default()
result = linq.last()
self.assertEqual(result, 3)

empty_linq = Linq([])
result = empty_linq.last_or_default(42)
result = empty_linq.last(42)
self.assertEqual(result, 42)

def test_any(self) -> None:
Expand Down Expand Up @@ -108,6 +108,26 @@ def test_batch(self) -> None:
result = linq.batch(2).to_list()
self.assertEqual(result, [(1, 2), (3, 4), (5,)])

def test_chunk_into(self) -> None:
linq = Linq([1, 2, 3, 4, 5])
result = linq.chunk_into(2).to_list()
self.assertEqual(result, [[1, 2], [3, 4], [5]])

def test_consecutive_pairs(self) -> None:
linq = Linq([1, 2, 3, 4])
result = linq.consecutive_pairs().to_list()
self.assertEqual(result, [(1, 2), (2, 3), (3, 4)])

def test_unique_seen(self) -> None:
linq = Linq([1, 2, 2, 3, 3, 3])
result = linq.unique_seen().to_list()
self.assertEqual(result, [1, 2, 3])

def test_interleave_with(self) -> None:
linq = Linq([1, 2, 3])
result = linq.interleave_with(['a', 'b', 'c']).to_list()
self.assertEqual(result, [1, 'a', 2, 'b', 3, 'c'])


if __name__ == '__main__':
unittest.main()

0 comments on commit a69d43f

Please sign in to comment.