Skip to content

Commit

Permalink
add stream methods + unit tests + bump version to 0.2.0
Browse files Browse the repository at this point in the history
- stream
- stream_dict_records
  • Loading branch information
erivlis committed Aug 3, 2024
1 parent 3299424 commit c69c872
Show file tree
Hide file tree
Showing 6 changed files with 622 additions and 8 deletions.
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,88 @@ print(unwrapped_data)
# Output: [{'key': 'key1', 'value': [{'key': 'subkey', 'value': 'value'}]}, {'key': 'key2', 'value': ['item1', 'item2']}]
```

#### `stream`

Takes a mapping and an optional item factory function, and generates items from the mapping.
If the item factory is provided, it applies the factory to each key-value pair before yielding.

```python
from collections import namedtuple

from mappingtools import stream


def custom_factory(key, value):
return f"{key}: {value}"


my_mapping = {'a': 1, 'b': 2, 'c': 3}

for item in stream(my_mapping, custom_factory):
print(item)
# Output:
# a: 1
# b: 2
# c: 3


MyTuple = namedtuple('MyTuple', ['key', 'value'])
data = {'a': 1, 'b': 2}

for item in stream(data, MyTuple):
print(item)
# Output:
# MyTuple(key='a', value=1)
# MyTuple(key='b', value=2)


```

#### `stream_dict_records`

generates dictionary records from a given mapping, where each record contains a key-value pair from the mapping with
customizable key and value names.

```python
from mappingtools import stream_dict_records

mapping = {'a': 1, 'b': 2}
records = stream_dict_records(mapping, key_name='letter', value_name='number')
for record in records:
print(record)
# Output:
# {'letter': 'a', 'number': 1}
# {'letter': 'b', 'number': 2}
```

#### `stream_namedtuples`

generates named tuple instances from a given mapping and named tuple class.

```python
from collections import namedtuple

from mappingtools import stream

MyTuple = namedtuple('MyTuple', ['key', 'value'])
data = {'a': 1, 'b': 2}

for item in stream(data, MyTuple):
print(item)
# Output:
# MyTuple(key='a', value=1)
# MyTuple(key='b', value=2)

def record(k, v):
return {'key': k, 'value': v}

