-
Notifications
You must be signed in to change notification settings - Fork 5
🐛 Refactor data loaders to be lazy and use generators to prevent memory problems #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,51 @@ | ||
import json as jsonlib | ||
from collections import namedtuple | ||
from typing import Any, Iterator | ||
|
||
from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable | ||
from pystreamapi.loaders.__loader_utils import LoaderUtils | ||
|
||
|
||
def json(src: str, read_from_src=False) -> LazyFileIterable: | ||
def json(src: str, read_from_src=False) -> Iterator[Any]: | ||
""" | ||
Loads JSON data from either a path or a string and converts it into a list of namedtuples. | ||
Lazily loads JSON data from either a path or a string and yields namedtuples. | ||
|
||
Returns: | ||
list: A list of namedtuples, where each namedtuple represents an object in the JSON. | ||
:param src: Either the path to a JSON file or a JSON string. | ||
:param read_from_src: If True, src is treated as a JSON string. If False, src is treated as | ||
a path to a JSON file. | ||
Args: | ||
src (str): Either the path to a JSON file or a JSON string. | ||
read_from_src (bool): If True, src is treated as a JSON string. | ||
If False, src is treated as a path to a JSON file. | ||
|
||
Yields: | ||
namedtuple: Each object in the JSON as a namedtuple. | ||
""" | ||
if read_from_src: | ||
return LazyFileIterable(lambda: __load_json_string(src)) | ||
return __lazy_load_json_string(src) | ||
path = LoaderUtils.validate_path(src) | ||
return LazyFileIterable(lambda: __load_json_file(path)) | ||
return __lazy_load_json_file(path) | ||
|
||
|
||
def __lazy_load_json_file(file_path: str) -> Iterator[Any]: | ||
"""Lazily read and parse a JSON file, yielding namedtuples.""" | ||
|
||
def generator(): | ||
# skipcq: PTC-W6004 | ||
with open(file_path, mode='r', encoding='utf-8') as jsonfile: | ||
src = jsonfile.read() | ||
if src == '': | ||
return | ||
yield from jsonlib.loads(src, object_hook=__dict_to_namedtuple) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Incorrect iteration over JSON file data for non-array top-level objects Instead of using |
||
|
||
return generator() | ||
|
||
|
||
def __load_json_file(file_path): | ||
"""Load a JSON file and convert it into a list of namedtuples""" | ||
# skipcq: PTC-W6004 | ||
with open(file_path, mode='r', encoding='utf-8') as jsonfile: | ||
src = jsonfile.read() | ||
if src == '': | ||
return [] | ||
data = jsonlib.loads(src, object_hook=__dict_to_namedtuple) | ||
return data | ||
def __lazy_load_json_string(json_string: str) -> Iterator[Any]: | ||
"""Lazily parse a JSON string, yielding namedtuples.""" | ||
|
||
def generator(): | ||
if not json_string.strip(): | ||
return | ||
yield from jsonlib.loads(json_string, object_hook=__dict_to_namedtuple) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Incorrect iteration over JSON string data for non-array top-level objects Handle lists and single objects separately instead of always using |
||
|
||
def __load_json_string(json_string): | ||
"""Load JSON data from a string and convert it into a list of namedtuples""" | ||
return jsonlib.loads(json_string, object_hook=__dict_to_namedtuple) | ||
return generator() | ||
|
||
|
||
def __dict_to_namedtuple(d, name='Item'): | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,11 +1,12 @@ | ||||||||||||||||||||||||||||||||||
from typing import Iterator, Any | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||
from defusedxml import ElementTree | ||||||||||||||||||||||||||||||||||
except ImportError as exc: | ||||||||||||||||||||||||||||||||||
raise ImportError( | ||||||||||||||||||||||||||||||||||
"Please install the xml_loader extra dependency to use the xml loader." | ||||||||||||||||||||||||||||||||||
) from exc | ||||||||||||||||||||||||||||||||||
from collections import namedtuple | ||||||||||||||||||||||||||||||||||
from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable | ||||||||||||||||||||||||||||||||||
from pystreamapi.loaders.__loader_utils import LoaderUtils | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
@@ -21,14 +22,14 @@ def __init__(self): | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, | ||||||||||||||||||||||||||||||||||
encoding="utf-8") -> LazyFileIterable: | ||||||||||||||||||||||||||||||||||
encoding="utf-8") -> Iterator[Any]: | ||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||
Loads XML data from either a path or a string and converts it into a list of namedtuples. | ||||||||||||||||||||||||||||||||||
Warning: This method isn't safe against malicious XML trees. Parse only safe XML from sources | ||||||||||||||||||||||||||||||||||
you trust. | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
Returns: | ||||||||||||||||||||||||||||||||||
LazyFileIterable: A list of namedtuples, where each namedtuple represents an XML element. | ||||||||||||||||||||||||||||||||||
An iterator with namedtuples, where each namedtuple represents an XML element. | ||||||||||||||||||||||||||||||||||
:param retrieve_children: If true, the children of the root element are used as stream | ||||||||||||||||||||||||||||||||||
elements. | ||||||||||||||||||||||||||||||||||
:param encoding: The encoding of the XML file. | ||||||||||||||||||||||||||||||||||
|
@@ -39,32 +40,37 @@ def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, | |||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||
config.cast_types = cast_types | ||||||||||||||||||||||||||||||||||
config.retrieve_children = retrieve_children | ||||||||||||||||||||||||||||||||||
Comment on lines
41
to
42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Module-level config mutation causes global side-effects Passing these options as function arguments or using a per-call config object would prevent unintended side effects from shared state. |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if read_from_src: | ||||||||||||||||||||||||||||||||||
return LazyFileIterable(lambda: __load_xml_string(src)) | ||||||||||||||||||||||||||||||||||
return _lazy_parse_xml_string(src) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
path = LoaderUtils.validate_path(src) | ||||||||||||||||||||||||||||||||||
return LazyFileIterable(lambda: __load_xml_file(path, encoding)) | ||||||||||||||||||||||||||||||||||
return _lazy_parse_xml_file(path, encoding) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def _lazy_parse_xml_file(file_path: str, encoding: str) -> Iterator[Any]: | ||||||||||||||||||||||||||||||||||
def generator(): | ||||||||||||||||||||||||||||||||||
with open(file_path, mode='r', encoding=encoding) as xmlfile: | ||||||||||||||||||||||||||||||||||
xml_string = xmlfile.read() | ||||||||||||||||||||||||||||||||||
yield from _parse_xml_string_lazy(xml_string) | ||||||||||||||||||||||||||||||||||
Comment on lines
+51
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (performance): Reads entire XML file into memory, may lead to high memory usage Consider using a streaming parser like
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return generator() | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __load_xml_file(file_path, encoding): | ||||||||||||||||||||||||||||||||||
"""Load an XML file and convert it into a list of namedtuples.""" | ||||||||||||||||||||||||||||||||||
# skipcq: PTC-W6004 | ||||||||||||||||||||||||||||||||||
with open(file_path, mode='r', encoding=encoding) as xmlfile: | ||||||||||||||||||||||||||||||||||
src = xmlfile.read() | ||||||||||||||||||||||||||||||||||
if src: | ||||||||||||||||||||||||||||||||||
return __parse_xml_string(src) | ||||||||||||||||||||||||||||||||||
return [] | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def _lazy_parse_xml_string(xml_string: str) -> Iterator[Any]: | ||||||||||||||||||||||||||||||||||
def generator(): | ||||||||||||||||||||||||||||||||||
yield from _parse_xml_string_lazy(xml_string) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __load_xml_string(xml_string): | ||||||||||||||||||||||||||||||||||
"""Load XML data from a string and convert it into a list of namedtuples.""" | ||||||||||||||||||||||||||||||||||
return __parse_xml_string(xml_string) | ||||||||||||||||||||||||||||||||||
return generator() | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __parse_xml_string(xml_string): | ||||||||||||||||||||||||||||||||||
"""Parse XML string and convert it into a list of namedtuples.""" | ||||||||||||||||||||||||||||||||||
def _parse_xml_string_lazy(xml_string: str) -> Iterator[Any]: | ||||||||||||||||||||||||||||||||||
root = ElementTree.fromstring(xml_string) | ||||||||||||||||||||||||||||||||||
parsed_xml = __parse_xml(root) | ||||||||||||||||||||||||||||||||||
return __flatten(parsed_xml) if config.retrieve_children else [parsed_xml] | ||||||||||||||||||||||||||||||||||
parsed = __parse_xml(root) | ||||||||||||||||||||||||||||||||||
if config.retrieve_children: | ||||||||||||||||||||||||||||||||||
yield from __flatten(parsed) | ||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||
yield parsed | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __parse_xml(element): | ||||||||||||||||||||||||||||||||||
|
@@ -107,11 +113,9 @@ def __filter_single_items(tag_dict): | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __flatten(data): | ||||||||||||||||||||||||||||||||||
"""Flatten a list of lists.""" | ||||||||||||||||||||||||||||||||||
res = [] | ||||||||||||||||||||||||||||||||||
"""Yield flattened elements from a possibly nested structure.""" | ||||||||||||||||||||||||||||||||||
for item in data: | ||||||||||||||||||||||||||||||||||
if isinstance(item, list): | ||||||||||||||||||||||||||||||||||
res.extend(item) | ||||||||||||||||||||||||||||||||||
yield from item | ||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||
res.append(item) | ||||||||||||||||||||||||||||||||||
return res | ||||||||||||||||||||||||||||||||||
yield item |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Whitespace-only JSON files not skipped
src == ''
does not handle files with only whitespace. Useif not src.strip():
to skip such files and prevent JSON decode errors.