From a1000dfe9e6e31e75e2447dca3246789a6ac6e2f Mon Sep 17 00:00:00 2001 From: kaliv0 Date: Mon, 26 Jan 2026 22:41:15 +0200 Subject: [PATCH 1/3] added file_options, refactored tests --- .github/workflows/ci.yml | 4 +- pyrio/__init__.py | 29 +- pyrio/decorators/__init__.py | 3 +- pyrio/exceptions/__init__.py | 10 +- pyrio/streams/file_stream.py | 35 +- pyrio/utils/__init__.py | 13 + pyrio/utils/file_options.py | 258 +++++ tests/test_dict_item.py | 180 ++-- tests/test_file_options.py | 299 ++++++ tests/test_file_stream.py | 1231 +++++++++++++---------- tests/test_itertools_mixin.py | 854 ++++++++-------- tests/test_optional.py | 155 ++- tests/test_stream.py | 1752 +++++++++++++++------------------ 13 files changed, 2735 insertions(+), 2088 deletions(-) create mode 100644 pyrio/utils/file_options.py create mode 100644 tests/test_file_options.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85b4b11..11430ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,8 @@ jobs: run: uv sync --all-extras --dev - name: Run lint run: | - uv run ruff check - uv run ruff format + uv run ruff check --no-fix + uv run ruff format --check - name: Run tests run: uv run pytest -v --cov=./pyrio --cov-fail-under=90 --cov-report=xml - name: Upload coverage to Codecov diff --git a/pyrio/__init__.py b/pyrio/__init__.py index dc0c1d7..1a81a7a 100644 --- a/pyrio/__init__.py +++ b/pyrio/__init__.py @@ -2,5 +2,32 @@ from .streams.file_stream import FileStream as FileStream from .utils.optional import Optional as Optional from .utils.dict_item import DictItem as DictItem +from .utils.file_options import ( + FileOptions as FileOptions, + CsvReadOptions as CsvReadOptions, + CsvWriteOptions as CsvWriteOptions, + JsonReadOptions as JsonReadOptions, + JsonWriteOptions as JsonWriteOptions, + YamlReadOptions as YamlReadOptions, + YamlWriteOptions as YamlWriteOptions, + XmlReadOptions as XmlReadOptions, + XmlWriteOptions as XmlWriteOptions, + PlainTextWriteOptions as PlainTextWriteOptions, +) -__all__ = ["Stream", "FileStream", "Optional", "DictItem"] +__all__ = [ + "Stream", + "FileStream", + "Optional", + "DictItem", + "FileOptions", + "CsvReadOptions", + "CsvWriteOptions", + "JsonReadOptions", + "JsonWriteOptions", + "YamlReadOptions", + "YamlWriteOptions", + "XmlReadOptions", + "XmlWriteOptions", + "PlainTextWriteOptions", +] diff --git a/pyrio/decorators/__init__.py b/pyrio/decorators/__init__.py index dd6bb96..5981274 100644 --- a/pyrio/decorators/__init__.py +++ b/pyrio/decorators/__init__.py @@ -1,3 +1,2 @@ -from .handler import pre_call as pre_call -from .handler import handle_consumed as handle_consumed +from .handler import pre_call as pre_call, handle_consumed as handle_consumed from .mapper import map_dict_items as map_dict_items diff --git a/pyrio/exceptions/__init__.py b/pyrio/exceptions/__init__.py index f2b5607..cb70a53 100644 --- a/pyrio/exceptions/__init__.py +++ b/pyrio/exceptions/__init__.py @@ -1,4 +1,6 @@ -from .exception import IllegalStateError as IllegalStateError -from .exception import NoneTypeError as NoneTypeError -from .exception import NoSuchElementError as NoSuchElementError -from .exception import UnsupportedTypeError as UnsupportedTypeError +from .exception import ( + IllegalStateError as IllegalStateError, + NoneTypeError as NoneTypeError, + NoSuchElementError as NoSuchElementError, + UnsupportedTypeError as UnsupportedTypeError, +) diff --git a/pyrio/streams/file_stream.py b/pyrio/streams/file_stream.py index 3e9122a..777163b 100644 --- a/pyrio/streams/file_stream.py +++ b/pyrio/streams/file_stream.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from pathlib import Path -from pyrio.utils import DictItem +from pyrio.utils import DictItem, Mappable from pyrio.streams import BaseStream, Stream from pyrio.exceptions import NoneTypeError @@ -98,14 +98,12 @@ def process(cls, file_path, *, f_open_options=None, f_read_options=None, **kwarg def _read_file(cls, file_path, f_open_options=None, f_read_options=None, **kwargs): path = cls._get_file_path(file_path) - if f_open_options is None: - f_open_options = {} - if f_read_options is None: - f_read_options = {} + f_open_options = cls._normalize_options(f_open_options) + f_read_options = cls._normalize_options(f_read_options) if (suffix := path.suffix) in DSV_TYPES: return cls._read_dsv(path, f_open_options, f_read_options) - elif suffix in MAPPING_READ_CONFIG.keys(): + elif suffix in MAPPING_READ_CONFIG: return cls._read_mapping(path, f_open_options, f_read_options, **kwargs) else: return cls._read_plain(path, f_open_options) @@ -156,14 +154,12 @@ def save( """Writes Stream to a new file (or updates an existing one) with advanced 'writing' options passed by the user""" path, tmp_path = self._prepare_file_paths(file_path) - if f_open_options is None: - f_open_options = {} - if f_write_options is None: - f_write_options = {} + f_open_options = self._normalize_options(f_open_options) + f_write_options = self._normalize_options(f_write_options) if (suffix := path.suffix) in DSV_TYPES: return self._write_dsv(path, tmp_path, f_open_options, f_write_options, null_handler) - elif suffix in MAPPING_READ_CONFIG.keys(): + elif suffix in MAPPING_READ_CONFIG: return self._write_mapping( path, tmp_path, f_open_options, f_write_options, null_handler, **kwargs ) @@ -184,7 +180,7 @@ def _write_dsv(self, path, tmp_path, f_open_options, f_write_options, null_handl (f_write_options, "fieldnames", output[0].keys() if output else ()), ] ) - with self._atomic_write(path, tmp_path, f_open_options) as f: + with self._atomic_write(path, tmp_path, f_open_options) as f: # noqa writer = csv.DictWriter(f, **f_write_options) writer.writeheader() writer.writerows(output) @@ -202,11 +198,11 @@ def _write_mapping( if path.suffix == ".xml": root = kwargs.get("xml_root", "root") output = {root: output} - io_opts_setting.append((f_write_options, "pretty", True)) + io_opts_setting.append((f_write_options, "pretty", True)) # noqa self._prepare_io_options(io_opts_setting) dump = getattr(importlib.import_module(config["import_mod"]), config["callable"]) - with self._atomic_write(path, tmp_path, f_open_options) as f: + with self._atomic_write(path, tmp_path, f_open_options) as f: # noqa dump(output, f, **f_write_options) def _write_plain(self, path, tmp_path, f_open_options, f_write_options): @@ -218,10 +214,19 @@ def _write_plain(self, path, tmp_path, f_open_options, f_write_options): if header or footer: output = f"{header}{output}{footer}" - with self._atomic_write(path, tmp_path, f_open_options) as f: + with self._atomic_write(path, tmp_path, f_open_options) as f: # noqa f.writelines(output) # ### helpers ### + @staticmethod + def _normalize_options(options): + """Converts option objects to dicts if needed, returns empty dict for None.""" + if options is None: + return {} + if isinstance(options, Mappable): + return options.to_dict() + return options + @staticmethod def _get_file_path(file_path, read_mode=True): path = Path(file_path) diff --git a/pyrio/utils/__init__.py b/pyrio/utils/__init__.py index 142a2dd..c39ec12 100644 --- a/pyrio/utils/__init__.py +++ b/pyrio/utils/__init__.py @@ -1,2 +1,15 @@ from .dict_item import DictItem as DictItem from .optional import Optional as Optional +from .file_options import ( + Mappable as Mappable, + FileOptions as FileOptions, + CsvReadOptions as CsvReadOptions, + CsvWriteOptions as CsvWriteOptions, + JsonReadOptions as JsonReadOptions, + JsonWriteOptions as JsonWriteOptions, + YamlReadOptions as YamlReadOptions, + YamlWriteOptions as YamlWriteOptions, + XmlReadOptions as XmlReadOptions, + XmlWriteOptions as XmlWriteOptions, + PlainTextWriteOptions as PlainTextWriteOptions, +) diff --git a/pyrio/utils/file_options.py b/pyrio/utils/file_options.py new file mode 100644 index 0000000..4876da7 --- /dev/null +++ b/pyrio/utils/file_options.py @@ -0,0 +1,258 @@ +from abc import ABC + + +class Mappable(ABC): + def to_dict(self): + """Returns dict with only non-None values""" + return {attr: val for attr, val in vars(self).items() if val is not None} + + def __repr__(self): + return f"{self.__class__.__name__}({', '.join(f'{attr}={val!r}' for attr, val in vars(self).items() if val is not None)})" + + +class FileOptions(Mappable): + """Options for file opening - applies to all file formats""" + + def __init__(self, encoding=None, errors=None, newline=None, buffering=None, mode=None): + self.encoding = encoding + self.errors = errors + self.newline = newline + self.buffering = buffering + self.mode = mode + + @staticmethod + def utf8(errors=None): + """Creates FileOptions with UTF-8 encoding""" + return FileOptions(encoding="utf-8", errors=errors) + + @staticmethod + def ascii(errors=None): + """Creates FileOptions with ASCII encoding""" + return FileOptions(encoding="ascii", errors=errors) + + @staticmethod + def append(encoding=None): + """Creates FileOptions for append mode""" + return FileOptions(mode="a", encoding=encoding) + + +# CSV +class CsvReadOptions(Mappable): + """Options for reading CSV files""" + + def __init__( + self, + delimiter=None, + quotechar=None, + escapechar=None, + doublequote=None, + skipinitialspace=None, + strict=None, + dialect=None, + fieldnames=None, + restkey=None, + restval=None, + ): + self.delimiter = delimiter + self.quotechar = quotechar + self.escapechar = escapechar + self.doublequote = doublequote + self.skipinitialspace = skipinitialspace + self.strict = strict + self.dialect = dialect + self.fieldnames = fieldnames + self.restkey = restkey + self.restval = restval + + @staticmethod + def excel(): + """Creates CsvReadOptions for Excel CSV dialect""" + return CsvReadOptions(dialect="excel") + + @staticmethod + def unix(): + """Creates CsvReadOptions for Unix CSV dialect""" + return CsvReadOptions(dialect="unix") + + +class CsvWriteOptions(Mappable): + """Options for writing CSV files""" + + def __init__( + self, + delimiter=None, + quotechar=None, + escapechar=None, + doublequote=None, + skipinitialspace=None, + strict=None, + lineterminator=None, + quoting=None, + dialect=None, + fieldnames=None, + restval=None, + extrasaction=None, + ): + self.delimiter = delimiter + self.quotechar = quotechar + self.escapechar = escapechar + self.doublequote = doublequote + self.skipinitialspace = skipinitialspace + self.strict = strict + self.lineterminator = lineterminator + self.quoting = quoting + self.dialect = dialect + self.fieldnames = fieldnames + self.restval = restval + self.extrasaction = extrasaction + + +class JsonReadOptions(Mappable): + """Options for reading JSON files""" + + def __init__( + self, + parse_float=None, + parse_int=None, + object_hook=None, + object_pairs_hook=None, + cls=None, + ): + self.parse_float = parse_float + self.parse_int = parse_int + self.object_hook = object_hook + self.object_pairs_hook = object_pairs_hook + self.cls = cls + + @staticmethod + def with_decimal(): + """Creates JsonReadOptions that parses floats as Decimal""" + from decimal import Decimal + + return JsonReadOptions(parse_float=Decimal) + + +class JsonWriteOptions(Mappable): + """Options for writing JSON files""" + + def __init__( + self, + indent=None, + separators=None, + sort_keys=None, + default=None, + ensure_ascii=None, + allow_nan=None, + skipkeys=None, + cls=None, + ): + self.indent = indent + self.separators = separators + self.sort_keys = sort_keys + self.default = default + self.ensure_ascii = ensure_ascii + self.allow_nan = allow_nan + self.skipkeys = skipkeys + self.cls = cls + + @staticmethod + def pretty(indent=2): + """Creates JsonWriteOptions for pretty-printed output""" + return JsonWriteOptions(indent=indent) + + @staticmethod + def compact(): + """Creates JsonWriteOptions for compact output with minimal whitespace""" + return JsonWriteOptions(separators=(",", ":")) + + @staticmethod + def sorted(indent=2): + """Creates JsonWriteOptions with sorted keys and pretty-printing""" + return JsonWriteOptions(indent=indent, sort_keys=True) + + +class YamlReadOptions(Mappable): + """Options for reading YAML files""" + + def __init__(self, loader=None): # noqa + self.loader = loader + + +class YamlWriteOptions(Mappable): + """Options for writing YAML files""" + + def __init__( + self, + default_flow_style=None, + allow_unicode=None, + indent=None, + width=None, + ): + self.default_flow_style = default_flow_style + self.allow_unicode = allow_unicode + self.indent = indent + self.width = width + + @staticmethod + def block_style(indent=2): + """Creates YamlWriteOptions for block-style output""" + return YamlWriteOptions(default_flow_style=False, indent=indent) + + @staticmethod + def flow_style(): + """Creates YamlWriteOptions for flow-style (inline) output""" + return YamlWriteOptions(default_flow_style=True) + + +class XmlReadOptions(Mappable): + """Options for reading XML files""" + + def __init__( + self, + process_namespaces=None, + namespaces=None, + attr_prefix=None, + cdata_key=None, + ): + self.process_namespaces = process_namespaces + self.namespaces = namespaces + self.attr_prefix = attr_prefix + self.cdata_key = cdata_key + + +class XmlWriteOptions(Mappable): + """Options for writing XML files""" + + def __init__( + self, + pretty=None, + indent=None, + short_empty_elements=None, + ): + self.pretty = pretty + self.indent = indent + self.short_empty_elements = short_empty_elements + + @staticmethod + def pretty_print(indent=4): + """Creates XmlWriteOptions for pretty-printed output""" + return XmlWriteOptions(pretty=True, indent=indent) + + +class PlainTextWriteOptions(Mappable): + """Options for writing plain text files""" + + def __init__( + self, + delimiter=None, + header=None, + footer=None, + ): + self.delimiter = delimiter + self.header = header + self.footer = footer + + @staticmethod + def with_header_footer(header="", footer="", delimiter="\n"): + """Creates PlainTextWriteOptions with header and footer""" + return PlainTextWriteOptions(delimiter=delimiter, header=header, footer=footer) diff --git a/tests/test_dict_item.py b/tests/test_dict_item.py index 6d59cd7..4089998 100644 --- a/tests/test_dict_item.py +++ b/tests/test_dict_item.py @@ -5,100 +5,94 @@ from pyrio import DictItem -def test_dict_item_map(json_dict): - dictitem = DictItem(key="data", value=json_dict) - assert dictitem.key == "data" - assert dictitem.value == ( - DictItem(key="Name", value="Jennifer Smith"), - DictItem(key="Security_Number", value=7867567898), - DictItem(key="Phone", value="555-123-4568"), - DictItem(key="Email", value=(DictItem(key="primary", value="jen123@gmail.com"),)), - DictItem(key="Hobbies", value=["Reading", "Sketching", "Horse Riding"]), - DictItem(key="Job", value=None), - ) - - -def test_dict_item_map_nested_dict(nested_json): - dictitem = DictItem(key="data", value=json.loads(nested_json)) - assert dictitem.value == ( - DictItem( - key="user", - value=( - DictItem(key="Name", value="John"), - DictItem(key="Phone", value="555-123-4568"), - DictItem(key="Security Number", value="3450678"), +class TestDictItem: + def test_dict_item_map(self, json_dict): + dictitem = DictItem(key="data", value=json_dict) + assert dictitem.key == "data" + assert dictitem.value == ( + DictItem(key="Name", value="Jennifer Smith"), + DictItem(key="Security_Number", value=7867567898), + DictItem(key="Phone", value="555-123-4568"), + DictItem(key="Email", value=(DictItem(key="primary", value="jen123@gmail.com"),)), + DictItem(key="Hobbies", value=["Reading", "Sketching", "Horse Riding"]), + DictItem(key="Job", value=None), + ) + + def test_dict_item_map_nested_dict(self, nested_json): + dictitem = DictItem(key="data", value=json.loads(nested_json)) + assert dictitem.value == ( + DictItem( + key="user", + value=( + DictItem(key="Name", value="John"), + DictItem(key="Phone", value="555-123-4568"), + DictItem(key="Security Number", value="3450678"), + ), ), - ), - DictItem( - key="super_user", - value=( - DictItem(key="Name", value="sudo"), - DictItem(key="Email", value="admin@sudo.su"), - DictItem(key="Some Other Number", value="000-0011"), + DictItem( + key="super_user", + value=( + DictItem(key="Name", value="sudo"), + DictItem(key="Email", value="admin@sudo.su"), + DictItem(key="Some Other Number", value="000-0011"), + ), ), - ), - DictItem( - key="fraud", - value=( - DictItem(key="Name", value="Freud"), - DictItem(key="Email", value="ziggy@psycho.au"), + DictItem( + key="fraud", + value=( + DictItem(key="Name", value="Freud"), + DictItem(key="Email", value="ziggy@psycho.au"), + ), ), - ), - ) - - -def test_dict_item_repr(json_dict): - assert str(DictItem(key="data", value=json_dict)) == ( - "DictItem(key='data', value=(" - "DictItem(key='Name', value='Jennifer Smith'), " - "DictItem(key='Security_Number', value=7867567898), " - "DictItem(key='Phone', value='555-123-4568'), " - "DictItem(key='Email', value=(DictItem(key='primary', value='jen123@gmail.com'),)), " - "DictItem(key='Hobbies', value=['Reading', 'Sketching', 'Horse Riding']), " - "DictItem(key='Job', value=None)))" - ) - - -def test_dict_item_eq(json_dict, nested_json): - assert DictItem(key="data", value=json.loads(nested_json)) == DictItem( - key="data", value=json.loads(nested_json) - ) - assert DictItem(key="foo", value=json.loads(nested_json)) != DictItem( - key="data", value=json.loads(nested_json) - ) - assert DictItem(key="data", value=json_dict) != DictItem( - key="data", value=json.loads(nested_json) - ) - - -def test_dict_item_eq_raises(json_dict): - nums = [1, 2, 3] - with pytest.raises(TypeError) as e: - DictItem(key="data", value=json_dict) == nums # noqa - assert str(e.value) == f"{nums} is not a DictItem" - - -@pytest.mark.parametrize("value", ["John", 42, (1, 2, 3), None]) -def test_dict_item_hash_returns_int(value): - assert isinstance(hash(DictItem(key="k", value=value)), int) - - -@pytest.mark.parametrize("value", ["John", 42, (1, 2, 3)]) -def test_dict_item_hash_consistency(value): - item = DictItem(key="k", value=value) - other = DictItem(key="k", value=value) - assert item == other - assert hash(item) == hash(other) - - -@pytest.mark.parametrize( - "value,expected_type", - [([1, 2, 3], "list"), ({"a": 1}, "dict"), ({"list": [1, 2], "dict": {"a": 1}}, "dict")], -) -def test_dict_item_hash_unhashable_value_raises(value, expected_type): - with pytest.raises(TypeError) as e: - hash(DictItem(key="k", value=value)) - assert ( - str(e.value) - == f"unhashable type: 'DictItem' (value of type '{expected_type}' is unhashable)" + ) + + def test_dict_item_repr(self, json_dict): + assert str(DictItem(key="data", value=json_dict)) == ( + "DictItem(key='data', value=(" + "DictItem(key='Name', value='Jennifer Smith'), " + "DictItem(key='Security_Number', value=7867567898), " + "DictItem(key='Phone', value='555-123-4568'), " + "DictItem(key='Email', value=(DictItem(key='primary', value='jen123@gmail.com'),)), " + "DictItem(key='Hobbies', value=['Reading', 'Sketching', 'Horse Riding']), " + "DictItem(key='Job', value=None)))" + ) + + def test_dict_item_eq(self, json_dict, nested_json): + assert DictItem(key="data", value=json.loads(nested_json)) == DictItem( + key="data", value=json.loads(nested_json) + ) + assert DictItem(key="foo", value=json.loads(nested_json)) != DictItem( + key="data", value=json.loads(nested_json) + ) + assert DictItem(key="data", value=json_dict) != DictItem( + key="data", value=json.loads(nested_json) + ) + + def test_dict_item_eq_raises(self, json_dict): + nums = [1, 2, 3] + with pytest.raises(TypeError) as e: + DictItem(key="data", value=json_dict) == nums # noqa + assert str(e.value) == f"{nums} is not a DictItem" + + @pytest.mark.parametrize("value", ["John", 42, (1, 2, 3), None]) + def test_dict_item_hash_returns_int(self, value): + assert isinstance(hash(DictItem(key="k", value=value)), int) + + @pytest.mark.parametrize("value", ["John", 42, (1, 2, 3)]) + def test_dict_item_hash_consistency(self, value): + item = DictItem(key="k", value=value) + other = DictItem(key="k", value=value) + assert item == other + assert hash(item) == hash(other) + + @pytest.mark.parametrize( + "value,expected_type", + [([1, 2, 3], "list"), ({"a": 1}, "dict"), ({"list": [1, 2], "dict": {"a": 1}}, "dict")], ) + def test_dict_item_hash_unhashable_value_raises(self, value, expected_type): + with pytest.raises(TypeError) as e: + hash(DictItem(key="k", value=value)) + assert ( + str(e.value) + == f"unhashable type: 'DictItem' (value of type '{expected_type}' is unhashable)" + ) diff --git a/tests/test_file_options.py b/tests/test_file_options.py new file mode 100644 index 0000000..8716679 --- /dev/null +++ b/tests/test_file_options.py @@ -0,0 +1,299 @@ +from decimal import Decimal + +import yaml + +from pyrio import ( + FileStream, + FileOptions, + CsvReadOptions, + CsvWriteOptions, + JsonReadOptions, + JsonWriteOptions, + YamlReadOptions, + YamlWriteOptions, + XmlReadOptions, + XmlWriteOptions, + PlainTextWriteOptions, +) + + +class TestFileOptions: + def test_init_with_all_params(self): + opts = FileOptions(encoding="utf-8", errors="strict", newline="\n", buffering=1, mode="w") + assert opts.encoding == "utf-8" + assert opts.errors == "strict" + assert opts.newline == "\n" + assert opts.buffering == 1 + assert opts.mode == "w" + + def test_to_dict_all_values(self): + opts = FileOptions(encoding="utf-8", errors="ignore", newline="", buffering=0, mode="w") + assert opts.to_dict() == { + "encoding": "utf-8", + "errors": "ignore", + "newline": "", + "buffering": 0, + "mode": "w", + } + + def test_to_dict_partial_values(self): + assert FileOptions(encoding="latin-1").to_dict() == {"encoding": "latin-1"} + + def test_to_dict_empty(self): + assert FileOptions().to_dict() == {} + + def test_utf8_factory(self): + opts = FileOptions.utf8() + assert opts.encoding == "utf-8" + assert opts.to_dict() == {"encoding": "utf-8"} + + def test_utf8_factory_with_errors(self): + opts = FileOptions.utf8(errors="replace") + assert opts.encoding == "utf-8" + assert opts.errors == "replace" + assert opts.to_dict() == {"encoding": "utf-8", "errors": "replace"} + + def test_ascii_factory(self): + opts = FileOptions.ascii() + assert opts.encoding == "ascii" + assert opts.to_dict() == {"encoding": "ascii"} + + def test_append_factory(self): + opts = FileOptions.append() + assert opts.mode == "a" + assert opts.to_dict() == {"mode": "a"} + + def test_append_factory_with_encoding(self): + opts = FileOptions.append(encoding="utf-8") + assert opts.mode == "a" + assert opts.encoding == "utf-8" + assert opts.to_dict() == {"encoding": "utf-8", "mode": "a"} + + def test_repr(self): + assert repr(FileOptions(encoding="utf-8")) == "FileOptions(encoding='utf-8')" + + +class TestCsvReadOptions: + def test_init_with_params(self): + opts = CsvReadOptions(delimiter=";", quotechar="'", strict=True) + assert opts.delimiter == ";" + assert opts.quotechar == "'" + assert opts.strict is True + + def test_to_dict(self): + assert CsvReadOptions(delimiter="|", skipinitialspace=True).to_dict() == { + "delimiter": "|", + "skipinitialspace": True, + } + + def test_excel_factory(self): + opts = CsvReadOptions.excel() + assert opts.dialect == "excel" + assert opts.to_dict() == {"dialect": "excel"} + + def test_unix_factory(self): + opts = CsvReadOptions.unix() + assert opts.dialect == "unix" + assert opts.to_dict() == {"dialect": "unix"} + + def test_repr(self): + assert repr(CsvReadOptions(delimiter=";")) == "CsvReadOptions(delimiter=';')" + + +class TestCsvWriteOptions: + def test_init_with_params(self): + opts = CsvWriteOptions(delimiter="\t", lineterminator="\r\n") + assert opts.delimiter == "\t" + assert opts.lineterminator == "\r\n" + + def test_to_dict(self): + assert CsvWriteOptions(delimiter=";", extrasaction="ignore").to_dict() == { + "delimiter": ";", + "extrasaction": "ignore", + } + + def test_repr(self): + assert repr(CsvWriteOptions(delimiter="|")) == "CsvWriteOptions(delimiter='|')" + + +class TestJsonReadOptions: + def test_init_with_params(self): + assert JsonReadOptions(parse_float=Decimal).parse_float == Decimal + + def test_to_dict(self): + assert JsonReadOptions(parse_float=Decimal, parse_int=str).to_dict() == { + "parse_float": Decimal, + "parse_int": str, + } + + def test_with_decimal_factory(self): + opts = JsonReadOptions.with_decimal() + assert opts.parse_float == Decimal + assert opts.to_dict() == {"parse_float": Decimal} + + def test_repr(self): + assert ( + repr(JsonReadOptions(parse_float=float)) + == "JsonReadOptions(parse_float=)" + ) + + +class TestJsonWriteOptions: + def test_init_with_params(self): + opts = JsonWriteOptions(indent=4, sort_keys=True) + assert opts.indent == 4 + assert opts.sort_keys is True + + def test_to_dict(self): + assert JsonWriteOptions(indent=2, ensure_ascii=False).to_dict() == { + "indent": 2, + "ensure_ascii": False, + } + + def test_pretty_factory(self): + opts = JsonWriteOptions.pretty() + assert opts.indent == 2 + assert opts.to_dict() == {"indent": 2} + + def test_pretty_factory_custom_indent(self): + opts = JsonWriteOptions.pretty(indent=4) + assert opts.indent == 4 + + def test_compact_factory(self): + opts = JsonWriteOptions.compact() + assert opts.separators == (",", ":") + assert opts.to_dict() == {"separators": (",", ":")} + + def test_sorted_factory(self): + opts = JsonWriteOptions.sorted() + assert opts.indent == 2 + assert opts.sort_keys is True + assert opts.to_dict() == {"indent": 2, "sort_keys": True} + + def test_repr(self): + assert repr(JsonWriteOptions(indent=2)) == "JsonWriteOptions(indent=2)" + + +class TestYamlReadOptions: + def test_init_with_params(self): + opts = YamlReadOptions(loader=yaml.SafeLoader) + assert opts.loader == yaml.SafeLoader + + def test_to_dict(self): + assert YamlReadOptions(loader=yaml.FullLoader).to_dict() == {"loader": yaml.FullLoader} + + def test_to_dict_empty(self): + assert YamlReadOptions().to_dict() == {} + + def test_repr(self): + assert ( + repr(YamlReadOptions(loader=yaml.FullLoader)) + == "YamlReadOptions(loader=)" + ) + + +class TestYamlWriteOptions: + def test_init_with_params(self): + opts = YamlWriteOptions(default_flow_style=False, indent=4) + assert opts.default_flow_style is False + assert opts.indent == 4 + + def test_to_dict(self): + assert YamlWriteOptions(allow_unicode=True, width=80).to_dict() == { + "allow_unicode": True, + "width": 80, + } + + def test_block_style_factory(self): + opts = YamlWriteOptions.block_style() + assert opts.default_flow_style is False + assert opts.indent == 2 + + def test_block_style_factory_custom_indent(self): + opts = YamlWriteOptions.block_style(indent=4) + assert opts.indent == 4 + + def test_flow_style_factory(self): + opts = YamlWriteOptions.flow_style() + assert opts.default_flow_style is True + + def test_repr(self): + assert repr(YamlWriteOptions(indent=2)) == "YamlWriteOptions(indent=2)" + + +class TestXmlReadOptions: + def test_init_with_params(self): + opts = XmlReadOptions(attr_prefix="@", cdata_key="#text") + assert opts.attr_prefix == "@" + assert opts.cdata_key == "#text" + + def test_to_dict(self): + assert XmlReadOptions(process_namespaces=True).to_dict() == {"process_namespaces": True} + + def test_repr(self): + assert ( + repr(XmlReadOptions(attr_prefix="@", cdata_key="#text", process_namespaces=True)) + == "XmlReadOptions(process_namespaces=True, attr_prefix='@', cdata_key='#text')" + ) + + +class TestXmlWriteOptions: + def test_init_with_params(self): + opts = XmlWriteOptions(pretty=True, indent=4) + assert opts.pretty is True + assert opts.indent == 4 + + def test_to_dict(self): + assert XmlWriteOptions(short_empty_elements=True).to_dict() == { + "short_empty_elements": True + } + + def test_pretty_print_factory(self): + opts = XmlWriteOptions.pretty_print() + assert opts.pretty is True + assert opts.indent == 4 + + def test_pretty_print_factory_custom_indent(self): + opts = XmlWriteOptions.pretty_print(indent=2) + assert opts.indent == 2 + + def test_repr(self): + assert repr(XmlWriteOptions(pretty=True)) == "XmlWriteOptions(pretty=True)" + + +class TestPlainTextWriteOptions: + def test_init_with_params(self): + opts = PlainTextWriteOptions(delimiter=", ", header="START", footer="END") + assert opts.delimiter == ", " + assert opts.header == "START" + assert opts.footer == "END" + + def test_to_dict(self): + assert PlainTextWriteOptions(header="# Header\n").to_dict() == {"header": "# Header\n"} + + def test_with_header_footer_factory(self): + opts = PlainTextWriteOptions.with_header_footer(header="BEGIN\n", footer="\nEND") + assert opts.header == "BEGIN\n" + assert opts.footer == "\nEND" + assert opts.delimiter == "\n" + + def test_repr(self): + assert repr(PlainTextWriteOptions(delimiter="|")) == "PlainTextWriteOptions(delimiter='|')" + + +class TestNormalizeOptions: + def test_normalize_none(self): + result = FileStream._normalize_options(None) + assert result == {} + + def test_normalize_dict(self): + input_dict = {"key": "value"} + result = FileStream._normalize_options(input_dict) + assert result == {"key": "value"} + assert result is input_dict # Should return same object + + def test_normalize_options_object(self): + opts = FileOptions(encoding="utf-8") + result = FileStream._normalize_options(opts) + assert result == {"encoding": "utf-8"} + assert isinstance(result, dict) diff --git a/tests/test_file_stream.py b/tests/test_file_stream.py index 6feeb37..3f441a5 100644 --- a/tests/test_file_stream.py +++ b/tests/test_file_stream.py @@ -4,576 +4,807 @@ import pytest -from pyrio import FileStream, Stream, DictItem -from pyrio.exceptions import IllegalStateError, NoneTypeError - - -def test_none_path_error(): - with pytest.raises(NoneTypeError) as e: - FileStream(None) - assert str(e.value) == "File path cannot be None" - - -def test_invalid_path_error(): - file_path = "./foo/bar.xyz" - with pytest.raises(FileNotFoundError) as e: - FileStream(file_path) - assert str(e.value) == f"No such file or directory: '{file_path}'" - - -def test_path_is_dir_error(): - file_path = "./tests/resources/" - with pytest.raises(IsADirectoryError) as e: - FileStream(file_path) - assert str(e.value) == f"Given path '{file_path}' is a directory" - - -@pytest.mark.parametrize( - "file_path", - [ - "./tests/resources/foo.json", - "./tests/resources/foo.toml", - "./tests/resources/foo.yaml", - "./tests/resources/foo.xml", - ], +from pyrio import ( + FileStream, + Stream, + DictItem, + FileOptions, + CsvReadOptions, + CsvWriteOptions, + JsonReadOptions, + JsonWriteOptions, + YamlWriteOptions, + XmlWriteOptions, + PlainTextWriteOptions, ) -def test_read_files(file_path): - assert FileStream(file_path).map(lambda x: f"{x.key}=>{x.value}").to_tuple() == ( - "abc=>xyz", - "qwerty=>42", - ) - - -def test_read_xml_custom_root(): - assert FileStream("./tests/resources/custom_root.xml").map( - lambda x: f"{x.key}=>{x.value}" - ).to_tuple() == ( - "abc=>xyz", - "qwerty=>42", - ) - - -def test_read_xml_include_root(): - assert FileStream.process("./tests/resources/custom_root.xml", include_root=True).map( - lambda x: f"root={x.key}: inner_records={str(Stream(x.value).to_dict())}" - ).to_list() == ["root=my-root: inner_records={'abc': 'xyz', 'qwerty': '42'}"] +from pyrio.exceptions import IllegalStateError, NoneTypeError -@pytest.mark.parametrize( - "file_path", - [ - "./tests/resources/bar.csv", - "./tests/resources/bar.tsv", - ], -) -def test_dsv(file_path): - assert FileStream(file_path).map( - lambda x: f"fizz: {x['fizz']}, buzz: {x['buzz']}" - ).to_tuple() == ( - "fizz: 42, buzz: 45", - "fizz: aaa, buzz: bbb", +class TestFileStream: + def test_none_path_error(self): + with pytest.raises(NoneTypeError) as e: + FileStream(None) + assert str(e.value) == "File path cannot be None" + + def test_invalid_path_error(self): + file_path = "./foo/bar.xyz" + with pytest.raises(FileNotFoundError) as e: + FileStream(file_path) + assert str(e.value) == f"No such file or directory: '{file_path}'" + + def test_path_is_dir_error(self): + file_path = "./tests/resources/" + with pytest.raises(IsADirectoryError) as e: + FileStream(file_path) + assert str(e.value) == f"Given path '{file_path}' is a directory" + + @pytest.mark.parametrize( + "file_path", + [ + "./tests/resources/foo.json", + "./tests/resources/foo.toml", + "./tests/resources/foo.yaml", + "./tests/resources/foo.xml", + ], ) + def test_read_files(self, file_path): + assert FileStream(file_path).map(lambda x: f"{x.key}=>{x.value}").to_tuple() == ( + "abc=>xyz", + "qwerty=>42", + ) + def test_read_xml_custom_root(self): + assert FileStream("./tests/resources/custom_root.xml").map( + lambda x: f"{x.key}=>{x.value}" + ).to_tuple() == ( + "abc=>xyz", + "qwerty=>42", + ) -def test_read_plain_text(): - lorem = FileStream("./tests/resources/plain.txt") - assert lorem.map(lambda x: x.strip()).to_string("||") == ( - "Lorem ipsum dolor sit amet, consectetur adipisicing elit," - "||sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - "||Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris" - "||nisi ut aliquip ex ea commodo consequat." - "||Duis aute irure dolor in reprehenderit in voluptate velit esse" - "||cillum dolore eu fugiat nulla pariatur." - "||Excepteur sint occaecat cupidatat non proident, sunt in culpa" - "||qui officia deserunt mollit anim id est laborum." + def test_read_xml_include_root(self): + assert FileStream.process("./tests/resources/custom_root.xml", include_root=True).map( + lambda x: f"root={x.key}: inner_records={str(Stream(x.value).to_dict())}" + ).to_list() == ["root=my-root: inner_records={'abc': 'xyz', 'qwerty': '42'}"] + + @pytest.mark.parametrize( + "file_path", + [ + "./tests/resources/bar.csv", + "./tests/resources/bar.tsv", + ], ) + def test_dsv(self, file_path): + assert FileStream(file_path).map( + lambda x: f"fizz: {x['fizz']}, buzz: {x['buzz']}" + ).to_tuple() == ( + "fizz: 42, buzz: 45", + "fizz: aaa, buzz: bbb", + ) + def test_read_plain_text(self): + lorem = FileStream("./tests/resources/plain.txt") + assert lorem.map(lambda x: x.strip()).to_string("||") == ( + "Lorem ipsum dolor sit amet, consectetur adipisicing elit," + "||sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + "||Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris" + "||nisi ut aliquip ex ea commodo consequat." + "||Duis aute irure dolor in reprehenderit in voluptate velit esse" + "||cillum dolore eu fugiat nulla pariatur." + "||Excepteur sint occaecat cupidatat non proident, sunt in culpa" + "||qui officia deserunt mollit anim id est laborum." + ) -def test_read_plain_and_query(): - assert FileStream("./tests/resources/plain.txt").map(lambda x: x.strip()).enumerate().filter( - lambda line: "id" in line[1] - ).to_dict() == { - 1: "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - 6: "Excepteur sint occaecat cupidatat non proident, sunt in culpa", - 7: "qui officia deserunt mollit anim id est laborum.", - } - - -def test_nested_json(): - assert FileStream("./tests/resources/nested.json").map(lambda x: x.value).flat_map( - lambda x: Stream(x) - .filter(lambda y: y.key == "second") - .flat_map(lambda z: z.value) - .to_tuple() - ).to_list() == [ - 1, - 2, - 3, - 4, - ] - - -def test_nested_dict(): - assert ( - FileStream("./tests/resources/nested_dicts.json") - .filter(lambda outer: "user" not in outer.key) - .map( - lambda outer: ( - Stream(outer.value) - .filter(lambda inner: inner.key == "Email") - .map( - lambda inner: ( - Stream(inner.value) - .filter(lambda deepest: deepest.key == "primary") - .map(lambda deepest: deepest.value) - .to_tuple() + def test_read_plain_and_query(self): + assert FileStream("./tests/resources/plain.txt").map( + lambda x: x.strip() + ).enumerate().filter(lambda line: "id" in line[1]).to_dict() == { + 1: "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + 6: "Excepteur sint occaecat cupidatat non proident, sunt in culpa", + 7: "qui officia deserunt mollit anim id est laborum.", + } + + def test_nested_json(self): + assert FileStream("./tests/resources/nested.json").map(lambda x: x.value).flat_map( + lambda x: Stream(x) + .filter(lambda y: y.key == "second") + .flat_map(lambda z: z.value) + .to_tuple() + ).to_list() == [ + 1, + 2, + 3, + 4, + ] + + def test_nested_dict(self): + assert ( + FileStream("./tests/resources/nested_dicts.json") + .filter(lambda outer: "user" not in outer.key) + .map( + lambda outer: ( + Stream(outer.value) + .filter(lambda inner: inner.key == "Email") + .map( + lambda inner: ( + Stream(inner.value) + .filter(lambda deepest: deepest.key == "primary") + .map(lambda deepest: deepest.value) + .to_tuple() + ) ) + .to_tuple() ) - .to_tuple() ) + .flatten() + .to_list() + ) == ["johnny@bravo.cash", "ziggy@psycho.au"] + + def test_complex_pipeline(self): + assert ( + FileStream("./tests/resources/long.json") + .filter(lambda x: "a" in x.key) + .map(lambda x: DictItem(x.key, sum(x.value) * 10)) + .sort(attrgetter("value"), reverse=True) + .map(lambda x: f"{str(x.value)}::{x.key}") + ).to_list() == ["230::xza", "110::abba", "30::a"] + + def test_reusing_stream(self): + stream = FileStream("./tests/resources/foo.json") + assert stream._is_consumed is False + + result = stream.map(lambda x: f"{x.key}=>{x.value}").tail(1).to_tuple() + assert result == ("qwerty=>42",) + assert stream._is_consumed + assert stream._file_handler.closed + + with pytest.raises(IllegalStateError) as e: + stream.map(lambda x: x.value * 10).to_list() + assert str(e.value) == "Stream object already consumed" + + def test_concat(self): + assert ( + FileStream("./tests/resources/long.json") + .concat(FileStream("./tests/resources/foo.json")) + .map(lambda x: f"{x.key}: {x.value}") + ).to_tuple() == ( + "a: [1, 2]", + "b: [2, 3, 4]", + "abba: [5, 6]", + "x: []", + "y: [55]", + "xza: [11, 12]", + "z: [3]", + "zzz: None", + "abc: xyz", + "qwerty: 42", ) - .flatten() - .to_list() - ) == ["johnny@bravo.cash", "ziggy@psycho.au"] - - -def test_complex_pipeline(): - assert ( - FileStream("./tests/resources/long.json") - .filter(lambda x: "a" in x.key) - .map(lambda x: DictItem(x.key, sum(x.value) * 10)) - .sort(attrgetter("value"), reverse=True) - .map(lambda x: f"{str(x.value)}::{x.key}") - ).to_list() == ["230::xza", "110::abba", "30::a"] - - -def test_reusing_stream(): - stream = FileStream("./tests/resources/foo.json") - assert stream._is_consumed is False - - result = stream.map(lambda x: f"{x.key}=>{x.value}").tail(1).to_tuple() - assert result == ("qwerty=>42",) - assert stream._is_consumed - assert stream._file_handler.closed - - with pytest.raises(IllegalStateError) as e: - stream.map(lambda x: x.value * 10).to_list() - assert str(e.value) == "Stream object already consumed" - - -def test_concat(): - assert ( - FileStream("./tests/resources/long.json") - .concat(FileStream("./tests/resources/foo.json")) - .map(lambda x: f"{x.key}: {x.value}") - ).to_tuple() == ( - "a: [1, 2]", - "b: [2, 3, 4]", - "abba: [5, 6]", - "x: []", - "y: [55]", - "xza: [11, 12]", - "z: [3]", - "zzz: None", - "abc: xyz", - "qwerty: 42", - ) - - -def test_prepend(json_dict): - in_memory_dict = Stream(json_dict).to_tuple() - assert ( - FileStream("./tests/resources/long.json") - .prepend(in_memory_dict) - .map(lambda x: f"key={x.key}, value={x.value}") - ).to_tuple() == ( - "key=Name, value=Jennifer Smith", - "key=Security_Number, value=7867567898", - "key=Phone, value=555-123-4568", - "key=Email, value=(DictItem(key='primary', value='jen123@gmail.com'),)", - "key=Hobbies, value=['Reading', 'Sketching', 'Horse Riding']", - "key=Job, value=None", - "key=a, value=[1, 2]", - "key=b, value=[2, 3, 4]", - "key=abba, value=[5, 6]", - "key=x, value=[]", - "key=y, value=[55]", - "key=xza, value=[11, 12]", - "key=z, value=[3]", - "key=zzz, value=None", - ) - - -@pytest.mark.parametrize( - "file_path", - ["./tests/resources/parse_float.toml", "./tests/resources/parse_float.json"], -) -def test_process(file_path): - def check_type(x): - match x.key: - case "a": - return isinstance(x.value, str) - case "b": - return isinstance(x.value, bool) - case "x" | "y": - return isinstance(x.value, Decimal) - case _: - return False - - assert FileStream.process(file_path, f_read_options={"parse_float": Decimal}).all_match( - check_type - ) - - -# ### save to file ### -def test_save_toml(tmp_file_dir, json_dict): - in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() - tmp_file_path = tmp_file_dir / "test.toml" - FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( - tmp_file_path, - null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, - ) - assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.toml").read() - - -def test_save_toml_default_null_handler(tmp_file_dir, json_dict): - in_memory_dict = Stream(json_dict).to_tuple() - tmp_file_path = tmp_file_dir / "test_default_null_handler.toml" - FileStream("./tests/resources/foo.toml").concat(in_memory_dict).save(tmp_file_path) - assert ( - tmp_file_path.read_text() - == open("./tests/resources/save_output/test_default_null_handler.toml").read() - ) + def test_prepend(self, json_dict): + in_memory_dict = Stream(json_dict).to_tuple() + assert ( + FileStream("./tests/resources/long.json") + .prepend(in_memory_dict) + .map(lambda x: f"key={x.key}, value={x.value}") + ).to_tuple() == ( + "key=Name, value=Jennifer Smith", + "key=Security_Number, value=7867567898", + "key=Phone, value=555-123-4568", + "key=Email, value=(DictItem(key='primary', value='jen123@gmail.com'),)", + "key=Hobbies, value=['Reading', 'Sketching', 'Horse Riding']", + "key=Job, value=None", + "key=a, value=[1, 2]", + "key=b, value=[2, 3, 4]", + "key=abba, value=[5, 6]", + "key=x, value=[]", + "key=y, value=[55]", + "key=xza, value=[11, 12]", + "key=z, value=[3]", + "key=zzz, value=None", + ) -@pytest.mark.parametrize( - "file_path, indent", - [("test.json", 2), ("test.yaml", 2), ("test.xml", 4)], -) -def test_save(tmp_file_dir, file_path, indent, json_dict): - in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() - tmp_file_path = tmp_file_dir / file_path - FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( - tmp_file_path, - f_open_options={"encoding": "utf-8"}, - f_write_options={"indent": indent}, + @pytest.mark.parametrize( + "file_path", + ["./tests/resources/parse_float.toml", "./tests/resources/parse_float.json"], ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + def test_process(self, file_path): + def check_type(x): + match x.key: + case "a": + return isinstance(x.value, str) + case "b": + return isinstance(x.value, bool) + case "x" | "y": + return isinstance(x.value, Decimal) + case _: + return False + + assert FileStream.process(file_path, f_read_options={"parse_float": Decimal}).all_match( + check_type + ) + # ### save to file ### + def test_save_toml(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "test.toml" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.toml").read() + + def test_save_toml_default_null_handler(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).to_tuple() + tmp_file_path = tmp_file_dir / "test_default_null_handler.toml" + FileStream("./tests/resources/foo.toml").concat(in_memory_dict).save(tmp_file_path) + assert ( + tmp_file_path.read_text() + == open("./tests/resources/save_output/test_default_null_handler.toml").read() + ) -@pytest.mark.parametrize( - "file_path, indent", - [("test_null_handler.json", 2), ("test_null_handler.yaml", 2), ("test_null_handler.xml", 4)], -) -def test_save_handle_null(tmp_file_dir, file_path, indent, json_dict): - in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() - tmp_file_path = tmp_file_dir / file_path - FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( - tmp_file_path, - f_open_options={"encoding": "utf-8"}, - f_write_options={"indent": indent}, - null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + @pytest.mark.parametrize( + "file_path, indent", + [("test.json", 2), ("test.yaml", 2), ("test.xml", 4)], ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_save_custom_xml_root(tmp_file_dir, json_dict): - file_path = "custom_root.xml" - tmp_file_path = tmp_file_dir / file_path - indent = 4 + def test_save(self, tmp_file_dir, file_path, indent, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / file_path + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options={"encoding": "utf-8"}, + f_write_options={"indent": indent}, + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) - in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() - FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( - tmp_file_path, - f_write_options={"indent": indent}, - null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, - xml_root="my-root", - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_save_plain(tmp_file_dir): - file_path = "lorem.txt" - tmp_file_path = tmp_file_dir / "lorem.txt" - fs = FileStream("./tests/resources/plain.txt") - ( - fs.map(lambda line: line.strip()) - .enumerate() - .filter(lambda line: "x" in line[1]) - .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") - .save(tmp_file_path) - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - assert fs._file_handler.closed - - -def test_save_raises(): - with pytest.raises(UnicodeDecodeError) as e: - FileStream("./tests/resources/awake.mp3").save("./tests/resources/woke.json") - assert str(e.value) == "'utf-8' codec can't decode byte 0xff in position 45: invalid start byte" - - -def test_update_plain(tmp_file_dir, json_dict): - file_path = "lorem.txt" - tmp_file_path = tmp_file_dir / file_path - shutil.copyfile("./tests/resources/plain.txt", tmp_file_path) - ( - FileStream(tmp_file_path) - .map(lambda line: line.strip()) - .enumerate() - .filter(lambda line: "x" in line[1]) - .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") - .save() + @pytest.mark.parametrize( + "file_path, indent", + [ + ("test_null_handler.json", 2), + ("test_null_handler.yaml", 2), + ("test_null_handler.xml", 4), + ], ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_update_file(tmp_file_dir, json_dict): - tmp_file_path = tmp_file_dir / "updated.json" - shutil.copyfile("./tests/resources/long.json", tmp_file_path) - ( - FileStream(tmp_file_path) - .map( - lambda x: DictItem(x.key, ", ".join((str(y) for y in x.value)) if x.value else x.value) - ) - .save( - f_write_options={"indent": 2}, + def test_save_handle_null(self, tmp_file_dir, file_path, indent, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / file_path + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options={"encoding": "utf-8"}, + f_write_options={"indent": indent}, null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, ) - ) - assert tmp_file_path.read_text() == open("./tests/resources/save_output/updated.json").read() - - -def test_filter_update_file(tmp_file_dir, json_dict): - file_path = "filtered.toml" - tmp_file_path = tmp_file_dir / file_path - shutil.copyfile("./tests/resources/test.toml", tmp_file_path) - ( - FileStream(tmp_file_path) - .filter(lambda x: isinstance(x.value, str)) - .reverse(comparator=lambda x: x.key) - .save() - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) + def test_save_custom_xml_root(self, tmp_file_dir, json_dict): + file_path = "custom_root.xml" + tmp_file_path = tmp_file_dir / file_path + indent = 4 -@pytest.mark.parametrize( - "file_path", - ["test.csv", "test.tsv"], -) -def test_save_csv(tmp_file_dir, file_path): - tmp_file_path = tmp_file_dir / file_path - FileStream("./tests/resources/bar.csv").save(tmp_file_path) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_save_convert_to_csv(tmp_file_dir): - tmp_file_path = tmp_file_dir / "converted.csv" - ( - FileStream("./tests/resources/convertable.json") - .filter( - lambda x: ( - Stream(x.value) - .find_first(lambda y: y.key == "name" and y.value == "Snake") - .or_else_get(lambda: None) - ) - is None + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_write_options={"indent": indent}, + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + xml_root="my-root", + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() ) - .map(lambda x: x.value) - .save(tmp_file_path) - ) - assert tmp_file_path.read_text() == open("./tests/resources/save_output/converted.csv").read() + def test_save_plain(self, tmp_file_dir): + file_path = "lorem.txt" + tmp_file_path = tmp_file_dir / "lorem.txt" + fs = FileStream("./tests/resources/plain.txt") + ( + fs.map(lambda line: line.strip()) + .enumerate() + .filter(lambda line: "x" in line[1]) + .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") + .save(tmp_file_path) + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) + assert fs._file_handler.closed + + def test_save_raises(self): + with pytest.raises(UnicodeDecodeError) as e: + FileStream("./tests/resources/awake.mp3").save("./tests/resources/woke.json") + assert ( + str(e.value) + == "'utf-8' codec can't decode byte 0xff in position 45: invalid start byte" + ) -def test_save_to_csv_with_null_handler(tmp_file_dir): - def _null_handler(dict_obj): - return Stream(dict_obj).to_dict(lambda x: DictItem(x.key, x.value or "N/A")) + def test_update_plain(self, tmp_file_dir, json_dict): + file_path = "lorem.txt" + tmp_file_path = tmp_file_dir / file_path + shutil.copyfile("./tests/resources/plain.txt", tmp_file_path) + ( + FileStream(tmp_file_path) + .map(lambda line: line.strip()) + .enumerate() + .filter(lambda line: "x" in line[1]) + .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") + .save() + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) - tmp_file_path = tmp_file_dir / "converted_null.csv" - ( - FileStream("./tests/resources/convertable.json") - .filter( - lambda x: ( - Stream(x.value) - .find_first(lambda y: y.key == "name" and y.value == "Snake") - .or_else_get(lambda: None) + def test_update_file(self, tmp_file_dir, json_dict): + tmp_file_path = tmp_file_dir / "updated.json" + shutil.copyfile("./tests/resources/long.json", tmp_file_path) + ( + FileStream(tmp_file_path) + .map( + lambda x: DictItem( + x.key, ", ".join((str(y) for y in x.value)) if x.value else x.value + ) + ) + .save( + f_write_options={"indent": 2}, + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, ) ) - .map(lambda x: x.value) - .save(tmp_file_path, null_handler=_null_handler) - ) - assert ( - tmp_file_path.read_text() == open("./tests/resources/save_output/converted_null.csv").read() - ) - - -def test_save_empty_csv(tmp_file_dir): - tmp_file_path = tmp_file_dir / "dead.csv" - shutil.copyfile("./tests/resources/bar.csv", tmp_file_path) - stream = FileStream(tmp_file_path) - stream._iterable = tuple() - stream.save() - assert tmp_file_path.read_text() == open("./tests/resources/save_output/empty.csv").read() + assert ( + tmp_file_path.read_text() == open("./tests/resources/save_output/updated.json").read() + ) + def test_filter_update_file(self, tmp_file_dir, json_dict): + file_path = "filtered.toml" + tmp_file_path = tmp_file_dir / file_path + shutil.copyfile("./tests/resources/test.toml", tmp_file_path) + ( + FileStream(tmp_file_path) + .filter(lambda x: isinstance(x.value, str)) + .reverse(comparator=lambda x: x.key) + .save() + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) -def test_update_csv(tmp_file_dir): - tmp_file_path = tmp_file_dir / "updated.csv" - shutil.copyfile("./tests/resources/editable.csv", tmp_file_path) - ( - FileStream(tmp_file_path) - .map(lambda x: (Stream(x).to_dict(lambda y: DictItem(y.key, y.value or "Unknown")))) - .save(tmp_file_path) + @pytest.mark.parametrize( + "file_path", + ["test.csv", "test.tsv"], ) - assert tmp_file_path.read_text() == open("./tests/resources/save_output/updated.csv").read() - - -def test_update_fails(tmp_file_dir): - def _raise(exception): - raise exception + def test_save_csv(self, tmp_file_dir, file_path): + tmp_file_path = tmp_file_dir / file_path + FileStream("./tests/resources/bar.csv").save(tmp_file_path) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) - tmp_file_path = tmp_file_dir / "fail.csv" - shutil.copyfile("./tests/resources/editable.csv", tmp_file_path) - with pytest.raises(IOError, match="Ooops Mr White..."): - FileStream(tmp_file_path).save( - tmp_file_path, null_handler=_raise(IOError("Ooops Mr White...")) + def test_save_convert_to_csv(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "converted.csv" + ( + FileStream("./tests/resources/convertable.json") + .filter( + lambda x: ( + Stream(x.value) + .find_first(lambda y: y.key == "name" and y.value == "Snake") + .or_else_get(lambda: None) + ) + is None + ) + .map(lambda x: x.value) + .save(tmp_file_path) + ) + assert ( + tmp_file_path.read_text() == open("./tests/resources/save_output/converted.csv").read() ) - assert tmp_file_path.read_text() == open("./tests/resources/editable.csv").read() + def test_save_to_csv_with_null_handler(self, tmp_file_dir): + def _null_handler(dict_obj): + return Stream(dict_obj).to_dict(lambda x: DictItem(x.key, x.value or "N/A")) -def test_combine_files_into_csv(tmp_file_dir): - tmp_file_path = tmp_file_dir / "merged.csv" - shutil.copyfile("./tests/resources/combine.csv", tmp_file_path) - ( - FileStream(tmp_file_path) - .concat( + tmp_file_path = tmp_file_dir / "converted_null.csv" + ( FileStream("./tests/resources/convertable.json") .filter( lambda x: ( Stream(x.value) - .find_first(lambda y: y.key == "name" and y.value != "Snake") + .find_first(lambda y: y.key == "name" and y.value == "Snake") .or_else_get(lambda: None) ) - is not None # explicit is better than implicit ) .map(lambda x: x.value) + .save(tmp_file_path, null_handler=_null_handler) + ) + assert ( + tmp_file_path.read_text() + == open("./tests/resources/save_output/converted_null.csv").read() ) - .map(lambda x: (Stream(x).to_dict(lambda y: DictItem(y.key, y.value or "N/A")))) - .save(tmp_file_path) - ) - assert tmp_file_path.read_text() == open("./tests/resources/save_output/merged.csv").read() + def test_save_empty_csv(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "dead.csv" + shutil.copyfile("./tests/resources/bar.csv", tmp_file_path) + stream = FileStream(tmp_file_path) + stream._iterable = tuple() + stream.save() + assert tmp_file_path.read_text() == open("./tests/resources/save_output/empty.csv").read() + + def test_update_csv(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "updated.csv" + shutil.copyfile("./tests/resources/editable.csv", tmp_file_path) + ( + FileStream(tmp_file_path) + .map(lambda x: (Stream(x).to_dict(lambda y: DictItem(y.key, y.value or "Unknown")))) + .save(tmp_file_path) + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/updated.csv").read() -def test_save_mapping_to_plain(tmp_file_dir, json_dict): - in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() - file_path = "dict_2_plain.txt" - tmp_file_path = tmp_file_dir / file_path - FileStream("./tests/resources/nested.json").prepend(in_memory_dict).map( - lambda x: f"{x._key}: {x._value}" - ).save( - tmp_file_path, - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_append_to_plain(tmp_file_dir, json_dict): - file_path = "append_map.txt" - tmp_file_path = tmp_file_dir / file_path - shutil.copyfile("./tests/resources/plain_dict.txt", tmp_file_path) - ( - FileStream(tmp_file_path) - .map(lambda line: line.strip()) - .enumerate() - .filter(lambda line: "ne" in line[1]) - .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") - .save(f_open_options={"mode": "a"}) - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() - - -def test_plain_text_header_footer(tmp_file_dir): - file_path = "foo.txt" - tmp_file_path = tmp_file_dir / file_path - shutil.copyfile("./tests/resources/plain.txt", tmp_file_path) - ( - FileStream(tmp_file_path) - .map(lambda line: line.strip()) - .enumerate() - .filter(lambda line: line[0] == 3) - .map(lambda line: f"{line[0]}: {line[1]}") - .save( - f_open_options={"mode": "a"}, - f_write_options={"header": "\nHeader\n", "footer": "\nFooter\n"}, + def test_update_fails(self, tmp_file_dir): + def _raise(exception): + raise exception + + tmp_file_path = tmp_file_dir / "fail.csv" + shutil.copyfile("./tests/resources/editable.csv", tmp_file_path) + with pytest.raises(IOError, match="Ooops Mr White..."): + FileStream(tmp_file_path).save( + tmp_file_path, null_handler=_raise(IOError("Ooops Mr White...")) + ) + assert tmp_file_path.read_text() == open("./tests/resources/editable.csv").read() + + def test_combine_files_into_csv(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "merged.csv" + shutil.copyfile("./tests/resources/combine.csv", tmp_file_path) + ( + FileStream(tmp_file_path) + .concat( + FileStream("./tests/resources/convertable.json") + .filter( + lambda x: ( + Stream(x.value) + .find_first(lambda y: y.key == "name" and y.value != "Snake") + .or_else_get(lambda: None) + ) + is not None # explicit is better than implicit + ) + .map(lambda x: x.value) + ) + .map(lambda x: (Stream(x).to_dict(lambda y: DictItem(y.key, y.value or "N/A")))) + .save(tmp_file_path) + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/merged.csv").read() + + def test_save_mapping_to_plain(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + file_path = "dict_2_plain.txt" + tmp_file_path = tmp_file_dir / file_path + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).map( + lambda x: f"{x._key}: {x._value}" + ).save( + tmp_file_path, + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) + + def test_append_to_plain(self, tmp_file_dir, json_dict): + file_path = "append_map.txt" + tmp_file_path = tmp_file_dir / file_path + shutil.copyfile("./tests/resources/plain_dict.txt", tmp_file_path) + ( + FileStream(tmp_file_path) + .map(lambda line: line.strip()) + .enumerate() + .filter(lambda line: "ne" in line[1]) + .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") + .save(f_open_options={"mode": "a"}) + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() ) - ) - assert tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + def test_plain_text_header_footer(self, tmp_file_dir): + file_path = "foo.txt" + tmp_file_path = tmp_file_dir / file_path + shutil.copyfile("./tests/resources/plain.txt", tmp_file_path) + ( + FileStream(tmp_file_path) + .map(lambda line: line.strip()) + .enumerate() + .filter(lambda line: line[0] == 3) + .map(lambda line: f"{line[0]}: {line[1]}") + .save( + f_open_options={"mode": "a"}, + f_write_options={"header": "\nHeader\n", "footer": "\nFooter\n"}, + ) + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) -def test_file_handler_closed_on_exception(monkeypatch): - from pyrio.streams import BaseStream - import builtins + def test_file_handler_closed_on_exception(self, monkeypatch): + from pyrio.streams import BaseStream + import builtins - # Track if close was called - close_called = [] - original_init = BaseStream.__init__ - original_open = builtins.open + # Track if close was called + close_called = [] + original_init = BaseStream.__init__ + original_open = builtins.open - def mock_init(self, iterable): - # set up _iterable - original_init(self, iterable) - raise RuntimeError("Simulated initialization error") + def mock_init(self, iterable): + # set up _iterable + original_init(self, iterable) + raise RuntimeError("Simulated initialization error") - def tracking_open(*args, **kwargs): - f = original_open(*args, **kwargs) - original_close = f.close + def tracking_open(*args, **kwargs): + f = original_open(*args, **kwargs) + original_close = f.close - def tracked_close(): - close_called.append(True) - return original_close() + def tracked_close(): + close_called.append(True) + return original_close() - f.close = tracked_close - return f + f.close = tracked_close + return f - monkeypatch.setattr(BaseStream, "__init__", mock_init) - monkeypatch.setattr(builtins, "open", tracking_open) + monkeypatch.setattr(BaseStream, "__init__", mock_init) + monkeypatch.setattr(builtins, "open", tracking_open) - with pytest.raises(RuntimeError, match="Simulated initialization error"): - FileStream("./tests/resources/foo.json") + with pytest.raises(RuntimeError, match="Simulated initialization error"): + FileStream("./tests/resources/foo.json") - assert len(close_called) > 0, "File handler was not closed after exception" + assert len(close_called) > 0, "File handler was not closed after exception" + def test_save_with_cleaning_up_tmp_file(self, tmp_file_dir): + from pathlib import Path -def test_save_with_cleaning_up_tmp_file(tmp_file_dir): - from pathlib import Path + source_path = tmp_file_dir / "stale_tmp_source.json" + shutil.copyfile("./tests/resources/foo.json", source_path) - source_path = tmp_file_dir / "stale_tmp_source.json" - shutil.copyfile("./tests/resources/foo.json", source_path) + tmp_path = Path(f"{source_path}.tmp") + tmp_path.write_text("stale tmp content") + assert tmp_path.exists() - tmp_path = Path(f"{source_path}.tmp") - tmp_path.write_text("stale tmp content") - assert tmp_path.exists() + FileStream(source_path).save() + assert not tmp_path.exists() - FileStream(source_path).save() - assert not tmp_path.exists() + def test_atomic_write_cleanup_on_serialization_error(self, tmp_file_dir): + from pathlib import Path + class NotSerializable: + pass -def test_atomic_write_cleanup_on_serialization_error(tmp_file_dir): - from pathlib import Path + source_path = tmp_file_dir / "serialize_fail.json" + shutil.copyfile("./tests/resources/foo.json", source_path) - class NotSerializable: - pass + fs = FileStream(source_path) + fs._iterable = (DictItem("key", NotSerializable()),) - source_path = tmp_file_dir / "serialize_fail.json" - shutil.copyfile("./tests/resources/foo.json", source_path) + with pytest.raises(TypeError): + fs.save() - fs = FileStream(source_path) - fs._iterable = (DictItem("key", NotSerializable()),) + # Tmp file should be cleaned up + tmp_path = Path(f"{source_path}.tmp") + assert not tmp_path.exists() - with pytest.raises(TypeError): - fs.save() + # Source file should be unchanged + assert source_path.read_text() == open("./tests/resources/foo.json").read() - # Tmp file should be cleaned up - tmp_path = Path(f"{source_path}.tmp") - assert not tmp_path.exists() + # ### file_options ### + # JSON options + def test_read_json_with_options(self): + result = FileStream.process( + "./tests/resources/parse_float.json", f_read_options=JsonReadOptions.with_decimal() + ).to_tuple() + assert any(isinstance(item.value, Decimal) for item in result if hasattr(item, "value")) - # Source file should be unchanged - assert source_path.read_text() == open("./tests/resources/foo.json").read() + def test_save_json_with_json_write_options(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "test_opts.json" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options=FileOptions.utf8(), + f_write_options=JsonWriteOptions.pretty(indent=2), + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.json").read() + + def test_save_json_with_sorted_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "sorted.json" + FileStream("./tests/resources/foo.json").save( + tmp_file_path, + f_write_options=JsonWriteOptions.sorted(indent=2), + ) + content = tmp_file_path.read_text() + # With sort_keys=True, "abc" should come before "qwerty" + assert content.index("abc") < content.index("qwerty") + + def test_save_json_with_compact_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "compact.json" + FileStream("./tests/resources/foo.json").save( + tmp_file_path, + f_write_options=JsonWriteOptions.compact(), + ) + content = tmp_file_path.read_text() + # Compact format has no spaces after colons/commas + assert '": "' not in content + assert '":"' in content + + def test_update_json_with_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "updated_opts.json" + shutil.copyfile("./tests/resources/long.json", tmp_file_path) + ( + FileStream(tmp_file_path) + .map( + lambda x: DictItem( + x.key, ", ".join((str(y) for y in x.value)) if x.value else x.value + ) + ) + .save( + f_write_options=JsonWriteOptions(indent=2), + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + ) + ) + assert ( + tmp_file_path.read_text() == open("./tests/resources/save_output/updated.json").read() + ) + + # CSV options + def test_read_csv_with_options(self): + result = FileStream.process( + "./tests/resources/bar.csv", + f_read_options=CsvReadOptions(delimiter=","), + ).to_tuple() + assert len(result) == 2 + assert result[0]["fizz"] == "42" + + def test_read_csv_with_excel_dialect(self): + result = FileStream.process( + "./tests/resources/bar.csv", + f_read_options=CsvReadOptions.excel(), + ).to_tuple() + assert len(result) == 2 + + def test_save_csv_with_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "test_opts.csv" + FileStream("./tests/resources/bar.csv").save( + tmp_file_path, + f_write_options=CsvWriteOptions(delimiter=","), + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.csv").read() + + def test_save_csv_with_custom_delimiter(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "semicolon.csv" + FileStream("./tests/resources/bar.csv").save( + tmp_file_path, + f_write_options=CsvWriteOptions(delimiter=";"), + ) + content = tmp_file_path.read_text() + assert ";" in content + assert "fizz;buzz" in content + + def test_read_csv_with_custom_quotechar(self, tmp_file_dir): + # Create a CSV with single quotes + tmp_file_path = tmp_file_dir / "quoted.csv" + tmp_file_path.write_text("name,value\n'hello','world'\n") + result = FileStream.process( + tmp_file_path, + f_read_options=CsvReadOptions(quotechar="'"), + ).to_tuple() + assert len(result) == 1 + + # YAML options + def test_save_yaml_with_options(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "test_opts.yaml" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options=FileOptions(encoding="utf-8"), + f_write_options=YamlWriteOptions(indent=2), + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.yaml").read() + + def test_save_yaml_with_block_style(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "block.yaml" + FileStream("./tests/resources/foo.yaml").save( + tmp_file_path, + f_write_options=YamlWriteOptions.block_style(indent=2), + ) + assert tmp_file_path.read_text() == "abc: xyz\nqwerty: 42\n" + + def test_save_yaml_with_flow_style(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "flow.yaml" + FileStream("./tests/resources/foo.yaml").save( + tmp_file_path, + f_write_options=YamlWriteOptions.flow_style(), + ) + assert tmp_file_path.read_text() == "{abc: xyz, qwerty: 42}\n" + + # XML options + def test_save_xml_with_options(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "test_opts.xml" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options=FileOptions.utf8(), + f_write_options=XmlWriteOptions(pretty=True, indent=4), + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.xml").read() + + def test_save_xml_with_pretty_print_factory(self, tmp_file_dir, json_dict): + file_path = "custom_root.xml" + tmp_file_path = tmp_file_dir / file_path + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_write_options=XmlWriteOptions.pretty_print(indent=4), + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + xml_root="my-root", + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) + + # Plain text options + def test_save_plain_with_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "test_opts.txt" + FileStream("./tests/resources/plain.txt").map(lambda x: x.strip()).head(2).save( + tmp_file_path, + f_write_options=PlainTextWriteOptions.with_header_footer( + header="---START---\n", footer="\n---END---" + ), + ) + assert tmp_file_path.read_text() == ( + "---START---\n" + "Lorem ipsum dolor sit amet, consectetur adipisicing elit,\n" + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" + "---END---" + ) + + # FileOptions tests + def test_append_to_plain_with_file_options(self, tmp_file_dir): + file_path = "append_map.txt" + tmp_file_path = tmp_file_dir / file_path + shutil.copyfile("./tests/resources/plain_dict.txt", tmp_file_path) + ( + FileStream(tmp_file_path) + .map(lambda line: line.strip()) + .enumerate() + .filter(lambda line: "ne" in line[1]) + .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") + .save(f_open_options=FileOptions.append()) + ) + assert ( + tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() + ) + + def test_save_with_utf8_file_options(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "utf8_opts.json" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options=FileOptions.utf8(), + f_write_options=JsonWriteOptions(indent=2), + ) + assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.json").read() + + def test_save_with_ascii_file_options(self, tmp_file_dir): + tmp_file_path = tmp_file_dir / "ascii.json" + FileStream("./tests/resources/foo.json").save( + tmp_file_path, + f_open_options=FileOptions.ascii(), + f_write_options=JsonWriteOptions(indent=2, ensure_ascii=True), + ) + content = tmp_file_path.read_text() + assert "abc" in content + + # Combined options tests + def test_save_with_combined_options(self, tmp_file_dir, json_dict): + in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() + tmp_file_path = tmp_file_dir / "combined.json" + FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( + tmp_file_path, + f_open_options=FileOptions(encoding="utf-8"), + f_write_options=JsonWriteOptions(indent=2), + null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, + ) + assert ( + tmp_file_path.read_text() + == open("./tests/resources/save_output/test_null_handler.json").read() + ) diff --git a/tests/test_itertools_mixin.py b/tests/test_itertools_mixin.py index de23d57..6332cd6 100644 --- a/tests/test_itertools_mixin.py +++ b/tests/test_itertools_mixin.py @@ -6,451 +6,409 @@ from pyrio import Stream -def test_accumulate(): - assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate).to_list() == list( - it.accumulate([1, 2, 3, 4, 5]) - ) - assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate, initial=100).to_list() == list( - it.accumulate([1, 2, 3, 4, 5], initial=100) - ) - - # NB -> 'func' although it's 'function' in the docs (!) - assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate, func=operator.mul).to_list() == list( - it.accumulate([1, 2, 3, 4, 5], operator.mul) - ) - - -def test_batched(): - flattened_data = ["roses", "red", "violets", "blue", "sugar", "sweet"] - assert Stream(flattened_data).use(it.batched, n=2).to_list() == list( - it.batched(flattened_data, 2) - ) - - -def test_chain(): - assert Stream("ABC").use(it.chain, iterables="DEF").to_list() == list(it.chain("ABC", "DEF")) - - -def test_chain_from_iterable(): - assert Stream(["ABC", "DEF"]).use(it.chain.from_iterable).to_list() == list( - it.chain.from_iterable(["ABC", "DEF"]) - ) - - -def test_combinations(): - assert Stream.of(1, 2, 3, 4).use(it.combinations, r=3).to_list() == list( - it.combinations([1, 2, 3, 4], r=3) - ) - - -def test_combinations_with_replacement(): - assert Stream("ABC").use(it.combinations_with_replacement, r=2).to_list() == list( - it.combinations_with_replacement("ABC", r=2) - ) - - -def test_compress(): - data = "ABCDEF" - selectors = [1, 0, 1, 0, 1, 1] - assert Stream(data).use(it.compress, selectors=selectors).to_list() == list( - it.compress(data, selectors) - ) - - -def test_count(): - assert Stream.empty().use(it.count, start=10).limit(5).to_list() == [10, 11, 12, 13, 14] - assert Stream.empty().use(it.count, start=10, step=2).limit(5).to_list() == [10, 12, 14, 16, 18] - - -def test_cycle(): - assert Stream("ABCD").use(it.cycle).limit(12).to_list() == [ - "A", - "B", - "C", - "D", - "A", - "B", - "C", - "D", - "A", - "B", - "C", - "D", - ] - - -def test_itertools_dropwhile(): - coll = [1, 4, 6, 3, 8] - predicate = lambda x: x < 5 # noqa - assert Stream(coll).use(it.dropwhile, predicate=predicate).to_list() == list( - it.dropwhile(predicate, coll) - ) - - -def test_itertools_filterfalse(): - coll = [1, 4, 6, 3, 8] - predicate = lambda x: x < 5 # noqa - assert Stream(coll).use(it.filterfalse, predicate=predicate).to_list() == list( - it.filterfalse(predicate, coll) - ) - - -def test_itertools_groupby(): - assert Stream("AAAABBBCCD").use(it.groupby).to_dict(lambda x: (x[0], list(x[1]))) == { - "A": ["A", "A", "A", "A"], - "B": ["B", "B", "B"], - "C": ["C", "C"], - "D": ["D"], - } - - -def test_itertools_islice(): - letters = "ABCDEFG" - assert Stream(letters).use(it.islice, stop=2).to_list() == list(it.islice(letters, 2)) - assert Stream(letters).use(it.islice, start=2, stop=None).to_list() == list( - it.islice(letters, 2, None) - ) - assert Stream(letters).use(it.islice, start=0, stop=None, step=2).to_list() == list( - it.islice(letters, 0, None, 2) - ) - - -def test_itertools_pairwise(): - letters = "ABCDEFG" - assert Stream(letters).use(it.pairwise).to_list() == list(it.pairwise(letters)) - - -def test_permutations(): - assert Stream(range(3)).use(it.permutations, r=3).to_list() == list( - it.permutations(range(3), r=3) - ) - - -def test_product(): - assert Stream.of("ABCD", "xy").use(it.product).to_list() == list(it.product("ABCD", "xy")) - assert Stream.of([1, 2, 3, 4], [5, 6]).use(it.product).to_list() == list( - it.product([1, 2, 3, 4], [5, 6]) - ) - assert Stream(range(3)).use(it.product, repeat=2).to_list() == list( - it.product(range(3), repeat=2) - ) - - -def test_repeat(): - assert Stream(10).use(it.repeat).limit(3).to_list() == [10, 10, 10] - assert Stream(10).use(it.repeat, times=3).to_list() == list(it.repeat(10, times=3)) - - -def test_starmap(): - assert Stream([(2, 5), (3, 2), (10, 3)]).use(it.starmap, function=pow).to_list() == list( - it.starmap(pow, [(2, 5), (3, 2), (10, 3)]) - ) - - -def test_itertools_takewhile(): - coll = [1, 4, 6, 3, 8] - predicate = lambda x: x < 5 # noqa - assert Stream(coll).use(it.takewhile, predicate=predicate).to_list() == list( - it.takewhile(predicate, coll) - ) - - -def test_tee(): - coll = [1, 2, 3, 4, 5, 6] - assert Stream(coll).use(it.tee, n=2).map(tuple).to_list() == [tuple(s) for s in it.tee(coll, 2)] - - -def test_zip_longest(): - assert Stream.of("ABCD", "xy").use(it.zip_longest, fillvalue="-").to_list() == list( - it.zip_longest("ABCD", "xy", fillvalue="-") - ) - assert Stream.of(range(3), range(2)).use(it.zip_longest).to_list() == list( - it.zip_longest(range(3), range(2)) - ) - - -# ### itertools 'recipes' ### -def test_tabulate(): - assert Stream.empty().tabulate(lambda x: x**2).limit(3).to_list() == [0, 1, 4] - assert Stream.empty().tabulate(lambda x: x**2, start=3).limit(3).to_list() == [9, 16, 25] - - -def test_repeat_func(): - operation = lambda x, y: x * y # noqa - args = [2, 3] - times = 4 - assert Stream(args).repeat_func(operation=operation, times=times).to_list() == [6, 6, 6, 6] - - -def test_ncycles(): - coll = {1, 2, 3} - count = 2 - assert Stream(coll).ncycles(count).to_list() == [1, 2, 3, 1, 2, 3] - - -def test_ncycles_zero_times(): - assert Stream({1, 2, 3}).ncycles(count=0).to_list() == [] - - -def test_ncycles_negative_times(): - assert Stream({1, 2, 3}).ncycles(count=-2).to_list() == [] - - -def test_consume(): - assert Stream.of(2, 3, 4, 5).consume(n=2).to_list() == [4, 5] - - -def test_consume_default_start(): - assert Stream.of(2, 3, 4, 5).consume().to_list() == [] - - -def test_consume_negative_start(): - with pytest.raises(ValueError) as e: - Stream.of(2, 3, 4, 5).consume(n=-2).to_list() - assert str(e.value) == "Consume boundary cannot be negative" - - -def test_take_nth(): - stream = Stream.of(2, 3, 4) - assert stream.take_nth(1).get() == 3 - assert stream._is_consumed - - -def test_take_nth_default_value(): - assert Stream.of(2, 3, 4).take_nth(10, default=66).get() == 66 - - -def test_take_nth_negative_index(): - assert Stream.of(2, 3, 4).take_nth(-1).get() == 4 - - -def test_take_nth_not_found(): - assert Stream.empty().take_nth(2).is_empty() - - -def test_all_equal(): - stream = Stream([2, 2, 2]) - assert stream.all_equal(key=int) - assert stream._is_consumed - - -def test_all_equal_false(): - assert Stream([2, 5, 3]).all_equal() is False - - -def test_all_equal_custom_key(Foo): - fizz = Foo("fizz", 42) - buzz = Foo("buzz", 42) - coll = [fizz, buzz] - assert Stream(coll).all_equal(key=lambda x: x.num) - assert Stream(coll).all_equal(key=lambda x: x.name) is False - - -# ### view ### -def test_view(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(2, 6).to_list() == [3, 4, 5, 6] - - -def test_view_default_stop(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(4).to_list() == [5, 6, 7, 8, 9] - - -def test_view_default_boundaries(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view().to_list() == [1, 2, 3, 4, 5, 6, 7, 8, 9] - - -def test_view_custom_step(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(step=2).to_list() == [1, 3, 5, 7, 9] - - -def test_view_custom_stop(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(stop=-3).to_list() == [1, 2, 3, 4, 5, 6] - - -def test_view_negative_start(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(-3).to_list() == [7, 8, 9] - - -def test_view_negative_stop(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(stop=-4).to_list() == [1, 2, 3, 4, 5] - - -def test_view_custom_boundaries(): - coll = [1, 2, 3, 4, 5, 6, 7, 8, 9] - assert Stream(coll).view(2, -3).to_list() == [3, 4, 5, 6] - assert Stream(coll).view(-5, -2).to_list() == [5, 6, 7] - - -def test_view_negative_step(): - with pytest.raises(ValueError) as e: - Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(step=-1).to_list() - assert str(e.value) == "Step must be a positive integer or None" - - -# ### sliding window ### -def test_sliding_window(): - assert Stream("ABCDEFG").sliding_window(4).to_list() == [ - ("A", "B", "C", "D"), - ("B", "C", "D", "E"), - ("C", "D", "E", "F"), - ("D", "E", "F", "G"), - ] - - -def test_sliding_window_empty_collection(): - assert Stream.empty().sliding_window(2).to_list() == [] - - -def test_sliding_window_negative_count(): - with pytest.raises(ValueError) as e: - Stream("ABCD").sliding_window(-1).to_list() - assert str(e.value) == "Window size cannot be negative" - - -def test_subslices(): - assert Stream("ABCD").subslices().to_list() == [ - "A", - "AB", - "ABC", - "ABCD", - "B", - "BC", - "BCD", - "C", - "CD", - "D", - ] - - -def test_subslices_empty_collection(): - assert Stream.empty().subslices().to_list() == [] - - -def test_partition(): - assert Stream(range(10)).partition(lambda x: x % 2 != 0).map(lambda x: list(x)).to_list() == [ - [1, 3, 5, 7, 9], - [0, 2, 4, 6, 8], - ] - - -def test_round_robin(): - assert Stream(["ABC", "D", "EF"]).round_robin().to_list() == ["A", "D", "E", "B", "F", "C"] - - -def test_grouper_fill(): - assert Stream("ABCDEFG").grouper(3, incomplete="fill", fill_value="x").to_list() == [ - ("A", "B", "C"), - ("D", "E", "F"), - ("G", "x", "x"), - ] - - -def test_grouper_default_incomplete(): - assert Stream("ABCDEFG").grouper(3, fill_value="x").to_list() == [ - ("A", "B", "C"), - ("D", "E", "F"), - ("G", "x", "x"), - ] - - -def test_grouper_default_fillvalue(): - assert Stream("ABCDEFG").grouper(3).to_list() == [ - ("A", "B", "C"), - ("D", "E", "F"), - ("G", None, None), - ] - - -def test_grouper_strict(): - with pytest.raises(ValueError) as e: - Stream("ABCDEFG").grouper(3, incomplete="strict").to_list() - assert str(e.value) == "zip() argument 2 is shorter than argument 1" - - -def test_grouper_ignore(): - assert Stream("ABCDEFG").grouper(3, incomplete="ignore").to_list() == [ - ("A", "B", "C"), - ("D", "E", "F"), - ] - - -def test_grouper_invalid_incomplete_flag(): - with pytest.raises(ValueError) as e: - Stream("ABCDEFG").grouper(3, incomplete="foo").to_list() - assert str(e.value) == "Invalid incomplete flag 'foo', expected: 'fill', 'strict', or 'ignore'" - - -# ### unique ### -def test_unique(): - assert Stream([[1, 2], [3, 4], [1, 2]]).unique().to_list() == [[1, 2], [3, 4]] - - -def test_unique_reverse(): - assert Stream([[1, 2], [3, 4], [1, 2]]).unique(reverse=True).to_list() == [[3, 4], [1, 2]] - - -def test_unique_custom_key(Foo): - foo = Foo("foo", 1) - bar = Foo("bar", 2) - fizz = Foo("fizz", 3) - buzz = Foo("buzz", 4) - coll = [foo, bar, fizz, buzz, foo, bar] - assert Stream(coll).unique(key=lambda x: x.num).to_dict(lambda x: (x.name, x.num)) == { - "foo": 1, - "bar": 2, - "fizz": 3, - "buzz": 4, - } - - -def test_unique_custom_key_reversed(Foo): - foo = Foo("foo", 1) - bar = Foo("bar", 2) - fizz = Foo("fizz", 3) - buzz = Foo("buzz", 4) - coll = [foo, bar, fizz, buzz, foo, bar] - assert Stream(coll).unique(key=lambda x: x.num, reverse=True).to_list() == [ - buzz, - fizz, - bar, - foo, - ] - - -def test_unique_just_seen(): - assert Stream("AAAABBBCCDAABBB").unique_just_seen().to_list() == ["A", "B", "C", "D", "A", "B"] - - -def test_unique_just_seen_custom_key(): - assert Stream("ABBcCAD").unique_just_seen(key=str.casefold).to_list() == [ - "A", - "B", - "c", - "A", - "D", - ] - - -def test_unique_just_seen_empty_collection(): - assert Stream([]).unique_just_seen().to_list() == [] - - -def test_unique_ever_seen(): - assert Stream("AAAABBBCCDAABBB").unique_ever_seen().to_list() == ["A", "B", "C", "D"] - - -def test_unique_ever_seen_custom_key(): - assert Stream("ABBcCAD").unique_ever_seen(key=str.casefold).to_list() == ["A", "B", "c", "D"] - - -# ### find_indices ### -def test_find_indices(): - assert Stream("AABCADEAF").find_indices("A").to_list() == [0, 1, 4, 7] - - -def test_find_indices_custom_start(): - assert Stream("AABCADEAF").find_indices(value="A", start=3).to_list() == [4, 7] - - -def test_find_indices_custom_stop(): - assert Stream("AABCADEAF").find_indices(value="A", stop=5).to_list() == [0, 1, 4] +class TestItertoolsMixin: + def test_accumulate(self): + assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate).to_list() == list( + it.accumulate([1, 2, 3, 4, 5]) + ) + assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate, initial=100).to_list() == list( + it.accumulate([1, 2, 3, 4, 5], initial=100) + ) + + # NB -> 'func' although it's 'function' in the docs (!) + assert Stream.of(1, 2, 3, 4, 5).use(it.accumulate, func=operator.mul).to_list() == list( + it.accumulate([1, 2, 3, 4, 5], operator.mul) + ) + + def test_batched(self): + flattened_data = ["roses", "red", "violets", "blue", "sugar", "sweet"] + assert Stream(flattened_data).use(it.batched, n=2).to_list() == list( + it.batched(flattened_data, 2) + ) + + def test_chain(self): + assert Stream("ABC").use(it.chain, iterables="DEF").to_list() == list( + it.chain("ABC", "DEF") + ) + + def test_chain_from_iterable(self): + assert Stream(["ABC", "DEF"]).use(it.chain.from_iterable).to_list() == list( + it.chain.from_iterable(["ABC", "DEF"]) + ) + + def test_combinations(self): + assert Stream.of(1, 2, 3, 4).use(it.combinations, r=3).to_list() == list( + it.combinations([1, 2, 3, 4], r=3) + ) + + def test_combinations_with_replacement(self): + assert Stream("ABC").use(it.combinations_with_replacement, r=2).to_list() == list( + it.combinations_with_replacement("ABC", r=2) + ) + + def test_compress(self): + data = "ABCDEF" + selectors = [1, 0, 1, 0, 1, 1] + assert Stream(data).use(it.compress, selectors=selectors).to_list() == list( + it.compress(data, selectors) + ) + + def test_count(self): + assert Stream.empty().use(it.count, start=10).limit(5).to_list() == [10, 11, 12, 13, 14] + assert Stream.empty().use(it.count, start=10, step=2).limit(5).to_list() == [ + 10, + 12, + 14, + 16, + 18, + ] + + def test_cycle(self): + assert Stream("ABCD").use(it.cycle).limit(12).to_list() == [ + "A", + "B", + "C", + "D", + "A", + "B", + "C", + "D", + "A", + "B", + "C", + "D", + ] + + def test_itertools_dropwhile(self): + coll = [1, 4, 6, 3, 8] + predicate = lambda x: x < 5 # noqa + assert Stream(coll).use(it.dropwhile, predicate=predicate).to_list() == list( + it.dropwhile(predicate, coll) + ) + + def test_itertools_filterfalse(self): + coll = [1, 4, 6, 3, 8] + predicate = lambda x: x < 5 # noqa + assert Stream(coll).use(it.filterfalse, predicate=predicate).to_list() == list( + it.filterfalse(predicate, coll) + ) + + def test_itertools_groupby(self): + assert Stream("AAAABBBCCD").use(it.groupby).to_dict(lambda x: (x[0], list(x[1]))) == { + "A": ["A", "A", "A", "A"], + "B": ["B", "B", "B"], + "C": ["C", "C"], + "D": ["D"], + } + + def test_itertools_islice(self): + letters = "ABCDEFG" + assert Stream(letters).use(it.islice, stop=2).to_list() == list(it.islice(letters, 2)) + assert Stream(letters).use(it.islice, start=2, stop=None).to_list() == list( + it.islice(letters, 2, None) + ) + assert Stream(letters).use(it.islice, start=0, stop=None, step=2).to_list() == list( + it.islice(letters, 0, None, 2) + ) + + def test_itertools_pairwise(self): + letters = "ABCDEFG" + assert Stream(letters).use(it.pairwise).to_list() == list(it.pairwise(letters)) + + def test_permutations(self): + assert Stream(range(3)).use(it.permutations, r=3).to_list() == list( + it.permutations(range(3), r=3) + ) + + def test_product(self): + assert Stream.of("ABCD", "xy").use(it.product).to_list() == list(it.product("ABCD", "xy")) + assert Stream.of([1, 2, 3, 4], [5, 6]).use(it.product).to_list() == list( + it.product([1, 2, 3, 4], [5, 6]) + ) + assert Stream(range(3)).use(it.product, repeat=2).to_list() == list( + it.product(range(3), repeat=2) + ) + + def test_repeat(self): + assert Stream(10).use(it.repeat).limit(3).to_list() == [10, 10, 10] + assert Stream(10).use(it.repeat, times=3).to_list() == list(it.repeat(10, times=3)) + + def test_starmap(self): + assert Stream([(2, 5), (3, 2), (10, 3)]).use(it.starmap, function=pow).to_list() == list( + it.starmap(pow, [(2, 5), (3, 2), (10, 3)]) + ) + + def test_itertools_takewhile(self): + coll = [1, 4, 6, 3, 8] + predicate = lambda x: x < 5 # noqa + assert Stream(coll).use(it.takewhile, predicate=predicate).to_list() == list( + it.takewhile(predicate, coll) + ) + + def test_tee(self): + coll = [1, 2, 3, 4, 5, 6] + assert Stream(coll).use(it.tee, n=2).map(tuple).to_list() == [ + tuple(s) for s in it.tee(coll, 2) + ] + + def test_zip_longest(self): + assert Stream.of("ABCD", "xy").use(it.zip_longest, fillvalue="-").to_list() == list( + it.zip_longest("ABCD", "xy", fillvalue="-") + ) + assert Stream.of(range(3), range(2)).use(it.zip_longest).to_list() == list( + it.zip_longest(range(3), range(2)) + ) + + # ### itertools 'recipes' ### + def test_tabulate(self): + assert Stream.empty().tabulate(lambda x: x**2).limit(3).to_list() == [0, 1, 4] + assert Stream.empty().tabulate(lambda x: x**2, start=3).limit(3).to_list() == [9, 16, 25] + + def test_repeat_func(self): + operation = lambda x, y: x * y # noqa + args = [2, 3] + times = 4 + assert Stream(args).repeat_func(operation=operation, times=times).to_list() == [6, 6, 6, 6] + + def test_ncycles(self): + coll = {1, 2, 3} + count = 2 + assert Stream(coll).ncycles(count).to_list() == [1, 2, 3, 1, 2, 3] + + def test_ncycles_zero_times(self): + assert Stream({1, 2, 3}).ncycles(count=0).to_list() == [] + + def test_ncycles_negative_times(self): + assert Stream({1, 2, 3}).ncycles(count=-2).to_list() == [] + + def test_consume(self): + assert Stream.of(2, 3, 4, 5).consume(n=2).to_list() == [4, 5] + + def test_consume_default_start(self): + assert Stream.of(2, 3, 4, 5).consume().to_list() == [] + + def test_consume_negative_start(self): + with pytest.raises(ValueError) as e: + Stream.of(2, 3, 4, 5).consume(n=-2).to_list() + assert str(e.value) == "Consume boundary cannot be negative" + + def test_take_nth(self): + stream = Stream.of(2, 3, 4) + assert stream.take_nth(1).get() == 3 + assert stream._is_consumed + + def test_take_nth_default_value(self): + assert Stream.of(2, 3, 4).take_nth(10, default=66).get() == 66 + + def test_take_nth_negative_index(self): + assert Stream.of(2, 3, 4).take_nth(-1).get() == 4 + + def test_take_nth_not_found(self): + assert Stream.empty().take_nth(2).is_empty() + + def test_all_equal(self): + stream = Stream([2, 2, 2]) + assert stream.all_equal(key=int) + assert stream._is_consumed + + def test_all_equal_false(self): + assert Stream([2, 5, 3]).all_equal() is False + + def test_all_equal_custom_key(self, Foo): + fizz = Foo("fizz", 42) + buzz = Foo("buzz", 42) + coll = [fizz, buzz] + assert Stream(coll).all_equal(key=lambda x: x.num) + assert Stream(coll).all_equal(key=lambda x: x.name) is False + + # ### view ### + def test_view(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(2, 6).to_list() == [3, 4, 5, 6] + + def test_view_default_stop(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(4).to_list() == [5, 6, 7, 8, 9] + + def test_view_default_boundaries(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view().to_list() == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + def test_view_custom_step(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(step=2).to_list() == [1, 3, 5, 7, 9] + + def test_view_custom_stop(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(stop=-3).to_list() == [1, 2, 3, 4, 5, 6] + + def test_view_negative_start(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(-3).to_list() == [7, 8, 9] + + def test_view_negative_stop(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(stop=-4).to_list() == [1, 2, 3, 4, 5] + + def test_view_custom_boundaries(self): + coll = [1, 2, 3, 4, 5, 6, 7, 8, 9] + assert Stream(coll).view(2, -3).to_list() == [3, 4, 5, 6] + assert Stream(coll).view(-5, -2).to_list() == [5, 6, 7] + + def test_view_negative_step(self): + with pytest.raises(ValueError) as e: + Stream([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(step=-1).to_list() + assert str(e.value) == "Step must be a positive integer or None" + + # ### sliding window ### + def test_sliding_window(self): + assert Stream("ABCDEFG").sliding_window(4).to_list() == [ + ("A", "B", "C", "D"), + ("B", "C", "D", "E"), + ("C", "D", "E", "F"), + ("D", "E", "F", "G"), + ] + + def test_sliding_window_empty_collection(self): + assert Stream.empty().sliding_window(2).to_list() == [] + + def test_sliding_window_negative_count(self): + with pytest.raises(ValueError) as e: + Stream("ABCD").sliding_window(-1).to_list() + assert str(e.value) == "Window size cannot be negative" + + def test_subslices(self): + assert Stream("ABCD").subslices().to_list() == [ + "A", + "AB", + "ABC", + "ABCD", + "B", + "BC", + "BCD", + "C", + "CD", + "D", + ] + + def test_subslices_empty_collection(self): + assert Stream.empty().subslices().to_list() == [] + + def test_partition(self): + assert Stream(range(10)).partition(lambda x: x % 2 != 0).map( + lambda x: list(x) + ).to_list() == [ + [1, 3, 5, 7, 9], + [0, 2, 4, 6, 8], + ] + + def test_round_robin(self): + assert Stream(["ABC", "D", "EF"]).round_robin().to_list() == ["A", "D", "E", "B", "F", "C"] + + def test_grouper_fill(self): + assert Stream("ABCDEFG").grouper(3, incomplete="fill", fill_value="x").to_list() == [ + ("A", "B", "C"), + ("D", "E", "F"), + ("G", "x", "x"), + ] + + def test_grouper_default_incomplete(self): + assert Stream("ABCDEFG").grouper(3, fill_value="x").to_list() == [ + ("A", "B", "C"), + ("D", "E", "F"), + ("G", "x", "x"), + ] + + def test_grouper_default_fillvalue(self): + assert Stream("ABCDEFG").grouper(3).to_list() == [ + ("A", "B", "C"), + ("D", "E", "F"), + ("G", None, None), + ] + + def test_grouper_strict(self): + with pytest.raises(ValueError) as e: + Stream("ABCDEFG").grouper(3, incomplete="strict").to_list() + assert str(e.value) == "zip() argument 2 is shorter than argument 1" + + def test_grouper_ignore(self): + assert Stream("ABCDEFG").grouper(3, incomplete="ignore").to_list() == [ + ("A", "B", "C"), + ("D", "E", "F"), + ] + + def test_grouper_invalid_incomplete_flag(self): + with pytest.raises(ValueError) as e: + Stream("ABCDEFG").grouper(3, incomplete="foo").to_list() + assert ( + str(e.value) == "Invalid incomplete flag 'foo', expected: 'fill', 'strict', or 'ignore'" + ) + + # ### unique ### + def test_unique(self): + assert Stream([[1, 2], [3, 4], [1, 2]]).unique().to_list() == [[1, 2], [3, 4]] + + def test_unique_reverse(self): + assert Stream([[1, 2], [3, 4], [1, 2]]).unique(reverse=True).to_list() == [[3, 4], [1, 2]] + + def test_unique_custom_key(self, Foo): + foo = Foo("foo", 1) + bar = Foo("bar", 2) + fizz = Foo("fizz", 3) + buzz = Foo("buzz", 4) + coll = [foo, bar, fizz, buzz, foo, bar] + assert Stream(coll).unique(key=lambda x: x.num).to_dict(lambda x: (x.name, x.num)) == { + "foo": 1, + "bar": 2, + "fizz": 3, + "buzz": 4, + } + + def test_unique_custom_key_reversed(self, Foo): + foo = Foo("foo", 1) + bar = Foo("bar", 2) + fizz = Foo("fizz", 3) + buzz = Foo("buzz", 4) + coll = [foo, bar, fizz, buzz, foo, bar] + assert Stream(coll).unique(key=lambda x: x.num, reverse=True).to_list() == [ + buzz, + fizz, + bar, + foo, + ] + + def test_unique_just_seen(self): + assert Stream("AAAABBBCCDAABBB").unique_just_seen().to_list() == [ + "A", + "B", + "C", + "D", + "A", + "B", + ] + + def test_unique_just_seen_custom_key(self): + assert Stream("ABBcCAD").unique_just_seen(key=str.casefold).to_list() == [ + "A", + "B", + "c", + "A", + "D", + ] + + def test_unique_just_seen_empty_collection(self): + assert Stream([]).unique_just_seen().to_list() == [] + + def test_unique_ever_seen(self): + assert Stream("AAAABBBCCDAABBB").unique_ever_seen().to_list() == ["A", "B", "C", "D"] + + def test_unique_ever_seen_custom_key(self): + assert Stream("ABBcCAD").unique_ever_seen(key=str.casefold).to_list() == [ + "A", + "B", + "c", + "D", + ] + + # ### find_indices ### + def test_find_indices(self): + assert Stream("AABCADEAF").find_indices("A").to_list() == [0, 1, 4, 7] + + def test_find_indices_custom_start(self): + assert Stream("AABCADEAF").find_indices(value="A", start=3).to_list() == [4, 7] + + def test_find_indices_custom_stop(self): + assert Stream("AABCADEAF").find_indices(value="A", stop=5).to_list() == [0, 1, 4] diff --git a/tests/test_optional.py b/tests/test_optional.py index a8303c2..338e8d8 100644 --- a/tests/test_optional.py +++ b/tests/test_optional.py @@ -7,86 +7,75 @@ from pyrio.exceptions import NoSuchElementError, NoneTypeError -def test_optional_get_raises(): - with pytest.raises(NoSuchElementError) as e: - Optional.empty().get() - assert str(e.value) == "Optional is empty" - - -def test_optional_of_none_raises(): - with pytest.raises(NoneTypeError) as e: - Optional.of(None) - assert str(e.value) == "Value cannot be None" - - -def test_print_optional(): - assert str(Optional.of(2)) == "Optional[2]" - assert str(Optional.of_nullable(None)) == "Optional[None]" - - -def test_is_empty(): - assert Optional.of(3).is_empty() is False - assert Optional.of_nullable(None).is_empty() - - -def test_get(): - assert Optional.of(3).get() == 3 - - -def test_is_present(): - assert Optional.of(3).is_present() - - -def test_if_present(): - f = io.StringIO() - with redirect_stdout(f): - Optional.of(3).if_present(action=lambda x: print(f"{x}", end="")) - assert f.getvalue() == "3" - - -def test_if_present_or_else(): - f = io.StringIO() - with redirect_stdout(f): - Optional.of(3).if_present_or_else( - action=lambda x: print(f"{x}", end=""), empty_action=lambda: print("BANG!", end="") - ) - assert f.getvalue() == "3" - - -def test_if_present_or_else_empty_action(): - f = io.StringIO() - with redirect_stdout(f): - Optional.empty().if_present_or_else( - action=lambda x: print(f"{x}", end=""), empty_action=lambda: print("BANG!", end="") - ) - assert f.getvalue() == "BANG!" - - -def test_or_else(): - assert Optional.of(3).or_else(4) == 3 - assert Optional.empty().or_else(4) == 4 - - -def test_or_else_get(Foo): - foo = Foo(name="Foo", num=43) - assert Optional.empty().or_else_get(supplier=lambda: foo) is foo - - -def test_or_else_raise(Foo): - with pytest.raises(NoSuchElementError) as e: - Optional.empty().or_else_raise() - assert str(e.value) == "Optional is empty" - - -def test_or_else_raise_custom_supplier(Foo): - err_msg = "Yo Mr. White...!" - - class DamnItError(Exception): - pass - - def damn_it_supplier(): - raise DamnItError(err_msg) - - with pytest.raises(DamnItError) as e: - Optional.empty().or_else_raise(damn_it_supplier) - assert str(e.value) == err_msg +class TestOptional: + def test_optional_get_raises(self): + with pytest.raises(NoSuchElementError) as e: + Optional.empty().get() + assert str(e.value) == "Optional is empty" + + def test_optional_of_none_raises(self): + with pytest.raises(NoneTypeError) as e: + Optional.of(None) + assert str(e.value) == "Value cannot be None" + + def test_print_optional(self): + assert str(Optional.of(2)) == "Optional[2]" + assert str(Optional.of_nullable(None)) == "Optional[None]" + + def test_is_empty(self): + assert Optional.of(3).is_empty() is False + assert Optional.of_nullable(None).is_empty() + + def test_get(self): + assert Optional.of(3).get() == 3 + + def test_is_present(self): + assert Optional.of(3).is_present() + + def test_if_present(self): + f = io.StringIO() + with redirect_stdout(f): + Optional.of(3).if_present(action=lambda x: print(f"{x}", end="")) + assert f.getvalue() == "3" + + def test_if_present_or_else(self): + f = io.StringIO() + with redirect_stdout(f): + Optional.of(3).if_present_or_else( + action=lambda x: print(f"{x}", end=""), empty_action=lambda: print("BANG!", end="") + ) + assert f.getvalue() == "3" + + def test_if_present_or_else_empty_action(self): + f = io.StringIO() + with redirect_stdout(f): + Optional.empty().if_present_or_else( + action=lambda x: print(f"{x}", end=""), empty_action=lambda: print("BANG!", end="") + ) + assert f.getvalue() == "BANG!" + + def test_or_else(self): + assert Optional.of(3).or_else(4) == 3 + assert Optional.empty().or_else(4) == 4 + + def test_or_else_get(self, Foo): + foo = Foo(name="Foo", num=43) + assert Optional.empty().or_else_get(supplier=lambda: foo) is foo + + def test_or_else_raise(self, Foo): + with pytest.raises(NoSuchElementError) as e: + Optional.empty().or_else_raise() + assert str(e.value) == "Optional is empty" + + def test_or_else_raise_custom_supplier(self, Foo): + err_msg = "Yo Mr. White...!" + + class DamnItError(Exception): + pass + + def damn_it_supplier(): + raise DamnItError(err_msg) + + with pytest.raises(DamnItError) as e: + Optional.empty().or_else_raise(damn_it_supplier) + assert str(e.value) == err_msg diff --git a/tests/test_stream.py b/tests/test_stream.py index bf84600..c0cf4a8 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -9,1001 +9,873 @@ from pyrio.exceptions import IllegalStateError, UnsupportedTypeError, NoneTypeError -def test_stream(): - assert Stream([1, 2, 3])._iterable == [1, 2, 3] +class TestStream: + def test_stream(self): + assert Stream([1, 2, 3])._iterable == [1, 2, 3] + def test_stream_from_none(self): + with pytest.raises(NoneTypeError) as e: + Stream(None) + assert str(e.value) == "Cannot create Stream from None" -def test_stream_from_none(): - with pytest.raises(NoneTypeError) as e: - Stream(None) - assert str(e.value) == "Cannot create Stream from None" + def test_stream_of(self): + assert Stream.of(1, 2, 3)._iterable == (1, 2, 3) + def test_stream_of_nullable(self): + assert Stream.of_nullable(None).count() == 0 -def test_stream_of(): - assert Stream.of(1, 2, 3)._iterable == (1, 2, 3) + nonempty_stream = Stream.of_nullable([1, 2, 3]) + assert nonempty_stream.count() != 0 + assert nonempty_stream._iterable == [1, 2, 3] + def test_empty_stream(self): + assert Stream.empty().count() == 0 -def test_stream_of_nullable(): - assert Stream.of_nullable(None).count() == 0 + def test_iterate(self): + assert Stream.iterate(0, lambda x: x + 1).limit(5).to_list() == [0, 1, 2, 3, 4] - nonempty_stream = Stream.of_nullable([1, 2, 3]) - assert nonempty_stream.count() != 0 - assert nonempty_stream._iterable == [1, 2, 3] + def test_iterate_skip(self): + assert Stream.iterate(0, lambda x: x + 1).skip(5).limit(5).to_list() == [5, 6, 7, 8, 9] + def test_iterate_with_predicate(self): + assert Stream.iterate(0, lambda x: x + 1, lambda x: x < 5).to_list() == [0, 1, 2, 3, 4] + assert Stream.iterate(0, lambda x: x + 1, lambda x: x < 0).to_list() == [] -def test_empty_stream(): - assert Stream.empty().count() == 0 + def test_generate(self): + assert Stream.generate(lambda: 42).limit(3).to_list() == [42, 42, 42] + def test_constant(self): + assert Stream.constant(8).limit(3).to_list() == [8, 8, 8] -def test_iterate(): - assert Stream.iterate(0, lambda x: x + 1).limit(5).to_list() == [0, 1, 2, 3, 4] + def test_range(self): + assert Stream.from_range(0, 10).to_list() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + assert Stream.from_range(0, 10, 3).to_list() == [0, 3, 6, 9] + assert Stream.from_range(10, -1, -2).to_list() == [10, 8, 6, 4, 2, 0] - -def test_iterate_skip(): - assert Stream.iterate(0, lambda x: x + 1).skip(5).limit(5).to_list() == [5, 6, 7, 8, 9] - - -def test_iterate_with_predicate(): - assert Stream.iterate(0, lambda x: x + 1, lambda x: x < 5).to_list() == [0, 1, 2, 3, 4] - assert Stream.iterate(0, lambda x: x + 1, lambda x: x < 0).to_list() == [] - - -def test_generate(): - assert Stream.generate(lambda: 42).limit(3).to_list() == [42, 42, 42] - - -def test_constant(): - assert Stream.constant(8).limit(3).to_list() == [8, 8, 8] - - -def test_range(): - assert Stream.from_range(0, 10).to_list() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - assert Stream.from_range(0, 10, 3).to_list() == [0, 3, 6, 9] - assert Stream.from_range(10, -1, -2).to_list() == [10, 8, 6, 4, 2, 0] - - -def test_iterable_from_string(): - json_str = '{"Name": "Jennifer Smith", "Phone": "555-123-4568", "Email": "jen123@gmail.com"}' - json_map = json.loads(json_str) - assert Stream(json_map).filter(lambda x: len(x.key) < 6).map(lambda x: x.key).to_tuple() == ( - "Name", - "Phone", - "Email", - ) - assert Stream(json_map).map(lambda x: f"***{x.value}***").to_tuple() == ( - "***Jennifer Smith***", - "***555-123-4568***", - "***jen123@gmail.com***", - ) - - -def test_empty_json_from_string(): - empty_json = "{}" - assert Stream(json.loads(empty_json)).to_tuple() == () - - -def test_filter(): - assert Stream([1, 2, 3, 4, 5, 6]).filter(lambda x: x % 2 == 0).to_list() == [2, 4, 6] - - -def test_map(): - assert Stream([1, 2, 3]).map(str).to_list() == ["1", "2", "3"] - - -def test_map_lambda(): - assert Stream([1, 2, 3]).map(lambda x: x + 5).to_list() == [6, 7, 8] - - -def test_map_dict(): - assert Stream({"x": 1, "y": 2}).map(lambda x: x.key + str(x.value)).to_list() == ["x1", "y2"] - - -def test_filter_map(): - assert Stream([None, "foo", "", "bar"]).filter_map(str.upper).to_list() == ["FOO", "", "BAR"] - - -def test_filter_map_discard_falsy(): - assert Stream.of(None, "foo", "", "bar", 0, []).filter_map( - str.upper, discard_falsy=True - ).to_list() == [ - "FOO", - "BAR", - ] - - -def test_reduce(): - assert Stream([1, 2, 3]).reduce(lambda acc, val: acc + val, identity=3).get() == 9 - - -def test_reduce_no_identity_provided(): - assert Stream([1, 2, 3]).reduce(lambda acc, val: acc + val).get() == 6 - - -def test_reduce_empty_collection(): - assert Stream([]).reduce(lambda acc, val: acc + val).is_empty() - - -def test_reduce_on_iterator(): - # NB: iterator has no len() - assert Stream(iter([1, 2, 3, 4])).reduce(lambda acc, val: acc + val).get() == 10 - - -def test_reduce_on_iterator_with_identity(): - assert Stream(iter([1, 2, 3])).reduce(lambda acc, val: acc + val, identity=10).get() == 16 - - -def test_reduce_on_empty_iterator(): - assert Stream(iter([])).reduce(lambda acc, val: acc + val).is_empty() - - -def test_reduce_on_empty_iterator_with_identity(): - assert Stream(iter([])).reduce(lambda acc, val: acc + val, identity=42).get() == 42 - - -def test_reduce_on_single_element_iterator(): - assert Stream(iter([5])).reduce(lambda acc, val: acc + val).get() == 5 - - -def test_for_each(): - f = io.StringIO() - with redirect_stdout(f): - Stream([1, 2, 3, 4]).for_each(lambda x: print(f"{'#' * x} ", end="")) - assert f.getvalue() == "# ## ### #### " - - -def test_enumerate(): - iterable = ["x", "y", "z"] - assert Stream(iterable).enumerate().to_list() == [(0, "x"), (1, "y"), (2, "z")] - assert Stream(iterable).enumerate(start=1).to_list() == [(1, "x"), (2, "y"), (3, "z")] - - -def test_peek(): - f = io.StringIO() - with redirect_stdout(f): - result = ( - Stream([1, 2, 3, 4]) - .filter(lambda x: x > 2) - .peek(lambda x: print(f"{x} ", end="")) - .map(lambda x: x * 20) - .to_list() + def test_iterable_from_string(self): + json_str = ( + '{"Name": "Jennifer Smith", "Phone": "555-123-4568", "Email": "jen123@gmail.com"}' + ) + json_map = json.loads(json_str) + assert Stream(json_map).filter(lambda x: len(x.key) < 6).map( + lambda x: x.key + ).to_tuple() == ( + "Name", + "Phone", + "Email", + ) + assert Stream(json_map).map(lambda x: f"***{x.value}***").to_tuple() == ( + "***Jennifer Smith***", + "***555-123-4568***", + "***jen123@gmail.com***", ) - assert f.getvalue() == "3 4 " - assert result == [60, 80] - - -# ### skip ### -def test_skip(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).skip(7).to_tuple() == (8, 9, 10) - - -def test_skip_empty(): - assert Stream.empty().skip(3).to_tuple() == () - - -def test_skip_bigger_than_stream_count(): - assert Stream([1, 2]).skip(5).to_tuple() == () - - -def test_skip_negative_count(): - with pytest.raises(ValueError) as e: - Stream([1, 2]).skip(-5).to_tuple() - assert str(e.value) == "Skip count cannot be negative" - - -# ### limit ### -def test_limit(): - assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).limit(3).to_tuple() == (1, 2, 3) - - -def test_limit_empty(): - assert Stream.empty().limit(3).to_tuple() == () - - -def test_limit_bigger_than_stream_count(): - assert Stream([1, 2]).limit(5).to_tuple() == (1, 2) - - -def test_limit_negative_count(): - with pytest.raises(ValueError) as e: - Stream([1, 2]).limit(-5).to_tuple() - assert str(e.value) == "Limit count cannot be negative" - - -def test_head(): - assert ( - Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).limit(3).to_tuple() - == Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).head(3).to_tuple() - ) - - -def test_head_negative_count(): - with pytest.raises(ValueError) as e: - Stream([1, 2]).head(-5).to_tuple() - assert str(e.value) == "Head count cannot be negative" - - -# ### tail ### -def test_tail(): - assert Stream.of(1, 2, 3, 4).tail(2).to_list() == [3, 4] - - -def test_tail_empty(): - assert Stream.empty().tail(3).to_tuple() == () - - -def test_tail_bigger_than_stream_count(): - assert Stream([1, 2]).tail(5).to_tuple() == (1, 2) - - -def test_tail_negative_count(): - with pytest.raises(ValueError) as e: - Stream([1, 2]).tail(-5).to_tuple() - assert str(e.value) == "Tail count cannot be negative" - - -# ### concat ### -def test_concat(): - assert Stream.of(1, 2, 3).concat(Stream.of(4, 5, 6)).to_list() == [1, 2, 3, 4, 5, 6] - assert Stream([1, 2, 3]).concat([4, 5]).to_list() == [1, 2, 3, 4, 5] - - -def test_concat_empty(): - assert Stream.empty().concat(Stream.of(1, 2, 3)).to_list() == [1, 2, 3] - assert Stream.concat(Stream.empty(), Stream.empty()).to_list() == [] - - -def test_concat_linear_collections(): - assert Stream((1, 2, 3)).concat([5, 6]).to_list() == [1, 2, 3, 5, 6] - assert Stream([1, 2, 3]).concat([(5, 6), [8]]).to_list() == [1, 2, 3, (5, 6), [8]] - assert Stream([1, 2, 3]).concat([(5, 6), [8]]).flatten().to_list() == [1, 2, 3, 5, 6, 8] - # hacky but works as Stream.of() is passed as self - assert Stream.concat(Stream.of(1, 2, 3)).concat((5, 6), {7}).to_list() == [1, 2, 3, 5, 6, 7] - - -def test_concat_dicts_to_stream(): - first_dict = {"x": 1, "y": 2} - second_dict = {"p": 33, "q": 44, "r": 55} - items_list = [ - DictItem(key="x", value=1), - DictItem(key="y", value=2), - DictItem(key="p", value=33), - DictItem(key="q", value=44), - DictItem(key="r", value=55), - ] - assert Stream.empty().concat(first_dict, second_dict).to_list() == items_list - assert Stream(first_dict).concat(Stream(second_dict)).to_list() == items_list - assert Stream(first_dict).concat(second_dict).to_list() == items_list - - assert Stream.concat(Stream.empty(), second_dict).to_list() == [ - DictItem(key="p", value=33), - DictItem(key="q", value=44), - DictItem(key="r", value=55), - ] - assert Stream(first_dict).concat(Stream(second_dict)).to_dict(lambda x: (x.key, x.value)) == { - "x": 1, - "y": 2, - "p": 33, - "q": 44, - "r": 55, - } - assert Stream(first_dict).concat(Stream(second_dict)).to_dict(lambda x: x) == { - "x": 1, - "y": 2, - "p": 33, - "q": 44, - "r": 55, - } - - -def test_concat_raises_non_iterable(): - with pytest.raises(TypeError) as e: - Stream([1, 2, 3]).concat(5).to_list() - assert str(e.value) == "'int' object is not iterable" - - -# ### prepend ### -def test_prepend_collection(): - assert Stream([2, 3, 4]).prepend([1]).to_list() == [1, 2, 3, 4] - assert Stream([2, 3, 4]).prepend((0, 1)).to_list() == [0, 1, 2, 3, 4] - assert Stream([3, 4, 5]).prepend(([0, 1], 2)).to_list() == [[0, 1], 2, 3, 4, 5] - assert Stream([3, 4, 5]).prepend(Stream.of([0, 1], 2)).to_list() == [[0, 1], 2, 3, 4, 5] - - -def test_prepend_dict(): - second_dict = {"x": 3, "y": 4} - first_dict = {"a": 1, "b": 2} - items_list = [ - DictItem(key="a", value=1), - DictItem(key="b", value=2), - DictItem(key="x", value=3), - DictItem(key="y", value=4), - ] - # two streams of dicts - assert Stream(second_dict).prepend(Stream(first_dict)).to_list() == items_list - # dict to stream of dict - assert Stream(second_dict).prepend(first_dict).to_list() == items_list - - assert Stream(second_dict).prepend(first_dict).filter(lambda x: x.value % 2 == 0).map( - lambda x: x.key - ).to_list() == ["b", "y"] - - -# ### flat ### -def test_flat_map(): - assert Stream([[1, 2], [3, 4], [5]]).flat_map(lambda x: Stream(x)).to_list() == [1, 2, 3, 4, 5] - - -def test_flat_map_raises(): - with pytest.raises(TypeError) as e: - assert Stream([[1, 2], 3]).flat_map(lambda x: Stream(x)).to_list() - assert str(e.value) == "'int' object is not iterable" - - -def test_flatten(): - assert Stream([[1, 2], [3, 4], [5]]).flatten().to_list() == [1, 2, 3, 4, 5] - - -def test_flatten_empty(): - assert Stream([[], [1, 2]]).flatten().to_list() == [1, 2] - - -def test_flatten_no_nested_levels(): - assert Stream([1, 2]).flatten().to_list() == [1, 2] - - -def test_flatten_multiple_levels(): - assert Stream([[[1, 2], [3, 4]], [5, 6], [7]]).flatten().to_list() == [1, 2, 3, 4, 5, 6, 7] - - -def test_flatten_strings(): - assert Stream([["abc"], "x", "y", "z"]).flatten().to_list() == ["abc", "x", "y", "z"] - - -# ### ### -def test_distinct(): - assert Stream([1, 1, 2, 2, 2, 3]).distinct().to_list() == [1, 2, 3] - - -def test_count(): - assert Stream([1, 2, 3, 4]).filter(lambda x: x % 2 == 0).count() == 2 - - -def test_count_empty_collection(): - assert Stream([]).count() == 0 - assert Stream.empty().count() == 0 - - -def test_sum(): - assert Stream.of(1, 2, 3, 4).sum() == 10 - - -def test_sum_empty_collection(): - assert Stream([]).sum() == 0 - assert Stream.empty().sum() == 0 - - -def test_sum_non_number_elements(): - with pytest.raises(ValueError) as e: - Stream.of("a", "b").sum() - assert str(e.value) == "Cannot apply sum on non-number elements" - - -def test_sum_on_iterator(): - assert Stream(iter([1, 2, 3, 4])).sum() == 10 - - -def test_sum_with_floats(): - assert Stream.of(1.5, 2.5, 3.0).sum() == 7.0 - - -def test_sum_with_none_values(): - assert Stream.of(1, None, 2, None, 3).sum() == 6 - - -def test_sum_mixed_types_raises(): - with pytest.raises(ValueError) as e: - Stream.of(1, 2, "three", 4).sum() - assert str(e.value) == "Cannot apply sum on non-number elements" - - -def test_sum_all_none(): - assert Stream.of(None, None, None).sum() == 0 - - -def test_average(): - assert Stream.of(1, 2, 3, 4, 5).average() == 3.0 - - -def test_average_empty_collection(): - assert Stream([]).average() == 0 - assert Stream.empty().average() == 0 - - -def test_average_non_number_elements(): - with pytest.raises(ValueError) as e: - Stream.of("a", "b").average() - assert str(e.value) == "Cannot apply average on non-number elements" - - -def test_average_on_iterator(): - assert Stream(iter([1, 2, 3, 4, 5])).average() == 3.0 - - -def test_average_with_floats(): - assert Stream.of(1.0, 2.0, 3.0).average() == 2.0 - - -def test_average_with_none_values(): - assert Stream.of(1, None, 2, None, 3).average() == 2.0 - - -def test_average_mixed_types_raises(): - with pytest.raises(ValueError) as e: - Stream.of(1, 2, "three", 4).average() - assert str(e.value) == "Cannot apply average on non-number elements" - - -def test_average_all_none(): - assert Stream.of(None, None, None).average() == 0 - - -def test_take_while(): - assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").take_while( - lambda x: x[0] == "a" - ).to_list() == ["adam", "aman", "ahmad"] - - -def test_take_while_no_elements(): - assert ( - Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin") - .take_while(lambda x: x[0] == "xyz") - .to_list() - == [] - ) - - -def test_drop_while(): - assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").drop_while( - lambda x: x[0] == "a" - ).to_list() == ["hamid", "muhammad", "aladdin"] - - -def test_drop_while_no_elements(): - assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").drop_while( - lambda x: x[0] == "xyz" - ).to_list() == ["adam", "aman", "ahmad", "hamid", "muhammad", "aladdin"] - - -def test_take_first(): - assert Stream.of(1, 2, 3, 4, 5).take_first().get() == 1 - assert Stream({"a": 1, "b": 2}).take_first().get() == DictItem(key="a", value=1) - - assert Stream.empty().take_first().is_empty() - assert Stream([]).take_first().is_empty() - - assert Stream([]).take_first(default=33).get() == 33 - - -def test_take_last(): - assert Stream.of(1, 2, 3, 4, 5).take_last().get() == 5 - assert Stream({"a": 1, "b": 2}).take_last().get() == DictItem(key="b", value=2) - - assert Stream.empty().take_last().is_empty() - assert Stream([]).take_last().is_empty() - - assert Stream([]).take_last(default=33).get() == 33 - - -def test_take_last_on_iterator(): - assert Stream(iter([1, 2, 3, 4, 5])).take_last().get() == 5 - - -def test_take_last_on_empty_iterator(): - assert Stream(iter([])).take_last().is_empty() - - -def test_take_last_on_empty_iterator_with_default(): - assert Stream(iter([])).take_last(default=99).get() == 99 - - -def test_take_last_on_single_element_iterator(): - assert Stream(iter([42])).take_last().get() == 42 - - -# ### sort ### -def test_sort(): - assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).sort().to_list() == [10, 20, 30, 50] - - -def test_sort_reverse(): - assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).sort(reverse=True).to_list() == [ - 50, - 30, - 20, - 10, - ] - - -def test_sort_comparator_function(): - assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort(itemgetter(1)).to_list() == [ - ("1", 10), - ("2", 20), - ("3", 30), - ("5", 50), - ] - - -def test_sort_multiple_keys(): - assert Stream.of((3, 30), (2, 30), (2, 20), (1, 20), (1, 10)).sort( - lambda x: (x[0], x[1]) - ).to_list() == [ - (1, 10), - (1, 20), - (2, 20), - (2, 30), - (3, 30), - ] - - -def test_sort_comparator_and_reverse(): - assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort( - itemgetter(1), reverse=True - ).to_list() == [ - ("5", 50), - ("3", 30), - ("2", 20), - ("1", 10), - ] + def test_empty_json_from_string(self): + empty_json = "{}" + assert Stream(json.loads(empty_json)).to_tuple() == () + + def test_filter(self): + assert Stream([1, 2, 3, 4, 5, 6]).filter(lambda x: x % 2 == 0).to_list() == [2, 4, 6] + + def test_map(self): + assert Stream([1, 2, 3]).map(str).to_list() == ["1", "2", "3"] + + def test_map_lambda(self): + assert Stream([1, 2, 3]).map(lambda x: x + 5).to_list() == [6, 7, 8] + + def test_map_dict(self): + assert Stream({"x": 1, "y": 2}).map(lambda x: x.key + str(x.value)).to_list() == [ + "x1", + "y2", + ] + + def test_filter_map(self): + assert Stream([None, "foo", "", "bar"]).filter_map(str.upper).to_list() == [ + "FOO", + "", + "BAR", + ] + + def test_filter_map_discard_falsy(self): + assert Stream.of(None, "foo", "", "bar", 0, []).filter_map( + str.upper, discard_falsy=True + ).to_list() == [ + "FOO", + "BAR", + ] + + def test_reduce(self): + assert Stream([1, 2, 3]).reduce(lambda acc, val: acc + val, identity=3).get() == 9 + + def test_reduce_no_identity_provided(self): + assert Stream([1, 2, 3]).reduce(lambda acc, val: acc + val).get() == 6 + + def test_reduce_empty_collection(self): + assert Stream([]).reduce(lambda acc, val: acc + val).is_empty() + + def test_reduce_on_iterator(self): + # NB: iterator has no len() + assert Stream(iter([1, 2, 3, 4])).reduce(lambda acc, val: acc + val).get() == 10 + + def test_reduce_on_iterator_with_identity(self): + assert Stream(iter([1, 2, 3])).reduce(lambda acc, val: acc + val, identity=10).get() == 16 + + def test_reduce_on_empty_iterator(self): + assert Stream(iter([])).reduce(lambda acc, val: acc + val).is_empty() + + def test_reduce_on_empty_iterator_with_identity(self): + assert Stream(iter([])).reduce(lambda acc, val: acc + val, identity=42).get() == 42 + + def test_reduce_on_single_element_iterator(self): + assert Stream(iter([5])).reduce(lambda acc, val: acc + val).get() == 5 + + def test_for_each(self): + f = io.StringIO() + with redirect_stdout(f): + Stream([1, 2, 3, 4]).for_each(lambda x: print(f"{'#' * x} ", end="")) + assert f.getvalue() == "# ## ### #### " + + def test_enumerate(self): + iterable = ["x", "y", "z"] + assert Stream(iterable).enumerate().to_list() == [(0, "x"), (1, "y"), (2, "z")] + assert Stream(iterable).enumerate(start=1).to_list() == [(1, "x"), (2, "y"), (3, "z")] + + def test_peek(self): + f = io.StringIO() + with redirect_stdout(f): + result = ( + Stream([1, 2, 3, 4]) + .filter(lambda x: x > 2) + .peek(lambda x: print(f"{x} ", end="")) + .map(lambda x: x * 20) + .to_list() + ) + assert f.getvalue() == "3 4 " + assert result == [60, 80] -# ### reverse ### -def test_reverse(): - assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).reverse().to_list() == [ - 50, - 30, - 20, - 10, - ] + # ### skip ### + def test_skip(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).skip(7).to_tuple() == (8, 9, 10) + def test_skip_empty(self): + assert Stream.empty().skip(3).to_tuple() == () -def test_reverse_with_custom_comparator(): - assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).reverse( - itemgetter(1) - ).to_list() == [ - ("5", 50), - ("3", 30), - ("2", 20), - ("1", 10), - ] + def test_skip_bigger_than_stream_count(self): + assert Stream([1, 2]).skip(5).to_tuple() == () + def test_skip_negative_count(self): + with pytest.raises(ValueError) as e: + Stream([1, 2]).skip(-5).to_tuple() + assert str(e.value) == "Skip count cannot be negative" -# ### ### -def test_complex_pipeline(): - assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort( - itemgetter(1), reverse=True - ).to_dict(lambda x: (x[0], x[1])) == {"5": 50, "3": 30, "2": 20, "1": 10} + # ### limit ### + def test_limit(self): + assert Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).limit(3).to_tuple() == (1, 2, 3) + def test_limit_empty(self): + assert Stream.empty().limit(3).to_tuple() == () -def test_reusing_stream(): - stream = Stream.of(1, 2, 3) - assert stream._is_consumed is False + def test_limit_bigger_than_stream_count(self): + assert Stream([1, 2]).limit(5).to_tuple() == (1, 2) - result = stream.map(str).to_list() - assert result == ["1", "2", "3"] - assert stream._is_consumed + def test_limit_negative_count(self): + with pytest.raises(ValueError) as e: + Stream([1, 2]).limit(-5).to_tuple() + assert str(e.value) == "Limit count cannot be negative" - with pytest.raises(IllegalStateError) as e: - stream.map(lambda x: x * 10).to_list() - assert str(e.value) == "Stream object already consumed" + def test_head(self): + assert ( + Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).limit(3).to_tuple() + == Stream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).head(3).to_tuple() + ) + def test_head_negative_count(self): + with pytest.raises(ValueError) as e: + Stream([1, 2]).head(-5).to_tuple() + assert str(e.value) == "Head count cannot be negative" + + # ### tail ### + def test_tail(self): + assert Stream.of(1, 2, 3, 4).tail(2).to_list() == [3, 4] + + def test_tail_empty(self): + assert Stream.empty().tail(3).to_tuple() == () + + def test_tail_bigger_than_stream_count(self): + assert Stream([1, 2]).tail(5).to_tuple() == (1, 2) + + def test_tail_negative_count(self): + with pytest.raises(ValueError) as e: + Stream([1, 2]).tail(-5).to_tuple() + assert str(e.value) == "Tail count cannot be negative" + + # ### concat ### + def test_concat(self): + assert Stream.of(1, 2, 3).concat(Stream.of(4, 5, 6)).to_list() == [1, 2, 3, 4, 5, 6] + assert Stream([1, 2, 3]).concat([4, 5]).to_list() == [1, 2, 3, 4, 5] + + def test_concat_empty(self): + assert Stream.empty().concat(Stream.of(1, 2, 3)).to_list() == [1, 2, 3] + assert Stream.concat(Stream.empty(), Stream.empty()).to_list() == [] + + def test_concat_linear_collections(self): + assert Stream((1, 2, 3)).concat([5, 6]).to_list() == [1, 2, 3, 5, 6] + assert Stream([1, 2, 3]).concat([(5, 6), [8]]).to_list() == [1, 2, 3, (5, 6), [8]] + assert Stream([1, 2, 3]).concat([(5, 6), [8]]).flatten().to_list() == [1, 2, 3, 5, 6, 8] + # hacky but works as Stream.of() is passed as self + assert Stream.concat(Stream.of(1, 2, 3)).concat((5, 6), {7}).to_list() == [1, 2, 3, 5, 6, 7] + + def test_concat_dicts_to_stream(self): + first_dict = {"x": 1, "y": 2} + second_dict = {"p": 33, "q": 44, "r": 55} + items_list = [ + DictItem(key="x", value=1), + DictItem(key="y", value=2), + DictItem(key="p", value=33), + DictItem(key="q", value=44), + DictItem(key="r", value=55), + ] + assert Stream.empty().concat(first_dict, second_dict).to_list() == items_list + assert Stream(first_dict).concat(Stream(second_dict)).to_list() == items_list + assert Stream(first_dict).concat(second_dict).to_list() == items_list + + assert Stream.concat(Stream.empty(), second_dict).to_list() == [ + DictItem(key="p", value=33), + DictItem(key="q", value=44), + DictItem(key="r", value=55), + ] + assert Stream(first_dict).concat(Stream(second_dict)).to_dict( + lambda x: (x.key, x.value) + ) == { + "x": 1, + "y": 2, + "p": 33, + "q": 44, + "r": 55, + } + assert Stream(first_dict).concat(Stream(second_dict)).to_dict(lambda x: x) == { + "x": 1, + "y": 2, + "p": 33, + "q": 44, + "r": 55, + } + + def test_concat_raises_non_iterable(self): + with pytest.raises(TypeError) as e: + Stream([1, 2, 3]).concat(5).to_list() + assert str(e.value) == "'int' object is not iterable" + + # ### prepend ### + def test_prepend_collection(self): + assert Stream([2, 3, 4]).prepend([1]).to_list() == [1, 2, 3, 4] + assert Stream([2, 3, 4]).prepend((0, 1)).to_list() == [0, 1, 2, 3, 4] + assert Stream([3, 4, 5]).prepend(([0, 1], 2)).to_list() == [[0, 1], 2, 3, 4, 5] + assert Stream([3, 4, 5]).prepend(Stream.of([0, 1], 2)).to_list() == [[0, 1], 2, 3, 4, 5] + + def test_prepend_dict(self): + second_dict = {"x": 3, "y": 4} + first_dict = {"a": 1, "b": 2} + items_list = [ + DictItem(key="a", value=1), + DictItem(key="b", value=2), + DictItem(key="x", value=3), + DictItem(key="y", value=4), + ] + # two streams of dicts + assert Stream(second_dict).prepend(Stream(first_dict)).to_list() == items_list + # dict to stream of dict + assert Stream(second_dict).prepend(first_dict).to_list() == items_list + + assert Stream(second_dict).prepend(first_dict).filter(lambda x: x.value % 2 == 0).map( + lambda x: x.key + ).to_list() == ["b", "y"] + + # ### flat ### + def test_flat_map(self): + assert Stream([[1, 2], [3, 4], [5]]).flat_map(lambda x: Stream(x)).to_list() == [ + 1, + 2, + 3, + 4, + 5, + ] + + def test_flat_map_raises(self): + with pytest.raises(TypeError) as e: + assert Stream([[1, 2], 3]).flat_map(lambda x: Stream(x)).to_list() + assert str(e.value) == "'int' object is not iterable" + + def test_flatten(self): + assert Stream([[1, 2], [3, 4], [5]]).flatten().to_list() == [1, 2, 3, 4, 5] + + def test_flatten_empty(self): + assert Stream([[], [1, 2]]).flatten().to_list() == [1, 2] + + def test_flatten_no_nested_levels(self): + assert Stream([1, 2]).flatten().to_list() == [1, 2] + + def test_flatten_multiple_levels(self): + assert Stream([[[1, 2], [3, 4]], [5, 6], [7]]).flatten().to_list() == [1, 2, 3, 4, 5, 6, 7] + + def test_flatten_strings(self): + assert Stream([["abc"], "x", "y", "z"]).flatten().to_list() == ["abc", "x", "y", "z"] + + # ### ### + def test_distinct(self): + assert Stream([1, 1, 2, 2, 2, 3]).distinct().to_list() == [1, 2, 3] + + def test_count(self): + assert Stream([1, 2, 3, 4]).filter(lambda x: x % 2 == 0).count() == 2 + + def test_count_empty_collection(self): + assert Stream([]).count() == 0 + assert Stream.empty().count() == 0 + + def test_sum(self): + assert Stream.of(1, 2, 3, 4).sum() == 10 + + def test_sum_empty_collection(self): + assert Stream([]).sum() == 0 + assert Stream.empty().sum() == 0 + + def test_sum_non_number_elements(self): + with pytest.raises(ValueError) as e: + Stream.of("a", "b").sum() + assert str(e.value) == "Cannot apply sum on non-number elements" + + def test_sum_on_iterator(self): + assert Stream(iter([1, 2, 3, 4])).sum() == 10 + + def test_sum_with_floats(self): + assert Stream.of(1.5, 2.5, 3.0).sum() == 7.0 + + def test_sum_with_none_values(self): + assert Stream.of(1, None, 2, None, 3).sum() == 6 + + def test_sum_mixed_types_raises(self): + with pytest.raises(ValueError) as e: + Stream.of(1, 2, "three", 4).sum() + assert str(e.value) == "Cannot apply sum on non-number elements" + + def test_sum_all_none(self): + assert Stream.of(None, None, None).sum() == 0 + + def test_average(self): + assert Stream.of(1, 2, 3, 4, 5).average() == 3.0 + + def test_average_empty_collection(self): + assert Stream([]).average() == 0 + assert Stream.empty().average() == 0 + + def test_average_non_number_elements(self): + with pytest.raises(ValueError) as e: + Stream.of("a", "b").average() + assert str(e.value) == "Cannot apply average on non-number elements" + + def test_average_on_iterator(self): + assert Stream(iter([1, 2, 3, 4, 5])).average() == 3.0 + + def test_average_with_floats(self): + assert Stream.of(1.0, 2.0, 3.0).average() == 2.0 + + def test_average_with_none_values(self): + assert Stream.of(1, None, 2, None, 3).average() == 2.0 -def test_stream_close(): - stream = Stream.of(1, 2, 3) - assert stream._is_consumed is False + def test_average_mixed_types_raises(self): + with pytest.raises(ValueError) as e: + Stream.of(1, 2, "three", 4).average() + assert str(e.value) == "Cannot apply average on non-number elements" - stream.close() - assert stream._is_consumed + def test_average_all_none(self): + assert Stream.of(None, None, None).average() == 0 + def test_take_while(self): + assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").take_while( + lambda x: x[0] == "a" + ).to_list() == ["adam", "aman", "ahmad"] -def test_stream_on_close_callback(): - f = io.StringIO() - with redirect_stdout(f): - result = ( - Stream([1, 2, 3, 4]) - .on_close(lambda: print("It was an honor", end="")) - .peek(lambda x: print(f"{'#' * x} ", end="")) - .map(lambda x: x * 2) + def test_take_while_no_elements(self): + assert ( + Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin") + .take_while(lambda x: x[0] == "xyz") .to_list() + == [] ) - assert result == [2, 4, 6, 8] - assert f.getvalue() == "# ## ### #### It was an honor" - - -def test_stream_on_close_callback_using_pointer_to_self(): - flag = False - - def flip(): - nonlocal flag - flag = True - - result = Stream([1, 2, 3, 4]).on_close(flip).map(lambda x: x * 2).to_list() - assert result == [2, 4, 6, 8] - assert flag is True - - -def test_compare_with(): - assert Stream([1, 2]).compare_with(Stream([1, 2])) - assert Stream([1, 2]).compare_with(Stream([2, 1])) is False - assert Stream([1, 2]).compare_with(Stream([3, 4])) is False - - -def test_compare_with_custom_key(Foo): - fizz = Foo("fizz", 1) - buzz = Foo("buzz", 2) - comparator = lambda x, y: x.num == y.num # noqa - - assert Stream([fizz, buzz]).compare_with(Stream([fizz, buzz]), comparator) - assert Stream([buzz, fizz]).compare_with(Stream([fizz, buzz]), comparator) is False - assert Stream([fizz, buzz]).compare_with(Stream([buzz]), comparator) is False - - -def test_quantify(): - assert Stream([2, 3, 4, 5, 6]).quantify(predicate=lambda x: x % 2 == 0) == 3 - - -def test_quantify_default_predicate(): - assert Stream([None, 1, "", 3, 0]).quantify() == 2 - - -# ### find ### -def test_find_first(): - assert Stream.of(1, 2, 3, 4).filter(lambda x: x % 2 == 0).find_first().get() == 2 - - -def test_find_first_with_predicate(): - assert Stream.of(1, 2, 3, 4).find_first(lambda x: x % 2 == 0).get() == 2 - - -def test_find_first_in_empty_stream(): - result = Stream.empty().find_first() - assert isinstance(result, Optional) - assert result.is_empty() - - -def test_find_any(): - assert Stream.of(1, 2, 3, 4).filter(lambda x: x % 2 == 0).find_any().get() in (2, 4) - - -def test_find_any_with_predicate(): - assert Stream.of(1, 2, 3, 4).find_any(lambda x: x % 2 == 0).get() in (2, 4) - - -def test_find_any_in_empty_stream(): - result = Stream.empty().find_any() - assert isinstance(result, Optional) - assert result.is_empty() - - -# ### match ### -def test_any_match(): - assert Stream.of(1, 2, 3, 4).any_match(lambda x: x > 2) - - -def test_any_match_false(): - assert Stream.of(1, 2, 3, 4).any_match(lambda x: x > 10) is False - - -def test_any_match_empty(): - assert Stream.empty().any_match(lambda x: x > 10) is False - - -def test_all_match(): - assert Stream.of(1, 2, 3, 4).all_match(lambda x: x > 0) - - -def test_all_match_false(): - assert Stream.of(1, 2, 3, 4).all_match(lambda x: x > 10) is False - - -def test_all_match_empty(): - assert Stream.empty().all_match(lambda x: x > 10) - - -def test_none_match(): - assert Stream.of(1, 2, 3, 4).none_match(lambda x: x < 0) - - -def test_none_match_false(): - assert Stream.of(1, 2, 3, 4).none_match(lambda x: x < 10) is False - - -def test_none_match_empty(): - assert Stream.empty().none_match(lambda x: x > 10) - - -def test_none_match_partial_match(): - assert Stream.of(1, 2, 3, 4).none_match(lambda x: x > 2) is False - - -def test_none_match_all_match(): - assert Stream.of(1, 2, 3, 4).none_match(lambda x: x > 0) is False - - -def test_none_match_single_match(): - assert Stream.of(1, 2, 3, 4).none_match(lambda x: x == 4) is False - - -# ### min ### -def test_min(): - assert Stream.of(2, 1, 3, 4).min().get() == 1 - - -def test_min_comparator(): - assert Stream.of("20", "101", "50").min(comparator=int).get() == "20" - - -def test_min_empty(): - result = Stream.empty().min() - assert isinstance(result, Optional) - assert result.is_empty() - - -def test_min_default_value(): - assert Stream.empty().min(default="foo").get() == "foo" - assert Stream.empty().min(default="foo").get() == Stream.empty().min().or_else("foo") - - -def test_min_objects(Foo): - fizz = Foo("fizz", 1) - buzz = Foo("buzz", 2) - coll = [fizz, buzz] - assert Stream(coll).min(lambda x: x.num).get() is fizz - - -# ### max ### -def test_max(): - assert Stream.of(2, 1, 3, 4).max().get() == 4 - - -def test_max_comparator(): - assert Stream.of("20", "101", "50").max(comparator=int).get() == "101" - - -def test_max_empty(): - result = Stream.empty().max() - assert isinstance(result, Optional) - assert result.is_empty() + def test_drop_while(self): + assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").drop_while( + lambda x: x[0] == "a" + ).to_list() == ["hamid", "muhammad", "aladdin"] + + def test_drop_while_no_elements(self): + assert Stream.of("adam", "aman", "ahmad", "hamid", "muhammad", "aladdin").drop_while( + lambda x: x[0] == "xyz" + ).to_list() == ["adam", "aman", "ahmad", "hamid", "muhammad", "aladdin"] + + def test_take_first(self): + assert Stream.of(1, 2, 3, 4, 5).take_first().get() == 1 + assert Stream({"a": 1, "b": 2}).take_first().get() == DictItem(key="a", value=1) + + assert Stream.empty().take_first().is_empty() + assert Stream([]).take_first().is_empty() + + assert Stream([]).take_first(default=33).get() == 33 + + def test_take_last(self): + assert Stream.of(1, 2, 3, 4, 5).take_last().get() == 5 + assert Stream({"a": 1, "b": 2}).take_last().get() == DictItem(key="b", value=2) + + assert Stream.empty().take_last().is_empty() + assert Stream([]).take_last().is_empty() + + assert Stream([]).take_last(default=33).get() == 33 + + def test_take_last_on_iterator(self): + assert Stream(iter([1, 2, 3, 4, 5])).take_last().get() == 5 + + def test_take_last_on_empty_iterator(self): + assert Stream(iter([])).take_last().is_empty() + + def test_take_last_on_empty_iterator_with_default(self): + assert Stream(iter([])).take_last(default=99).get() == 99 + + def test_take_last_on_single_element_iterator(self): + assert Stream(iter([42])).take_last().get() == 42 + + # ### sort ### + def test_sort(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).sort().to_list() == [10, 20, 30, 50] + + def test_sort_reverse(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).sort(reverse=True).to_list() == [ + 50, + 30, + 20, + 10, + ] + + def test_sort_comparator_function(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort( + itemgetter(1) + ).to_list() == [ + ("1", 10), + ("2", 20), + ("3", 30), + ("5", 50), + ] + + def test_sort_multiple_keys(self): + assert Stream.of((3, 30), (2, 30), (2, 20), (1, 20), (1, 10)).sort( + lambda x: (x[0], x[1]) + ).to_list() == [ + (1, 10), + (1, 20), + (2, 20), + (2, 30), + (3, 30), + ] + + def test_sort_comparator_and_reverse(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort( + itemgetter(1), reverse=True + ).to_list() == [ + ("5", 50), + ("3", 30), + ("2", 20), + ("1", 10), + ] + + # ### reverse ### + def test_reverse(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: x * 10).reverse().to_list() == [ + 50, + 30, + 20, + 10, + ] + + def test_reverse_with_custom_comparator(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).reverse( + itemgetter(1) + ).to_list() == [ + ("5", 50), + ("3", 30), + ("2", 20), + ("1", 10), + ] + + # ### ### + def test_complex_pipeline(self): + assert Stream.of(3, 5, 2, 1).map(lambda x: (str(x), x * 10)).sort( + itemgetter(1), reverse=True + ).to_dict(lambda x: (x[0], x[1])) == {"5": 50, "3": 30, "2": 20, "1": 10} + + def test_reusing_stream(self): + stream = Stream.of(1, 2, 3) + assert stream._is_consumed is False + + result = stream.map(str).to_list() + assert result == ["1", "2", "3"] + assert stream._is_consumed + + with pytest.raises(IllegalStateError) as e: + stream.map(lambda x: x * 10).to_list() + assert str(e.value) == "Stream object already consumed" + + def test_stream_close(self): + stream = Stream.of(1, 2, 3) + assert stream._is_consumed is False + + stream.close() + assert stream._is_consumed + + def test_stream_on_close_callback(self): + f = io.StringIO() + with redirect_stdout(f): + result = ( + Stream([1, 2, 3, 4]) + .on_close(lambda: print("It was an honor", end="")) + .peek(lambda x: print(f"{'#' * x} ", end="")) + .map(lambda x: x * 2) + .to_list() + ) + assert result == [2, 4, 6, 8] + assert f.getvalue() == "# ## ### #### It was an honor" -def test_max_default_value(): - assert Stream.empty().max(default="foo").get() == "foo" - assert Stream.empty().max(default="foo").get() == Stream.empty().max().or_else("foo") + def test_stream_on_close_callback_using_pointer_to_self(self): + flag = False + def flip(): + nonlocal flag + flag = True -def test_max_objects(Foo): - fizz = Foo("fizz", 1) - buzz = Foo("buzz", 2) - coll = [fizz, buzz] - assert Stream(coll).max(lambda x: x.num).get() is buzz + result = Stream([1, 2, 3, 4]).on_close(flip).map(lambda x: x * 2).to_list() + assert result == [2, 4, 6, 8] + assert flag is True + def test_compare_with(self): + assert Stream([1, 2]).compare_with(Stream([1, 2])) + assert Stream([1, 2]).compare_with(Stream([2, 1])) is False + assert Stream([1, 2]).compare_with(Stream([3, 4])) is False -# ### collectors ### -def test_to_list(): - result = Stream((1, 2, 3)).to_list() - assert result == [1, 2, 3] - assert type(result) is list + def test_compare_with_custom_key(self, Foo): + fizz = Foo("fizz", 1) + buzz = Foo("buzz", 2) + comparator = lambda x, y: x.num == y.num # noqa + assert Stream([fizz, buzz]).compare_with(Stream([fizz, buzz]), comparator) + assert Stream([buzz, fizz]).compare_with(Stream([fizz, buzz]), comparator) is False + assert Stream([fizz, buzz]).compare_with(Stream([buzz]), comparator) is False -def test_to_tuple(): - result = Stream([1, 2, 3]).to_tuple() - assert result == (1, 2, 3) - assert type(result) is tuple + def test_quantify(self): + assert Stream([2, 3, 4, 5, 6]).quantify(predicate=lambda x: x % 2 == 0) == 3 + def test_quantify_default_predicate(self): + assert Stream([None, 1, "", 3, 0]).quantify() == 2 -def test_to_set(): - result = Stream([1, 2, 2, 3, 3, 3]).to_set() - assert result == {1, 2, 3} - assert type(result) is set + # ### find ### + def test_find_first(self): + assert Stream.of(1, 2, 3, 4).filter(lambda x: x % 2 == 0).find_first().get() == 2 + def test_find_first_with_predicate(self): + assert Stream.of(1, 2, 3, 4).find_first(lambda x: x % 2 == 0).get() == 2 -def test_to_dict(Foo): - coll = [Foo("fizz", 1), Foo("buzz", 2)] - assert Stream(coll).to_dict(lambda x: (x.name, x.num)) == {"fizz": 1, "buzz": 2} + def test_find_first_in_empty_stream(self): + result = Stream.empty().find_first() + assert isinstance(result, Optional) + assert result.is_empty() + def test_find_any(self): + assert Stream.of(1, 2, 3, 4).filter(lambda x: x % 2 == 0).find_any().get() in (2, 4) -def test_to_dict_via_dict_items(Foo): - first_dict = {"x": 1, "y": 2} - second_dict = {"p": 33, "q": 44, "r": None} - assert Stream(first_dict).concat(Stream(second_dict)).to_dict( - lambda x: DictItem(x.key, x.value or 0) - ) == { - "x": 1, - "y": 2, - "p": 33, - "q": 44, - "r": 0, - } + def test_find_any_with_predicate(self): + assert Stream.of(1, 2, 3, 4).find_any(lambda x: x % 2 == 0).get() in (2, 4) - coll = [Foo("jazz", 11), Foo("mambo", 22)] - assert Stream(coll).to_dict(lambda x: DictItem(x.name, x.num)) == {"jazz": 11, "mambo": 22} + def test_find_any_in_empty_stream(self): + result = Stream.empty().find_any() + assert isinstance(result, Optional) + assert result.is_empty() + # ### match ### + def test_any_match(self): + assert Stream.of(1, 2, 3, 4).any_match(lambda x: x > 2) -def test_to_dict_merger(Foo): - coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] - assert Stream(coll).to_dict( - collector=lambda x: (x.name, x.num), merger=lambda old, new: old - ) == { - "fizz": 1, - "buzz": 2, - } + def test_any_match_false(self): + assert Stream.of(1, 2, 3, 4).any_match(lambda x: x > 10) is False + def test_any_match_empty(self): + assert Stream.empty().any_match(lambda x: x > 10) is False -def test_to_dict_duplicate_key_no_merger_raises(Foo): - coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] - with pytest.raises(IllegalStateError) as e: - Stream(coll).to_dict( - collector=lambda x: (x.name, x.num), + def test_all_match(self): + assert Stream.of(1, 2, 3, 4).all_match(lambda x: x > 0) + + def test_all_match_false(self): + assert Stream.of(1, 2, 3, 4).all_match(lambda x: x > 10) is False + + def test_all_match_empty(self): + assert Stream.empty().all_match(lambda x: x > 10) + + def test_none_match(self): + assert Stream.of(1, 2, 3, 4).none_match(lambda x: x < 0) + + def test_none_match_false(self): + assert Stream.of(1, 2, 3, 4).none_match(lambda x: x < 10) is False + + def test_none_match_empty(self): + assert Stream.empty().none_match(lambda x: x > 10) + + def test_none_match_partial_match(self): + assert Stream.of(1, 2, 3, 4).none_match(lambda x: x > 2) is False + + def test_none_match_all_match(self): + assert Stream.of(1, 2, 3, 4).none_match(lambda x: x > 0) is False + + def test_none_match_single_match(self): + assert Stream.of(1, 2, 3, 4).none_match(lambda x: x == 4) is False + + # ### min ### + def test_min(self): + assert Stream.of(2, 1, 3, 4).min().get() == 1 + + def test_min_comparator(self): + assert Stream.of("20", "101", "50").min(comparator=int).get() == "20" + + def test_min_empty(self): + result = Stream.empty().min() + assert isinstance(result, Optional) + assert result.is_empty() + + def test_min_default_value(self): + assert Stream.empty().min(default="foo").get() == "foo" + assert Stream.empty().min(default="foo").get() == Stream.empty().min().or_else("foo") + + def test_min_objects(self, Foo): + fizz = Foo("fizz", 1) + buzz = Foo("buzz", 2) + coll = [fizz, buzz] + assert Stream(coll).min(lambda x: x.num).get() is fizz + + # ### max ### + def test_max(self): + assert Stream.of(2, 1, 3, 4).max().get() == 4 + + def test_max_comparator(self): + assert Stream.of("20", "101", "50").max(comparator=int).get() == "101" + + def test_max_empty(self): + result = Stream.empty().max() + assert isinstance(result, Optional) + assert result.is_empty() + + def test_max_default_value(self): + assert Stream.empty().max(default="foo").get() == "foo" + assert Stream.empty().max(default="foo").get() == Stream.empty().max().or_else("foo") + + def test_max_objects(self, Foo): + fizz = Foo("fizz", 1) + buzz = Foo("buzz", 2) + coll = [fizz, buzz] + assert Stream(coll).max(lambda x: x.num).get() is buzz + + # ### collectors ### + def test_to_list(self): + result = Stream((1, 2, 3)).to_list() + assert result == [1, 2, 3] + assert type(result) is list + + def test_to_tuple(self): + result = Stream([1, 2, 3]).to_tuple() + assert result == (1, 2, 3) + assert type(result) is tuple + + def test_to_set(self): + result = Stream([1, 2, 2, 3, 3, 3]).to_set() + assert result == {1, 2, 3} + assert type(result) is set + + def test_to_dict(self, Foo): + coll = [Foo("fizz", 1), Foo("buzz", 2)] + assert Stream(coll).to_dict(lambda x: (x.name, x.num)) == {"fizz": 1, "buzz": 2} + + def test_to_dict_via_dict_items(self, Foo): + first_dict = {"x": 1, "y": 2} + second_dict = {"p": 33, "q": 44, "r": None} + assert Stream(first_dict).concat(Stream(second_dict)).to_dict( + lambda x: DictItem(x.key, x.value or 0) + ) == { + "x": 1, + "y": 2, + "p": 33, + "q": 44, + "r": 0, + } + + coll = [Foo("jazz", 11), Foo("mambo", 22)] + assert Stream(coll).to_dict(lambda x: DictItem(x.name, x.num)) == {"jazz": 11, "mambo": 22} + + def test_to_dict_merger(self, Foo): + coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] + assert Stream(coll).to_dict( + collector=lambda x: (x.name, x.num), merger=lambda old, new: old + ) == { + "fizz": 1, + "buzz": 2, + } + + def test_to_dict_duplicate_key_no_merger_raises(self, Foo): + coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] + with pytest.raises(IllegalStateError) as e: + Stream(coll).to_dict( + collector=lambda x: (x.name, x.num), + ) + assert str(e.value) == "Key 'fizz' already exists" + + def test_to_dict_raises(self): + with pytest.raises(UnsupportedTypeError, match="Cannot create dict items from 'int' type"): + Stream.of(("x", 1), ("b", 2)).to_dict(lambda x: x[1]) + + def test_collect(self): + assert Stream([1, 2, 3]).collect(tuple) == (1, 2, 3) + assert Stream.of(1, 2, 3).collect(list) == [1, 2, 3] + assert Stream.of(1, 1, 2, 2, 2, 3).collect(set) == {1, 2, 3} + assert Stream.of(1, 2, 3, 4).collect(dict, lambda x: (str(x), x * 10)) == { + "1": 10, + "2": 20, + "3": 30, + "4": 40, + } + assert Stream([1, 2, 3, 4, 5]).collect(str) == "1, 2, 3, 4, 5" + assert Stream([1, 2, 3, 4, 5]).collect(str, str_delimiter=" | ") == "1 | 2 | 3 | 4 | 5" + assert Stream(["x", "y", "z"]).collect(str, str_delimiter="") == "xyz" + + def test_to_dict_no_collector(self, Foo): + first_dict = {"x": 1, "y": 2} + second_dict = {"p": 33, "q": 44, "r": None} + assert Stream(first_dict).concat(Stream(second_dict)).collect(dict) == { + "x": 1, + "y": 2, + "p": 33, + "q": 44, + "r": None, + } + + coll = [Foo("jazz", 11), Foo("mambo", 22)] + assert Stream(coll).to_dict(lambda x: DictItem(x.name, x.num)) == {"jazz": 11, "mambo": 22} + + def test_collect_to_dict_raises(self, Foo): + coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] + with pytest.raises(UnsupportedTypeError) as e: + Stream(coll).collect(dict) + assert str(e.value) == "Cannot create dict items from 'Foo' type" + + with pytest.raises(UnsupportedTypeError) as e: + Stream(coll).collect(dict, lambda x: "invalid") + assert str(e.value) == "Cannot create dict items from 'str' type" + + def test_collect_invalid_type(self, Foo): + coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] + with pytest.raises(ValueError) as e: + Stream(coll).collect(33) + assert str(e.value) == "Invalid collection type" + + def test_group_by(self): + assert Stream("AAAABBBCCD").group_by() == { + "A": ["A", "A", "A", "A"], + "B": ["B", "B", "B"], + "C": ["C", "C"], + "D": ["D"], + } + + def test_group_by_custom_collector(self): + assert Stream("AAAABBBCCD").group_by(collector=lambda k, g: (k, len(g))) == { + "A": 4, + "B": 3, + "C": 2, + "D": 1, + } + + def test_group_by_objects(self, Foo): + coll = [ + Foo("fizz", 1), + Foo("fizz", 2), + Foo("fizz", 3), + Foo("buzz", 2), + Foo("buzz", 3), + Foo("buzz", 4), + Foo("buzz", 5), + ] + + assert Stream(coll).group_by( + classifier=lambda obj: obj.name, + collector=lambda k, g: (k, [(obj.name, obj.num) for obj in list(g)]), + ) == { + "fizz": [("fizz", 1), ("fizz", 2), ("fizz", 3)], + "buzz": [("buzz", 2), ("buzz", 3), ("buzz", 4), ("buzz", 5)], + } + + def test_group_by_empty(self): + assert Stream.empty().group_by() == {} + assert Stream([]).group_by(classifier=lambda x: x) == {} + + def test_group_by_unconsumed_groups(self): + stream = Stream("AAABBB") + keys = [key for key, group in stream._group_by()] + assert keys == ["A", "B"] + + def test_to_string(self, nested_json): + assert Stream([1, (2, 3), {4, 5, 6}]).to_string() == "1, (2, 3), {4, 5, 6}" + assert ( + Stream({"a": 1, "b": [2, 3]}).map(lambda x: {x.key: x.value}).to_string(delimiter=" | ") + == "{'a': 1} | {'b': [2, 3]}" + ) + assert Stream(["x", "y", "z"]).to_string(delimiter="") == "xyz" + assert ( + Stream(json.loads(nested_json)).collect(str) + == Stream(json.loads(nested_json)).to_string() ) - assert str(e.value) == "Key 'fizz' already exists" - - -def test_to_dict_raises(): - with pytest.raises(UnsupportedTypeError, match="Cannot create dict items from 'int' type"): - Stream.of(("x", 1), ("b", 2)).to_dict(lambda x: x[1]) - - -def test_collect(): - assert Stream([1, 2, 3]).collect(tuple) == (1, 2, 3) - assert Stream.of(1, 2, 3).collect(list) == [1, 2, 3] - assert Stream.of(1, 1, 2, 2, 2, 3).collect(set) == {1, 2, 3} - assert Stream.of(1, 2, 3, 4).collect(dict, lambda x: (str(x), x * 10)) == { - "1": 10, - "2": 20, - "3": 30, - "4": 40, - } - assert Stream([1, 2, 3, 4, 5]).collect(str) == "1, 2, 3, 4, 5" - assert Stream([1, 2, 3, 4, 5]).collect(str, str_delimiter=" | ") == "1 | 2 | 3 | 4 | 5" - assert Stream(["x", "y", "z"]).collect(str, str_delimiter="") == "xyz" - - -def test_to_dict_no_collector(Foo): - first_dict = {"x": 1, "y": 2} - second_dict = {"p": 33, "q": 44, "r": None} - assert Stream(first_dict).concat(Stream(second_dict)).collect(dict) == { - "x": 1, - "y": 2, - "p": 33, - "q": 44, - "r": None, - } - - coll = [Foo("jazz", 11), Foo("mambo", 22)] - assert Stream(coll).to_dict(lambda x: DictItem(x.name, x.num)) == {"jazz": 11, "mambo": 22} - - -def test_collect_to_dict_raises(Foo): - coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] - with pytest.raises(UnsupportedTypeError) as e: - Stream(coll).collect(dict) - assert str(e.value) == "Cannot create dict items from 'Foo' type" - - with pytest.raises(UnsupportedTypeError) as e: - Stream(coll).collect(dict, lambda x: "invalid") - assert str(e.value) == "Cannot create dict items from 'str' type" - - -def test_collect_invalid_type(Foo): - coll = [Foo("fizz", 1), Foo("fizz", 2), Foo("buzz", 2)] - with pytest.raises(ValueError) as e: - Stream(coll).collect(33) - assert str(e.value) == "Invalid collection type" - - -def test_group_by(): - assert Stream("AAAABBBCCD").group_by() == { - "A": ["A", "A", "A", "A"], - "B": ["B", "B", "B"], - "C": ["C", "C"], - "D": ["D"], - } - - -def test_group_by_custom_collector(): - assert Stream("AAAABBBCCD").group_by(collector=lambda k, g: (k, len(g))) == { - "A": 4, - "B": 3, - "C": 2, - "D": 1, - } - - -def test_group_by_objects(Foo): - coll = [ - Foo("fizz", 1), - Foo("fizz", 2), - Foo("fizz", 3), - Foo("buzz", 2), - Foo("buzz", 3), - Foo("buzz", 4), - Foo("buzz", 5), - ] - - assert Stream(coll).group_by( - classifier=lambda obj: obj.name, - collector=lambda k, g: (k, [(obj.name, obj.num) for obj in list(g)]), - ) == { - "fizz": [("fizz", 1), ("fizz", 2), ("fizz", 3)], - "buzz": [("buzz", 2), ("buzz", 3), ("buzz", 4), ("buzz", 5)], - } - - -def test_group_by_empty(): - assert Stream.empty().group_by() == {} - assert Stream([]).group_by(classifier=lambda x: x) == {} - - -def test_group_by_unconsumed_groups(): - stream = Stream("AAABBB") - keys = [key for key, group in stream._group_by()] - assert keys == ["A", "B"] - - -def test_to_string(nested_json): - assert Stream([1, (2, 3), {4, 5, 6}]).to_string() == "1, (2, 3), {4, 5, 6}" - assert ( - Stream({"a": 1, "b": [2, 3]}).map(lambda x: {x.key: x.value}).to_string(delimiter=" | ") - == "{'a': 1} | {'b': [2, 3]}" - ) - assert Stream(["x", "y", "z"]).to_string(delimiter="") == "xyz" - assert ( - Stream(json.loads(nested_json)).collect(str) == Stream(json.loads(nested_json)).to_string() - ) - - -def test_repr(nested_json): - assert str(Stream.of(1, 2, 3)) == "Stream.of(1, 2, 3)" - assert str(Stream([1, 2, 3])) == "Stream.of(1, 2, 3)" - assert ( - str(Stream({"a": 1, "b": [2, 3], "c": {"x": "yz"}})) - == "Stream.of(DictItem(key='a', value=1), DictItem(key='b', value=[2, 3]), DictItem(key='c', value=(DictItem(key='x', value='yz'),)))" - ) - assert str(Stream(json.loads(nested_json))) == ( - "Stream.of(" - "DictItem(key='user', value=(DictItem(key='Name', value='John'), DictItem(key='Phone', value='555-123-4568'), DictItem(key='Security Number', value='3450678'))), " - "DictItem(key='super_user', value=(DictItem(key='Name', value='sudo'), DictItem(key='Email', value='admin@sudo.su'), DictItem(key='Some Other Number', value='000-0011'))), " - "DictItem(key='fraud', value=(DictItem(key='Name', value='Freud'), DictItem(key='Email', value='ziggy@psycho.au'))))" - ) + def test_repr(self, nested_json): + assert str(Stream.of(1, 2, 3)) == "Stream.of(1, 2, 3)" + assert str(Stream([1, 2, 3])) == "Stream.of(1, 2, 3)" + assert ( + str(Stream({"a": 1, "b": [2, 3], "c": {"x": "yz"}})) + == "Stream.of(DictItem(key='a', value=1), DictItem(key='b', value=[2, 3]), DictItem(key='c', value=(DictItem(key='x', value='yz'),)))" + ) + assert str(Stream(json.loads(nested_json))) == ( + "Stream.of(" + "DictItem(key='user', value=(DictItem(key='Name', value='John'), DictItem(key='Phone', value='555-123-4568'), DictItem(key='Security Number', value='3450678'))), " + "DictItem(key='super_user', value=(DictItem(key='Name', value='sudo'), DictItem(key='Email', value='admin@sudo.su'), DictItem(key='Some Other Number', value='000-0011'))), " + "DictItem(key='fraud', value=(DictItem(key='Name', value='Freud'), DictItem(key='Email', value='ziggy@psycho.au'))))" + ) -# ### nested streams ### -def test_nested_json_from_string(nested_json): - assert ( - Stream(json.loads(nested_json)) - .filter(lambda outer: "user" in outer.key) - .flat_map( - lambda outer: ( - Stream(outer.value) - .filter(lambda inner: len(inner.key) < 6) - .map(lambda inner: inner.value) - .to_list() + # ### nested streams ### + def test_nested_json_from_string(self, nested_json): + assert ( + Stream(json.loads(nested_json)) + .filter(lambda outer: "user" in outer.key) + .flat_map( + lambda outer: ( + Stream(outer.value) + .filter(lambda inner: len(inner.key) < 6) + .map(lambda inner: inner.value) + .to_list() + ) ) + .to_tuple() + ) == ("John", "555-123-4568", "sudo", "admin@sudo.su") + + def test_nested_json_querying_nested_dict_items(self, nested_json): + assert ( + Stream(json.loads(nested_json)) + .flat_map(lambda x: x.value) + .filter(lambda x: x.key == "Name" and "F" in x.value) + .map(lambda x: x.value) + .find_first() + .get() + == "Freud" ) - .to_tuple() - ) == ("John", "555-123-4568", "sudo", "admin@sudo.su") - - -def test_nested_json_querying_nested_dict_items(nested_json): - assert ( - Stream(json.loads(nested_json)) - .flat_map(lambda x: x.value) - .filter(lambda x: x.key == "Name" and "F" in x.value) - .map(lambda x: x.value) - .find_first() - .get() - == "Freud" - ) # ### hacker-rank ### From e9a42712edb1575b80cb09c68dae16e8bceffe04 Mon Sep 17 00:00:00 2001 From: kaliv0 Date: Mon, 26 Jan 2026 22:54:59 +0200 Subject: [PATCH 2/3] added support for .yml alias --- pyproject.toml | 1 + pyrio/streams/file_stream.py | 102 ++++++++++-------- tests/resources/foo.yml | 2 + tests/resources/save_output/test.yml | 13 +++ .../save_output/test_null_handler.yml | 13 +++ tests/test_file_stream.py | 4 +- 6 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 tests/resources/foo.yml create mode 100644 tests/resources/save_output/test.yml create mode 100644 tests/resources/save_output/test_null_handler.yml diff --git a/pyproject.toml b/pyproject.toml index f2de10a..2cd11a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ license-files = ["LICENSE"] [project.optional-dependencies] fs = [ + "aldict>=1.1.1", "pyyaml>=6.0.2", "tomli-w>=1.1.0", "xmltodict>=0.14.2", diff --git a/pyrio/streams/file_stream.py b/pyrio/streams/file_stream.py index 777163b..4f7ec9d 100644 --- a/pyrio/streams/file_stream.py +++ b/pyrio/streams/file_stream.py @@ -3,60 +3,70 @@ from contextlib import contextmanager from pathlib import Path +from aldict import AliasDict + from pyrio.utils import DictItem, Mappable from pyrio.streams import BaseStream, Stream from pyrio.exceptions import NoneTypeError TEMP_PATH = "{file_path}.tmp" DSV_TYPES = {".csv", ".tsv"} -MAPPING_READ_CONFIG = { - ".toml": { - "import_mod": "tomllib", - "callable": "load", - "read_mode": "rb", - }, - ".json": { - "import_mod": "json", - "callable": "load", - "read_mode": "r", - }, - ".yaml": { - "import_mod": "yaml", - "callable": "safe_load", - "read_mode": "r", - }, - ".xml": { - "import_mod": "xmltodict", - "callable": "parse", - "read_mode": "rb", - }, -} -MAPPING_WRITE_CONFIG = { - ".toml": { - "import_mod": "tomli_w", - "callable": "dump", - "write_mode": "wb", - "default_null_handler": lambda x: DictItem(x.key, "N/A") if x.value is None else x, - }, - ".json": { - "import_mod": "json", - "callable": "dump", - "write_mode": "w", - "default_null_handler": None, - }, - ".yaml": { - "import_mod": "yaml", - "callable": "dump", - "write_mode": "w", - "default_null_handler": None, + +MAPPING_READ_CONFIG = AliasDict( + { + ".toml": { + "import_mod": "tomllib", + "callable": "load", + "read_mode": "rb", + }, + ".json": { + "import_mod": "json", + "callable": "load", + "read_mode": "r", + }, + ".yaml": { + "import_mod": "yaml", + "callable": "safe_load", + "read_mode": "r", + }, + ".xml": { + "import_mod": "xmltodict", + "callable": "parse", + "read_mode": "rb", + }, }, - ".xml": { - "import_mod": "xmltodict", - "callable": "unparse", - "write_mode": "w", - "default_null_handler": None, + aliases={".yaml": ".yml"}, +) + +MAPPING_WRITE_CONFIG = AliasDict( + { + ".toml": { + "import_mod": "tomli_w", + "callable": "dump", + "write_mode": "wb", + "default_null_handler": lambda x: DictItem(x.key, "N/A") if x.value is None else x, + }, + ".json": { + "import_mod": "json", + "callable": "dump", + "write_mode": "w", + "default_null_handler": None, + }, + ".yaml": { + "import_mod": "yaml", + "callable": "dump", + "write_mode": "w", + "default_null_handler": None, + }, + ".xml": { + "import_mod": "xmltodict", + "callable": "unparse", + "write_mode": "w", + "default_null_handler": None, + }, }, -} + aliases={".yaml": ".yml"}, +) class FileStream(BaseStream): diff --git a/tests/resources/foo.yml b/tests/resources/foo.yml new file mode 100644 index 0000000..0ecb85d --- /dev/null +++ b/tests/resources/foo.yml @@ -0,0 +1,2 @@ +abc: xyz +qwerty: 42 diff --git a/tests/resources/save_output/test.yml b/tests/resources/save_output/test.yml new file mode 100644 index 0000000..e6963b7 --- /dev/null +++ b/tests/resources/save_output/test.yml @@ -0,0 +1,13 @@ +Email: + primary: jen123@gmail.com +Job: null +Name: Jennifer Smith +Phone: 555-123-4568 +first: + second: + - 1 + - 2 + - 3 + - 4 + still-second: + third: 42 diff --git a/tests/resources/save_output/test_null_handler.yml b/tests/resources/save_output/test_null_handler.yml new file mode 100644 index 0000000..d64b32d --- /dev/null +++ b/tests/resources/save_output/test_null_handler.yml @@ -0,0 +1,13 @@ +Email: + primary: jen123@gmail.com +Job: Unknown +Name: Jennifer Smith +Phone: 555-123-4568 +first: + second: + - 1 + - 2 + - 3 + - 4 + still-second: + third: 42 diff --git a/tests/test_file_stream.py b/tests/test_file_stream.py index 3f441a5..682e801 100644 --- a/tests/test_file_stream.py +++ b/tests/test_file_stream.py @@ -44,6 +44,7 @@ def test_path_is_dir_error(self): "./tests/resources/foo.json", "./tests/resources/foo.toml", "./tests/resources/foo.yaml", + "./tests/resources/foo.yml", "./tests/resources/foo.xml", ], ) @@ -243,7 +244,7 @@ def test_save_toml_default_null_handler(self, tmp_file_dir, json_dict): @pytest.mark.parametrize( "file_path, indent", - [("test.json", 2), ("test.yaml", 2), ("test.xml", 4)], + [("test.json", 2), ("test.yaml", 2), ("test.yml", 2), ("test.xml", 4)], ) def test_save(self, tmp_file_dir, file_path, indent, json_dict): in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() @@ -262,6 +263,7 @@ def test_save(self, tmp_file_dir, file_path, indent, json_dict): [ ("test_null_handler.json", 2), ("test_null_handler.yaml", 2), + ("test_null_handler.yml", 2), ("test_null_handler.xml", 4), ], ) From 02bbf8f674cc1f40725f872c251c365b890a1107 Mon Sep 17 00:00:00 2001 From: kaliv0 Date: Tue, 27 Jan 2026 13:06:28 +0200 Subject: [PATCH 3/3] refactored code --- README.md | 36 +++++++++ docs/quickstart.md | 36 +++++++++ pyrio/__init__.py | 40 +++++----- pyrio/streams/file_stream.py | 20 +++-- pyrio/utils/__init__.py | 20 ++--- pyrio/utils/file_options.py | 58 +++++++-------- tests/test_dict_item.py | 16 ++-- tests/test_file_options.py | 140 ++++++++++++++++------------------- tests/test_file_stream.py | 85 ++++++++++----------- tests/test_stream.py | 62 ++++++++-------- 10 files changed, 284 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index 4c9abcf..dd517ec 100644 --- a/README.md +++ b/README.md @@ -585,6 +585,42 @@ FileStream("path/to/file.json").concat(in_memory_dict).save( ) ``` +-------------------------------------------- +NB: instead of passing file_options as dict you can use provided custom classes for different supported file types +```python +FileStream("./tests/resources/plain.txt").map(lambda x: x.strip()).head(2).save( + file_path="path/to/file.txt", + f_write_options=TextWriteOpts.with_(header="---START---\n", footer="\n---END---"), +) + +# ---START--- +# Lorem ipsum dolor sit amet, consectetur adipisicing elit, +# sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +# ---END--- +``` + +```python +FileStream("./tests/resources/nested.md").save( + file_path="path/to/output.yaml", + f_open_options=FileOpts.utf8(), + f_write_options=YamlWriteOpts.block_style(), +) + +# Email: +# primary: jen123@gmail.com +# Job: null +# Name: Jennifer Smith +# Phone: 555-123-4568 +# first: +# second: +# - 1 +# - 2 +# - 3 +# - 4 +# still-second: +# third: 42 +``` + -------------------------------------------- ### How far can we actually push it? ```python diff --git a/docs/quickstart.md b/docs/quickstart.md index 7decde4..a28f078 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -566,6 +566,42 @@ FileStream("path/to/file.json").concat(in_memory_dict).save( ) ``` +-------------------------------------------- +NB: instead of passing file_options as dict you can use provided custom classes for the different supported file types +```python +FileStream("./tests/resources/plain.txt").map(lambda x: x.strip()).head(2).save( + file_path="path/to/file.txt", + f_write_options=TextWriteOpts.with_(header="---START---\n", footer="\n---END---"), +) + +# ---START--- +# Lorem ipsum dolor sit amet, consectetur adipisicing elit, +# sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +# ---END--- +``` + +```python +FileStream("./tests/resources/nested.md").save( + file_path="path/to/output.yaml", + f_open_options=FileOpts.utf8(), + f_write_options=YamlWriteOpts.block_style(), +) + +# Email: +# primary: jen123@gmail.com +# Job: null +# Name: Jennifer Smith +# Phone: 555-123-4568 +# first: +# second: +# - 1 +# - 2 +# - 3 +# - 4 +# still-second: +# third: 42 +``` + -------------------------------------------- ### How far can we actually push it? ```python diff --git a/pyrio/__init__.py b/pyrio/__init__.py index 1a81a7a..ce4b4ef 100644 --- a/pyrio/__init__.py +++ b/pyrio/__init__.py @@ -3,16 +3,16 @@ from .utils.optional import Optional as Optional from .utils.dict_item import DictItem as DictItem from .utils.file_options import ( - FileOptions as FileOptions, - CsvReadOptions as CsvReadOptions, - CsvWriteOptions as CsvWriteOptions, - JsonReadOptions as JsonReadOptions, - JsonWriteOptions as JsonWriteOptions, - YamlReadOptions as YamlReadOptions, - YamlWriteOptions as YamlWriteOptions, - XmlReadOptions as XmlReadOptions, - XmlWriteOptions as XmlWriteOptions, - PlainTextWriteOptions as PlainTextWriteOptions, + FileOpts as FileOpts, + CsvReadOpts as CsvReadOpts, + CsvWriteOpts as CsvWriteOpts, + JsonReadOpts as JsonReadOpts, + JsonWriteOpts as JsonWriteOpts, + YamlReadOpts as YamlReadOpts, + YamlWriteOpts as YamlWriteOpts, + XmlReadOpts as XmlReadOpts, + XmlWriteOpts as XmlWriteOpts, + TextWriteOpts as TextWriteOpts, ) __all__ = [ @@ -20,14 +20,14 @@ "FileStream", "Optional", "DictItem", - "FileOptions", - "CsvReadOptions", - "CsvWriteOptions", - "JsonReadOptions", - "JsonWriteOptions", - "YamlReadOptions", - "YamlWriteOptions", - "XmlReadOptions", - "XmlWriteOptions", - "PlainTextWriteOptions", + "FileOpts", + "CsvReadOpts", + "CsvWriteOpts", + "JsonReadOpts", + "JsonWriteOpts", + "YamlReadOpts", + "YamlWriteOpts", + "XmlReadOpts", + "XmlWriteOpts", + "TextWriteOpts", ] diff --git a/pyrio/streams/file_stream.py b/pyrio/streams/file_stream.py index 4f7ec9d..be941be 100644 --- a/pyrio/streams/file_stream.py +++ b/pyrio/streams/file_stream.py @@ -10,7 +10,15 @@ from pyrio.exceptions import NoneTypeError TEMP_PATH = "{file_path}.tmp" -DSV_TYPES = {".csv", ".tsv"} + +DSV_CONFIG = { + ".csv": { + "delimiter": ",", + }, + ".tsv": { + "delimiter": "\t", + }, +} MAPPING_READ_CONFIG = AliasDict( { @@ -111,7 +119,7 @@ def _read_file(cls, file_path, f_open_options=None, f_read_options=None, **kwarg f_open_options = cls._normalize_options(f_open_options) f_read_options = cls._normalize_options(f_read_options) - if (suffix := path.suffix) in DSV_TYPES: + if (suffix := path.suffix) in DSV_CONFIG: return cls._read_dsv(path, f_open_options, f_read_options) elif suffix in MAPPING_READ_CONFIG: return cls._read_mapping(path, f_open_options, f_read_options, **kwargs) @@ -125,7 +133,7 @@ def _read_dsv(path, f_open_options, f_read_options): FileStream._prepare_io_options( [ (f_open_options, "newline", ""), - (f_read_options, "delimiter", "\t" if path.suffix == ".tsv" else ","), + (f_read_options, "delimiter", DSV_CONFIG[path.suffix]["delimiter"]), ] ) file_handler = open(path, **f_open_options) @@ -167,9 +175,9 @@ def save( f_open_options = self._normalize_options(f_open_options) f_write_options = self._normalize_options(f_write_options) - if (suffix := path.suffix) in DSV_TYPES: + if (suffix := path.suffix) in DSV_CONFIG: return self._write_dsv(path, tmp_path, f_open_options, f_write_options, null_handler) - elif suffix in MAPPING_READ_CONFIG: + elif suffix in MAPPING_WRITE_CONFIG: return self._write_mapping( path, tmp_path, f_open_options, f_write_options, null_handler, **kwargs ) @@ -186,7 +194,7 @@ def _write_dsv(self, path, tmp_path, f_open_options, f_write_options, null_handl self._prepare_io_options( [ (f_open_options, "mode", "w"), - (f_write_options, "delimiter", "\t" if path.suffix == ".tsv" else ","), + (f_write_options, "delimiter", DSV_CONFIG[path.suffix]["delimiter"]), (f_write_options, "fieldnames", output[0].keys() if output else ()), ] ) diff --git a/pyrio/utils/__init__.py b/pyrio/utils/__init__.py index c39ec12..8b9ad2d 100644 --- a/pyrio/utils/__init__.py +++ b/pyrio/utils/__init__.py @@ -2,14 +2,14 @@ from .optional import Optional as Optional from .file_options import ( Mappable as Mappable, - FileOptions as FileOptions, - CsvReadOptions as CsvReadOptions, - CsvWriteOptions as CsvWriteOptions, - JsonReadOptions as JsonReadOptions, - JsonWriteOptions as JsonWriteOptions, - YamlReadOptions as YamlReadOptions, - YamlWriteOptions as YamlWriteOptions, - XmlReadOptions as XmlReadOptions, - XmlWriteOptions as XmlWriteOptions, - PlainTextWriteOptions as PlainTextWriteOptions, + FileOpts as FileOpts, + CsvReadOpts as CsvReadOpts, + CsvWriteOpts as CsvWriteOpts, + JsonReadOpts as JsonReadOpts, + JsonWriteOpts as JsonWriteOpts, + YamlReadOpts as YamlReadOpts, + YamlWriteOpts as YamlWriteOpts, + XmlReadOpts as XmlReadOpts, + XmlWriteOpts as XmlWriteOpts, + TextWriteOpts as TextWriteOpts, ) diff --git a/pyrio/utils/file_options.py b/pyrio/utils/file_options.py index 4876da7..2c00f01 100644 --- a/pyrio/utils/file_options.py +++ b/pyrio/utils/file_options.py @@ -1,7 +1,4 @@ -from abc import ABC - - -class Mappable(ABC): +class Mappable: def to_dict(self): """Returns dict with only non-None values""" return {attr: val for attr, val in vars(self).items() if val is not None} @@ -10,7 +7,7 @@ def __repr__(self): return f"{self.__class__.__name__}({', '.join(f'{attr}={val!r}' for attr, val in vars(self).items() if val is not None)})" -class FileOptions(Mappable): +class FileOpts(Mappable): """Options for file opening - applies to all file formats""" def __init__(self, encoding=None, errors=None, newline=None, buffering=None, mode=None): @@ -23,21 +20,16 @@ def __init__(self, encoding=None, errors=None, newline=None, buffering=None, mod @staticmethod def utf8(errors=None): """Creates FileOptions with UTF-8 encoding""" - return FileOptions(encoding="utf-8", errors=errors) + return FileOpts(encoding="utf-8", errors=errors) @staticmethod def ascii(errors=None): """Creates FileOptions with ASCII encoding""" - return FileOptions(encoding="ascii", errors=errors) - - @staticmethod - def append(encoding=None): - """Creates FileOptions for append mode""" - return FileOptions(mode="a", encoding=encoding) + return FileOpts(encoding="ascii", errors=errors) # CSV -class CsvReadOptions(Mappable): +class CsvReadOpts(Mappable): """Options for reading CSV files""" def __init__( @@ -67,15 +59,15 @@ def __init__( @staticmethod def excel(): """Creates CsvReadOptions for Excel CSV dialect""" - return CsvReadOptions(dialect="excel") + return CsvReadOpts(dialect="excel") @staticmethod def unix(): """Creates CsvReadOptions for Unix CSV dialect""" - return CsvReadOptions(dialect="unix") + return CsvReadOpts(dialect="unix") -class CsvWriteOptions(Mappable): +class CsvWriteOpts(Mappable): """Options for writing CSV files""" def __init__( @@ -107,7 +99,7 @@ def __init__( self.extrasaction = extrasaction -class JsonReadOptions(Mappable): +class JsonReadOpts(Mappable): """Options for reading JSON files""" def __init__( @@ -129,10 +121,10 @@ def with_decimal(): """Creates JsonReadOptions that parses floats as Decimal""" from decimal import Decimal - return JsonReadOptions(parse_float=Decimal) + return JsonReadOpts(parse_float=Decimal) -class JsonWriteOptions(Mappable): +class JsonWriteOpts(Mappable): """Options for writing JSON files""" def __init__( @@ -158,27 +150,27 @@ def __init__( @staticmethod def pretty(indent=2): """Creates JsonWriteOptions for pretty-printed output""" - return JsonWriteOptions(indent=indent) + return JsonWriteOpts(indent=indent) @staticmethod def compact(): """Creates JsonWriteOptions for compact output with minimal whitespace""" - return JsonWriteOptions(separators=(",", ":")) + return JsonWriteOpts(separators=(",", ":")) @staticmethod def sorted(indent=2): """Creates JsonWriteOptions with sorted keys and pretty-printing""" - return JsonWriteOptions(indent=indent, sort_keys=True) + return JsonWriteOpts(indent=indent, sort_keys=True) -class YamlReadOptions(Mappable): +class YamlReadOpts(Mappable): """Options for reading YAML files""" def __init__(self, loader=None): # noqa self.loader = loader -class YamlWriteOptions(Mappable): +class YamlWriteOpts(Mappable): """Options for writing YAML files""" def __init__( @@ -196,15 +188,15 @@ def __init__( @staticmethod def block_style(indent=2): """Creates YamlWriteOptions for block-style output""" - return YamlWriteOptions(default_flow_style=False, indent=indent) + return YamlWriteOpts(default_flow_style=False, indent=indent) @staticmethod def flow_style(): """Creates YamlWriteOptions for flow-style (inline) output""" - return YamlWriteOptions(default_flow_style=True) + return YamlWriteOpts(default_flow_style=True) -class XmlReadOptions(Mappable): +class XmlReadOpts(Mappable): """Options for reading XML files""" def __init__( @@ -220,7 +212,7 @@ def __init__( self.cdata_key = cdata_key -class XmlWriteOptions(Mappable): +class XmlWriteOpts(Mappable): """Options for writing XML files""" def __init__( @@ -234,12 +226,12 @@ def __init__( self.short_empty_elements = short_empty_elements @staticmethod - def pretty_print(indent=4): + def pretty(indent=4): """Creates XmlWriteOptions for pretty-printed output""" - return XmlWriteOptions(pretty=True, indent=indent) + return XmlWriteOpts(pretty=True, indent=indent) -class PlainTextWriteOptions(Mappable): +class TextWriteOpts(Mappable): """Options for writing plain text files""" def __init__( @@ -253,6 +245,6 @@ def __init__( self.footer = footer @staticmethod - def with_header_footer(header="", footer="", delimiter="\n"): + def with_(*, header="", footer="", delimiter="\n"): """Creates PlainTextWriteOptions with header and footer""" - return PlainTextWriteOptions(delimiter=delimiter, header=header, footer=footer) + return TextWriteOpts(delimiter=delimiter, header=header, footer=footer) diff --git a/tests/test_dict_item.py b/tests/test_dict_item.py index 4089998..f55b949 100644 --- a/tests/test_dict_item.py +++ b/tests/test_dict_item.py @@ -6,7 +6,7 @@ class TestDictItem: - def test_dict_item_map(self, json_dict): + def test_map(self, json_dict): dictitem = DictItem(key="data", value=json_dict) assert dictitem.key == "data" assert dictitem.value == ( @@ -18,7 +18,7 @@ def test_dict_item_map(self, json_dict): DictItem(key="Job", value=None), ) - def test_dict_item_map_nested_dict(self, nested_json): + def test_map_nested_dict(self, nested_json): dictitem = DictItem(key="data", value=json.loads(nested_json)) assert dictitem.value == ( DictItem( @@ -46,7 +46,7 @@ def test_dict_item_map_nested_dict(self, nested_json): ), ) - def test_dict_item_repr(self, json_dict): + def test_repr(self, json_dict): assert str(DictItem(key="data", value=json_dict)) == ( "DictItem(key='data', value=(" "DictItem(key='Name', value='Jennifer Smith'), " @@ -57,7 +57,7 @@ def test_dict_item_repr(self, json_dict): "DictItem(key='Job', value=None)))" ) - def test_dict_item_eq(self, json_dict, nested_json): + def test_eq(self, json_dict, nested_json): assert DictItem(key="data", value=json.loads(nested_json)) == DictItem( key="data", value=json.loads(nested_json) ) @@ -68,18 +68,18 @@ def test_dict_item_eq(self, json_dict, nested_json): key="data", value=json.loads(nested_json) ) - def test_dict_item_eq_raises(self, json_dict): + def test_eq_raises(self, json_dict): nums = [1, 2, 3] with pytest.raises(TypeError) as e: DictItem(key="data", value=json_dict) == nums # noqa assert str(e.value) == f"{nums} is not a DictItem" @pytest.mark.parametrize("value", ["John", 42, (1, 2, 3), None]) - def test_dict_item_hash_returns_int(self, value): + def test_hash_returns_int(self, value): assert isinstance(hash(DictItem(key="k", value=value)), int) @pytest.mark.parametrize("value", ["John", 42, (1, 2, 3)]) - def test_dict_item_hash_consistency(self, value): + def test_hash_consistency(self, value): item = DictItem(key="k", value=value) other = DictItem(key="k", value=value) assert item == other @@ -89,7 +89,7 @@ def test_dict_item_hash_consistency(self, value): "value,expected_type", [([1, 2, 3], "list"), ({"a": 1}, "dict"), ({"list": [1, 2], "dict": {"a": 1}}, "dict")], ) - def test_dict_item_hash_unhashable_value_raises(self, value, expected_type): + def test_hash_unhashable_value_raises(self, value, expected_type): with pytest.raises(TypeError) as e: hash(DictItem(key="k", value=value)) assert ( diff --git a/tests/test_file_options.py b/tests/test_file_options.py index 8716679..cd21ad0 100644 --- a/tests/test_file_options.py +++ b/tests/test_file_options.py @@ -4,22 +4,22 @@ from pyrio import ( FileStream, - FileOptions, - CsvReadOptions, - CsvWriteOptions, - JsonReadOptions, - JsonWriteOptions, - YamlReadOptions, - YamlWriteOptions, - XmlReadOptions, - XmlWriteOptions, - PlainTextWriteOptions, + FileOpts, + CsvReadOpts, + CsvWriteOpts, + JsonReadOpts, + JsonWriteOpts, + YamlReadOpts, + YamlWriteOpts, + XmlReadOpts, + XmlWriteOpts, + TextWriteOpts, ) class TestFileOptions: def test_init_with_all_params(self): - opts = FileOptions(encoding="utf-8", errors="strict", newline="\n", buffering=1, mode="w") + opts = FileOpts(encoding="utf-8", errors="strict", newline="\n", buffering=1, mode="w") assert opts.encoding == "utf-8" assert opts.errors == "strict" assert opts.newline == "\n" @@ -27,7 +27,7 @@ def test_init_with_all_params(self): assert opts.mode == "w" def test_to_dict_all_values(self): - opts = FileOptions(encoding="utf-8", errors="ignore", newline="", buffering=0, mode="w") + opts = FileOpts(encoding="utf-8", errors="ignore", newline="", buffering=0, mode="w") assert opts.to_dict() == { "encoding": "utf-8", "errors": "ignore", @@ -37,248 +37,232 @@ def test_to_dict_all_values(self): } def test_to_dict_partial_values(self): - assert FileOptions(encoding="latin-1").to_dict() == {"encoding": "latin-1"} + assert FileOpts(encoding="latin-1").to_dict() == {"encoding": "latin-1"} def test_to_dict_empty(self): - assert FileOptions().to_dict() == {} + assert FileOpts().to_dict() == {} def test_utf8_factory(self): - opts = FileOptions.utf8() + opts = FileOpts.utf8() assert opts.encoding == "utf-8" assert opts.to_dict() == {"encoding": "utf-8"} def test_utf8_factory_with_errors(self): - opts = FileOptions.utf8(errors="replace") + opts = FileOpts.utf8(errors="replace") assert opts.encoding == "utf-8" assert opts.errors == "replace" assert opts.to_dict() == {"encoding": "utf-8", "errors": "replace"} def test_ascii_factory(self): - opts = FileOptions.ascii() + opts = FileOpts.ascii() assert opts.encoding == "ascii" assert opts.to_dict() == {"encoding": "ascii"} - def test_append_factory(self): - opts = FileOptions.append() - assert opts.mode == "a" - assert opts.to_dict() == {"mode": "a"} - - def test_append_factory_with_encoding(self): - opts = FileOptions.append(encoding="utf-8") - assert opts.mode == "a" - assert opts.encoding == "utf-8" - assert opts.to_dict() == {"encoding": "utf-8", "mode": "a"} - def test_repr(self): - assert repr(FileOptions(encoding="utf-8")) == "FileOptions(encoding='utf-8')" + assert str(FileOpts(encoding="utf-8")) == "FileOpts(encoding='utf-8')" class TestCsvReadOptions: def test_init_with_params(self): - opts = CsvReadOptions(delimiter=";", quotechar="'", strict=True) + opts = CsvReadOpts(delimiter=";", quotechar="'", strict=True) assert opts.delimiter == ";" assert opts.quotechar == "'" assert opts.strict is True def test_to_dict(self): - assert CsvReadOptions(delimiter="|", skipinitialspace=True).to_dict() == { + assert CsvReadOpts(delimiter="|", skipinitialspace=True).to_dict() == { "delimiter": "|", "skipinitialspace": True, } def test_excel_factory(self): - opts = CsvReadOptions.excel() + opts = CsvReadOpts.excel() assert opts.dialect == "excel" assert opts.to_dict() == {"dialect": "excel"} def test_unix_factory(self): - opts = CsvReadOptions.unix() + opts = CsvReadOpts.unix() assert opts.dialect == "unix" assert opts.to_dict() == {"dialect": "unix"} def test_repr(self): - assert repr(CsvReadOptions(delimiter=";")) == "CsvReadOptions(delimiter=';')" + assert str(CsvReadOpts(delimiter=";")) == "CsvReadOpts(delimiter=';')" class TestCsvWriteOptions: def test_init_with_params(self): - opts = CsvWriteOptions(delimiter="\t", lineterminator="\r\n") + opts = CsvWriteOpts(delimiter="\t", lineterminator="\r\n") assert opts.delimiter == "\t" assert opts.lineterminator == "\r\n" def test_to_dict(self): - assert CsvWriteOptions(delimiter=";", extrasaction="ignore").to_dict() == { + assert CsvWriteOpts(delimiter=";", extrasaction="ignore").to_dict() == { "delimiter": ";", "extrasaction": "ignore", } def test_repr(self): - assert repr(CsvWriteOptions(delimiter="|")) == "CsvWriteOptions(delimiter='|')" + assert str(CsvWriteOpts(delimiter="|")) == "CsvWriteOpts(delimiter='|')" class TestJsonReadOptions: def test_init_with_params(self): - assert JsonReadOptions(parse_float=Decimal).parse_float == Decimal + assert JsonReadOpts(parse_float=Decimal).parse_float == Decimal def test_to_dict(self): - assert JsonReadOptions(parse_float=Decimal, parse_int=str).to_dict() == { + assert JsonReadOpts(parse_float=Decimal, parse_int=str).to_dict() == { "parse_float": Decimal, "parse_int": str, } def test_with_decimal_factory(self): - opts = JsonReadOptions.with_decimal() + opts = JsonReadOpts.with_decimal() assert opts.parse_float == Decimal assert opts.to_dict() == {"parse_float": Decimal} def test_repr(self): - assert ( - repr(JsonReadOptions(parse_float=float)) - == "JsonReadOptions(parse_float=)" - ) + assert str(JsonReadOpts(parse_float=float)) == "JsonReadOpts(parse_float=)" class TestJsonWriteOptions: def test_init_with_params(self): - opts = JsonWriteOptions(indent=4, sort_keys=True) + opts = JsonWriteOpts(indent=4, sort_keys=True) assert opts.indent == 4 assert opts.sort_keys is True def test_to_dict(self): - assert JsonWriteOptions(indent=2, ensure_ascii=False).to_dict() == { + assert JsonWriteOpts(indent=2, ensure_ascii=False).to_dict() == { "indent": 2, "ensure_ascii": False, } def test_pretty_factory(self): - opts = JsonWriteOptions.pretty() + opts = JsonWriteOpts.pretty() assert opts.indent == 2 assert opts.to_dict() == {"indent": 2} def test_pretty_factory_custom_indent(self): - opts = JsonWriteOptions.pretty(indent=4) + opts = JsonWriteOpts.pretty(indent=4) assert opts.indent == 4 def test_compact_factory(self): - opts = JsonWriteOptions.compact() + opts = JsonWriteOpts.compact() assert opts.separators == (",", ":") assert opts.to_dict() == {"separators": (",", ":")} def test_sorted_factory(self): - opts = JsonWriteOptions.sorted() + opts = JsonWriteOpts.sorted() assert opts.indent == 2 assert opts.sort_keys is True assert opts.to_dict() == {"indent": 2, "sort_keys": True} def test_repr(self): - assert repr(JsonWriteOptions(indent=2)) == "JsonWriteOptions(indent=2)" + assert str(JsonWriteOpts(indent=2)) == "JsonWriteOpts(indent=2)" class TestYamlReadOptions: def test_init_with_params(self): - opts = YamlReadOptions(loader=yaml.SafeLoader) + opts = YamlReadOpts(loader=yaml.SafeLoader) assert opts.loader == yaml.SafeLoader def test_to_dict(self): - assert YamlReadOptions(loader=yaml.FullLoader).to_dict() == {"loader": yaml.FullLoader} + assert YamlReadOpts(loader=yaml.FullLoader).to_dict() == {"loader": yaml.FullLoader} def test_to_dict_empty(self): - assert YamlReadOptions().to_dict() == {} + assert YamlReadOpts().to_dict() == {} def test_repr(self): assert ( - repr(YamlReadOptions(loader=yaml.FullLoader)) - == "YamlReadOptions(loader=)" + str(YamlReadOpts(loader=yaml.FullLoader)) + == "YamlReadOpts(loader=)" ) class TestYamlWriteOptions: def test_init_with_params(self): - opts = YamlWriteOptions(default_flow_style=False, indent=4) + opts = YamlWriteOpts(default_flow_style=False, indent=4) assert opts.default_flow_style is False assert opts.indent == 4 def test_to_dict(self): - assert YamlWriteOptions(allow_unicode=True, width=80).to_dict() == { + assert YamlWriteOpts(allow_unicode=True, width=80).to_dict() == { "allow_unicode": True, "width": 80, } def test_block_style_factory(self): - opts = YamlWriteOptions.block_style() + opts = YamlWriteOpts.block_style() assert opts.default_flow_style is False assert opts.indent == 2 def test_block_style_factory_custom_indent(self): - opts = YamlWriteOptions.block_style(indent=4) + opts = YamlWriteOpts.block_style(indent=4) assert opts.indent == 4 def test_flow_style_factory(self): - opts = YamlWriteOptions.flow_style() + opts = YamlWriteOpts.flow_style() assert opts.default_flow_style is True def test_repr(self): - assert repr(YamlWriteOptions(indent=2)) == "YamlWriteOptions(indent=2)" + assert str(YamlWriteOpts(indent=2)) == "YamlWriteOpts(indent=2)" class TestXmlReadOptions: def test_init_with_params(self): - opts = XmlReadOptions(attr_prefix="@", cdata_key="#text") + opts = XmlReadOpts(attr_prefix="@", cdata_key="#text") assert opts.attr_prefix == "@" assert opts.cdata_key == "#text" def test_to_dict(self): - assert XmlReadOptions(process_namespaces=True).to_dict() == {"process_namespaces": True} + assert XmlReadOpts(process_namespaces=True).to_dict() == {"process_namespaces": True} def test_repr(self): assert ( - repr(XmlReadOptions(attr_prefix="@", cdata_key="#text", process_namespaces=True)) - == "XmlReadOptions(process_namespaces=True, attr_prefix='@', cdata_key='#text')" + str(XmlReadOpts(attr_prefix="@", cdata_key="#text", process_namespaces=True)) + == "XmlReadOpts(process_namespaces=True, attr_prefix='@', cdata_key='#text')" ) class TestXmlWriteOptions: def test_init_with_params(self): - opts = XmlWriteOptions(pretty=True, indent=4) + opts = XmlWriteOpts(pretty=True, indent=4) assert opts.pretty is True assert opts.indent == 4 def test_to_dict(self): - assert XmlWriteOptions(short_empty_elements=True).to_dict() == { - "short_empty_elements": True - } + assert XmlWriteOpts(short_empty_elements=True).to_dict() == {"short_empty_elements": True} def test_pretty_print_factory(self): - opts = XmlWriteOptions.pretty_print() + opts = XmlWriteOpts.pretty() assert opts.pretty is True assert opts.indent == 4 def test_pretty_print_factory_custom_indent(self): - opts = XmlWriteOptions.pretty_print(indent=2) + opts = XmlWriteOpts.pretty(indent=2) assert opts.indent == 2 def test_repr(self): - assert repr(XmlWriteOptions(pretty=True)) == "XmlWriteOptions(pretty=True)" + assert str(XmlWriteOpts(pretty=True)) == "XmlWriteOpts(pretty=True)" class TestPlainTextWriteOptions: def test_init_with_params(self): - opts = PlainTextWriteOptions(delimiter=", ", header="START", footer="END") + opts = TextWriteOpts(delimiter=", ", header="START", footer="END") assert opts.delimiter == ", " assert opts.header == "START" assert opts.footer == "END" def test_to_dict(self): - assert PlainTextWriteOptions(header="# Header\n").to_dict() == {"header": "# Header\n"} + assert TextWriteOpts(header="# Header\n").to_dict() == {"header": "# Header\n"} def test_with_header_footer_factory(self): - opts = PlainTextWriteOptions.with_header_footer(header="BEGIN\n", footer="\nEND") + opts = TextWriteOpts.with_(header="BEGIN\n", footer="\nEND") assert opts.header == "BEGIN\n" assert opts.footer == "\nEND" assert opts.delimiter == "\n" def test_repr(self): - assert repr(PlainTextWriteOptions(delimiter="|")) == "PlainTextWriteOptions(delimiter='|')" + assert str(TextWriteOpts(delimiter="|")) == "TextWriteOpts(delimiter='|')" class TestNormalizeOptions: @@ -293,7 +277,7 @@ def test_normalize_dict(self): assert result is input_dict # Should return same object def test_normalize_options_object(self): - opts = FileOptions(encoding="utf-8") + opts = FileOpts(encoding="utf-8") result = FileStream._normalize_options(opts) assert result == {"encoding": "utf-8"} assert isinstance(result, dict) diff --git a/tests/test_file_stream.py b/tests/test_file_stream.py index 682e801..cdbb80b 100644 --- a/tests/test_file_stream.py +++ b/tests/test_file_stream.py @@ -8,14 +8,14 @@ FileStream, Stream, DictItem, - FileOptions, - CsvReadOptions, - CsvWriteOptions, - JsonReadOptions, - JsonWriteOptions, - YamlWriteOptions, - XmlWriteOptions, - PlainTextWriteOptions, + FileOpts, + CsvReadOpts, + CsvWriteOpts, + JsonReadOpts, + JsonWriteOpts, + YamlWriteOpts, + XmlWriteOpts, + TextWriteOpts, ) from pyrio.exceptions import IllegalStateError, NoneTypeError @@ -440,15 +440,13 @@ def test_update_csv(self, tmp_file_dir): assert tmp_file_path.read_text() == open("./tests/resources/save_output/updated.csv").read() def test_update_fails(self, tmp_file_dir): - def _raise(exception): - raise exception + def _raise_err(_): + raise IOError("Ooops Mr White...") tmp_file_path = tmp_file_dir / "fail.csv" shutil.copyfile("./tests/resources/editable.csv", tmp_file_path) with pytest.raises(IOError, match="Ooops Mr White..."): - FileStream(tmp_file_path).save( - tmp_file_path, null_handler=_raise(IOError("Ooops Mr White...")) - ) + FileStream(tmp_file_path).save(tmp_file_path, null_handler=_raise_err) assert tmp_file_path.read_text() == open("./tests/resources/editable.csv").read() def test_combine_files_into_csv(self, tmp_file_dir): @@ -530,9 +528,9 @@ def test_file_handler_closed_on_exception(self, monkeypatch): original_init = BaseStream.__init__ original_open = builtins.open - def mock_init(self, iterable): + def mock_init(self_, iterable): # set up _iterable - original_init(self, iterable) + original_init(self_, iterable) raise RuntimeError("Simulated initialization error") def tracking_open(*args, **kwargs): @@ -593,7 +591,8 @@ class NotSerializable: # JSON options def test_read_json_with_options(self): result = FileStream.process( - "./tests/resources/parse_float.json", f_read_options=JsonReadOptions.with_decimal() + "./tests/resources/parse_float.json", + f_read_options=JsonReadOpts(parse_float=Decimal), ).to_tuple() assert any(isinstance(item.value, Decimal) for item in result if hasattr(item, "value")) @@ -602,8 +601,8 @@ def test_save_json_with_json_write_options(self, tmp_file_dir, json_dict): tmp_file_path = tmp_file_dir / "test_opts.json" FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_open_options=FileOptions.utf8(), - f_write_options=JsonWriteOptions.pretty(indent=2), + f_open_options=FileOpts.utf8(), + f_write_options=JsonWriteOpts.pretty(indent=2), ) assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.json").read() @@ -611,7 +610,7 @@ def test_save_json_with_sorted_options(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "sorted.json" FileStream("./tests/resources/foo.json").save( tmp_file_path, - f_write_options=JsonWriteOptions.sorted(indent=2), + f_write_options=JsonWriteOpts.sorted(indent=2), ) content = tmp_file_path.read_text() # With sort_keys=True, "abc" should come before "qwerty" @@ -621,7 +620,7 @@ def test_save_json_with_compact_options(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "compact.json" FileStream("./tests/resources/foo.json").save( tmp_file_path, - f_write_options=JsonWriteOptions.compact(), + f_write_options=JsonWriteOpts.compact(), ) content = tmp_file_path.read_text() # Compact format has no spaces after colons/commas @@ -639,7 +638,7 @@ def test_update_json_with_options(self, tmp_file_dir): ) ) .save( - f_write_options=JsonWriteOptions(indent=2), + f_write_options=JsonWriteOpts(indent=2), null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, ) ) @@ -651,7 +650,7 @@ def test_update_json_with_options(self, tmp_file_dir): def test_read_csv_with_options(self): result = FileStream.process( "./tests/resources/bar.csv", - f_read_options=CsvReadOptions(delimiter=","), + f_read_options=CsvReadOpts(delimiter=","), ).to_tuple() assert len(result) == 2 assert result[0]["fizz"] == "42" @@ -659,7 +658,7 @@ def test_read_csv_with_options(self): def test_read_csv_with_excel_dialect(self): result = FileStream.process( "./tests/resources/bar.csv", - f_read_options=CsvReadOptions.excel(), + f_read_options=CsvReadOpts.excel(), ).to_tuple() assert len(result) == 2 @@ -667,7 +666,7 @@ def test_save_csv_with_options(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "test_opts.csv" FileStream("./tests/resources/bar.csv").save( tmp_file_path, - f_write_options=CsvWriteOptions(delimiter=","), + f_write_options=CsvWriteOpts(delimiter=","), ) assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.csv").read() @@ -675,7 +674,7 @@ def test_save_csv_with_custom_delimiter(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "semicolon.csv" FileStream("./tests/resources/bar.csv").save( tmp_file_path, - f_write_options=CsvWriteOptions(delimiter=";"), + f_write_options=CsvWriteOpts(delimiter=";"), ) content = tmp_file_path.read_text() assert ";" in content @@ -687,7 +686,7 @@ def test_read_csv_with_custom_quotechar(self, tmp_file_dir): tmp_file_path.write_text("name,value\n'hello','world'\n") result = FileStream.process( tmp_file_path, - f_read_options=CsvReadOptions(quotechar="'"), + f_read_options=CsvReadOpts(quotechar="'"), ).to_tuple() assert len(result) == 1 @@ -697,8 +696,8 @@ def test_save_yaml_with_options(self, tmp_file_dir, json_dict): tmp_file_path = tmp_file_dir / "test_opts.yaml" FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_open_options=FileOptions(encoding="utf-8"), - f_write_options=YamlWriteOptions(indent=2), + f_open_options=FileOpts.utf8(), + f_write_options=YamlWriteOpts.block_style(), ) assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.yaml").read() @@ -706,7 +705,7 @@ def test_save_yaml_with_block_style(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "block.yaml" FileStream("./tests/resources/foo.yaml").save( tmp_file_path, - f_write_options=YamlWriteOptions.block_style(indent=2), + f_write_options=YamlWriteOpts.block_style(indent=2), ) assert tmp_file_path.read_text() == "abc: xyz\nqwerty: 42\n" @@ -714,7 +713,7 @@ def test_save_yaml_with_flow_style(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "flow.yaml" FileStream("./tests/resources/foo.yaml").save( tmp_file_path, - f_write_options=YamlWriteOptions.flow_style(), + f_write_options=YamlWriteOpts.flow_style(), ) assert tmp_file_path.read_text() == "{abc: xyz, qwerty: 42}\n" @@ -724,8 +723,8 @@ def test_save_xml_with_options(self, tmp_file_dir, json_dict): tmp_file_path = tmp_file_dir / "test_opts.xml" FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_open_options=FileOptions.utf8(), - f_write_options=XmlWriteOptions(pretty=True, indent=4), + f_open_options=FileOpts.utf8(), + f_write_options=XmlWriteOpts(pretty=True, indent=4), ) assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.xml").read() @@ -735,7 +734,7 @@ def test_save_xml_with_pretty_print_factory(self, tmp_file_dir, json_dict): in_memory_dict = Stream(json_dict).filter(lambda x: len(x.key) < 6).to_tuple() FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_write_options=XmlWriteOptions.pretty_print(indent=4), + f_write_options=XmlWriteOpts.pretty(indent=4), null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, xml_root="my-root", ) @@ -748,9 +747,7 @@ def test_save_plain_with_options(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "test_opts.txt" FileStream("./tests/resources/plain.txt").map(lambda x: x.strip()).head(2).save( tmp_file_path, - f_write_options=PlainTextWriteOptions.with_header_footer( - header="---START---\n", footer="\n---END---" - ), + f_write_options=TextWriteOpts.with_(header="---START---\n", footer="\n---END---"), ) assert tmp_file_path.read_text() == ( "---START---\n" @@ -759,7 +756,7 @@ def test_save_plain_with_options(self, tmp_file_dir): "---END---" ) - # FileOptions tests + # FileOpts tests def test_append_to_plain_with_file_options(self, tmp_file_dir): file_path = "append_map.txt" tmp_file_path = tmp_file_dir / file_path @@ -770,7 +767,7 @@ def test_append_to_plain_with_file_options(self, tmp_file_dir): .enumerate() .filter(lambda line: "ne" in line[1]) .map(lambda line: f"line_num:{line[0]}, text='{line[1]}'") - .save(f_open_options=FileOptions.append()) + .save(f_open_options=FileOpts(mode="a")) ) assert ( tmp_file_path.read_text() == open(f"./tests/resources/save_output/{file_path}").read() @@ -781,8 +778,8 @@ def test_save_with_utf8_file_options(self, tmp_file_dir, json_dict): tmp_file_path = tmp_file_dir / "utf8_opts.json" FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_open_options=FileOptions.utf8(), - f_write_options=JsonWriteOptions(indent=2), + f_open_options=FileOpts.utf8(), + f_write_options=JsonWriteOpts(indent=2), ) assert tmp_file_path.read_text() == open("./tests/resources/save_output/test.json").read() @@ -790,8 +787,8 @@ def test_save_with_ascii_file_options(self, tmp_file_dir): tmp_file_path = tmp_file_dir / "ascii.json" FileStream("./tests/resources/foo.json").save( tmp_file_path, - f_open_options=FileOptions.ascii(), - f_write_options=JsonWriteOptions(indent=2, ensure_ascii=True), + f_open_options=FileOpts.ascii(), + f_write_options=JsonWriteOpts(indent=2, ensure_ascii=True), ) content = tmp_file_path.read_text() assert "abc" in content @@ -802,8 +799,8 @@ def test_save_with_combined_options(self, tmp_file_dir, json_dict): tmp_file_path = tmp_file_dir / "combined.json" FileStream("./tests/resources/nested.json").prepend(in_memory_dict).save( tmp_file_path, - f_open_options=FileOptions(encoding="utf-8"), - f_write_options=JsonWriteOptions(indent=2), + f_open_options=FileOpts(encoding="utf-8"), + f_write_options=JsonWriteOpts(indent=2), null_handler=lambda x: DictItem(x.key, "Unknown") if x.value is None else x, ) assert ( diff --git a/tests/test_stream.py b/tests/test_stream.py index c0cf4a8..9bbb159 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -877,34 +877,36 @@ def test_nested_json_querying_nested_dict_items(self, nested_json): == "Freud" ) + # ### hacker-rank ### + def test_hackerrank(self): + from enum import Enum -# ### hacker-rank ### -def test_hackerrank(): - from enum import Enum - - # count vowels and constants in given string - string = "123Ab5oc-E6db#bCi9<>" - all_vowels = "AEIOUaeiou" - - class CharType(Enum): - VOWELS = "vowels" - CONSONANTS = "consonants" - - assert ( - Stream(string) - .filter(lambda ch: ch.isalpha()) - .partition(lambda ch: ch in all_vowels) - .enumerate() - .map(lambda x: (CharType.VOWELS if x[0] == 0 else CharType.CONSONANTS, len(tuple(x[1])))) - .to_dict() - ) == {CharType.CONSONANTS: 6, CharType.VOWELS: 4} - - -@pytest.mark.parametrize( - "string, expected", - [("a1b2c3c2b1a", True), ("abc321", False), ("xyyx", True), ("aba", True), ("z", True)], -) -def test_leetcode(string, expected): - # check if given string is palindrome; string length is guaranteed to be > 0 - stop = len(string) // 2 if len(string) > 1 else 1 - assert Stream.from_range(0, stop).all_match(lambda x: string[x] == string[-x - 1]) is expected + # count vowels and constants in given string + string = "123Ab5oc-E6db#bCi9<>" + all_vowels = "AEIOUaeiou" + + class CharType(Enum): + VOWELS = "vowels" + CONSONANTS = "consonants" + + assert ( + Stream(string) + .filter(lambda ch: ch.isalpha()) + .partition(lambda ch: ch in all_vowels) + .enumerate() + .map( + lambda x: (CharType.VOWELS if x[0] == 0 else CharType.CONSONANTS, len(tuple(x[1]))) + ) + .to_dict() + ) == {CharType.CONSONANTS: 6, CharType.VOWELS: 4} + + @pytest.mark.parametrize( + "string, expected", + [("a1b2c3c2b1a", True), ("abc321", False), ("xyyx", True), ("aba", True), ("z", True)], + ) + def test_leetcode(self, string, expected): + # check if given string is palindrome; string length is guaranteed to be > 0 + stop = len(string) // 2 if len(string) > 1 else 1 + assert ( + Stream.from_range(0, stop).all_match(lambda x: string[x] == string[-x - 1]) is expected + )