for item in stream(data, record):
print(item)
# output:
# {'key': 'a', 'value': 1}
# {'key': 'b', 'value': 2}
```

### Collectors

#### `nested_defaultdict`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "mappingtools"
version = "0.1.0"
version = "0.2.0"
authors = [
{ name = "Eran Rivlis", email = "[email protected]" },
]
Expand Down
53 changes: 46 additions & 7 deletions src/mappingtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections.abc import Callable, Generator, Iterable, Mapping
from enum import Enum, auto
from itertools import chain
from typing import Any, TypeVar
from typing import Any, NamedTuple, TypeVar

K = TypeVar('K')
KT = TypeVar('KT')
Expand Down Expand Up @@ -204,7 +204,8 @@ def remove(keys: Iterable[K], *mappings: Mapping[K, Any]) -> Generator[Mapping[K


def inverse(mapping: Mapping[Any, set]) -> Mapping[Any, set]:
"""Return a new dictionary with keys and values swapped from the input mapping.
"""
Return a new dictionary with keys and values swapped from the input mapping.
Args:
mapping (Mapping[Any, set]): The input mapping to invert.
Expand Down Expand Up @@ -342,10 +343,9 @@ def listify(obj: Any, key_name: str = 'key', value_name: str = 'value') -> Any:
Returns:
Any: The unwrapped object.
"""
return _process_obj(obj, _listify_mapping, _listify_iterable, _listify_class,
key_name=key_name, value_name=value_name)
return _process_obj(obj, _listify_mapping, _listify_iterable, _listify_class, key_name=key_name,
value_name=value_name)


def _listify_mapping(obj: Mapping, key_name, value_name) -> list[dict]:
Expand All @@ -361,7 +361,46 @@ def _listify_class(obj, key_name, value_name):
not k.startswith('_')]


def stream(mapping: Mapping, item_factory: Callable[[Any, Any], Any] | None = None) -> Generator[Any, Any, None]:
"""
Generate a stream of items from a mapping.
Args:
mapping (Mapping): The mapping object to stream items from.
item_factory (Callable[[Any, Any], Any], optional): A function that transforms each key-value pair from
the mapping. Defaults to None.
Yields:
The streamed items from the mapping.
"""

items = mapping.items() if item_factory is None else iter(item_factory(k, v) for k, v in mapping.items())
yield from items


def stream_dict_records(mapping: Mapping,
key_name: str = 'key',
value_name: str = 'value') -> Generator[Mapping[str, Any], Any, None]:
"""
Generate dictionary records from a mapping.
Args:
mapping (Mapping): The input mapping to generate records from.
key_name (str): The name to use for the key in the generated records. Defaults to 'key'.
value_name (str): The name to use for the value in the generated records. Defaults to 'value'.
Yields:
dictionary records based on the input mapping.
"""

def record(k, v):
return {key_name: k, value_name: v}

yield from stream(mapping, record)


__all__ = (
'distinct', 'keep', 'remove', 'inverse', 'nested_defaultdict', 'listify', 'simplify', 'strictify', 'Category',
'CategoryCounter', 'MappingCollector', 'MappingCollectorMode'
'distinct', 'keep', 'remove', 'inverse', 'nested_defaultdict', 'listify', 'simplify', 'stream',
'stream_dict_records', 'strictify', 'Category', 'CategoryCounter',
'MappingCollector', 'MappingCollectorMode'
)
176 changes: 176 additions & 0 deletions tests/test_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Generated by CodiumAI
import dataclasses

from mappingtools import stream


# stream function yields items from the mapping when item_factory is None
def test_yields_items_without_item_factory():
# Arrange
mapping = {'a': 1, 'b': 2}

# Act
result = list(stream(mapping))

# Assert
assert result == [('a', 1), ('b', 2)]


# stream function yields transformed items when item_factory is provided
def test_yields_transformed_items_with_item_factory():
# Arrange
mapping = {'a': 1, 'b': 2}

def item_factory(k, v):
return k, v * 2

# Act
result = list(stream(mapping, item_factory))

# Assert
assert result == [('a', 2), ('b', 4)]


# stream function works with different types of mappings (e.g., dict, defaultdict)
def test_works_with_different_mappings():
# Arrange
from collections import defaultdict
mapping = defaultdict(int, {'a': 1, 'b': 2})

# Act
result = list(stream(mapping))

# Assert
assert result == [('a', 1), ('b', 2)]


# stream function handles empty mappings correctly
def test_handles_empty_mappings():
# Arrange
mapping = {}

# Act
result = list(stream(mapping))

# Assert
assert result == []


# stream function works with various item_factory functions
def test_works_with_various_item_factories():
# Arrange
mapping = {'a': 1, 'b': 2}

def item_factory(k, v):
return k.upper(), v + 10

# Act
result = list(stream(mapping, item_factory))

# Assert
assert result == [('A', 11), ('B', 12)]


# stream function handles mappings with non-hashable keys
def test_handles_non_hashable_keys():
# Arrange
mapping = {('a',): 1, ('b',): 2}

# Act
result = list(stream(mapping))

# Assert
assert result == [(('a',), 1), (('b',), 2)]


# stream function handles mappings with None values
def test_handles_none_values():
# Arrange
mapping = {'a': None, 'b': 2}

# Act
result = list(stream(mapping))

# Assert
assert result == [('a', None), ('b', 2)]


# stream function handles mappings with mixed data types
def test_handles_mixed_data_types():
# Arrange
mapping = {'a': 1, 'b': 'two', 'c': [3]}

# Act
result = list(stream(mapping))

# Assert
assert result == [('a', 1), ('b', 'two'), ('c', [3])]


# stream function handles large mappings efficiently
def test_handles_large_mappings_efficiently():
# Arrange
mapping = {i: i for i in range(1000000)}

# Act & Assert
for i, item in enumerate(stream(mapping)):
assert item == (i, i)
if i >= 10:
break # Only check the first few items for efficiency


# stream function handles mappings with special characters in keys or values
def test_handles_special_characters_in_keys_or_values():
# Arrange
mapping = {'a!@#': 'value$%^', 'b&*(': 'value)'}

# Act
result = list(stream(mapping))

# Assert
assert result == [('a!@#', 'value$%^'), ('b&*(', 'value)')]


# stream function handles mappings with nested structures
def test_handles_nested_structures():
# Arrange
mapping = {'a': {'nested': 1}, 'b': [2, 3]}

# Act
result = list(stream(mapping))

# Assert
assert result == [('a', {'nested': 1}), ('b', [2, 3])]


# stream function handles mappings with cyclic references
def test_handles_cyclic_references():
# Arrange
a = {}
b = {'a': a}
a['b'] = b

mapping = {'a': a, 'b': b}

# Act & Assert (checking for no infinite loop)
result = list(stream(mapping))

assert len(result) == 2
assert ('a', a) in result
assert ('b', b) in result


def test_handles_dataclass_factory():
mapping = {'a': 1, 'b': 2}

@dataclasses.dataclass
class CustomDC:
key: str
value: int

result = list(stream(mapping, CustomDC))

assert result[0].key == 'a'
assert result[0].value == 1
assert result[1].key == 'b'
assert result[1].value == 2
Loading

0 comments on commit c69c872

Please sign in to comment.