core: migrate code to benefit from 3.9 stuff (#401)

for now keeping ruff on 3.8 target version, need to sort out modules as well
This commit is contained in:
karlicoss 2024-10-19 20:55:09 +01:00 committed by GitHub
parent bc7c3ac253
commit d3f9a8e8b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 515 additions and 404 deletions

View file

@ -7,11 +7,14 @@ filtered iterator
See the select_range function below
"""
from __future__ import annotations
import re
import time
from collections.abc import Iterator
from datetime import date, datetime, timedelta
from functools import lru_cache
from typing import Any, Callable, Iterator, NamedTuple, Optional, Type
from functools import cache
from typing import Any, Callable, NamedTuple
import more_itertools
@ -25,7 +28,9 @@ from .query import (
select,
)
timedelta_regex = re.compile(r"^((?P<weeks>[\.\d]+?)w)?((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$")
timedelta_regex = re.compile(
r"^((?P<weeks>[\.\d]+?)w)?((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$"
)
# https://stackoverflow.com/a/51916936
@ -88,7 +93,7 @@ def parse_datetime_float(date_str: str) -> float:
# dateparser is a bit more lenient than the above, lets you type
# all sorts of dates as inputs
# https://github.com/scrapinghub/dateparser#how-to-use
res: Optional[datetime] = dateparser.parse(ds, settings={"DATE_ORDER": "YMD"})
res: datetime | None = dateparser.parse(ds, settings={"DATE_ORDER": "YMD"})
if res is not None:
return res.timestamp()
@ -98,7 +103,7 @@ def parse_datetime_float(date_str: str) -> float:
# probably DateLike input? but a user could specify an order_key
# which is an epoch timestamp or a float value which they
# expect to be converted to a datetime to compare
@lru_cache(maxsize=None)
@cache
def _datelike_to_float(dl: Any) -> float:
if isinstance(dl, datetime):
return dl.timestamp()
@ -130,11 +135,12 @@ class RangeTuple(NamedTuple):
of the timeframe -- 'before'
- before and after - anything after 'after' and before 'before', acts as a time range
"""
# technically doesn't need to be Optional[Any],
# just to make it more clear these can be None
after: Optional[Any]
before: Optional[Any]
within: Optional[Any]
after: Any | None
before: Any | None
within: Any | None
Converter = Callable[[Any], Any]
@ -145,9 +151,9 @@ def _parse_range(
unparsed_range: RangeTuple,
end_parser: Converter,
within_parser: Converter,
parsed_range: Optional[RangeTuple] = None,
error_message: Optional[str] = None
) -> Optional[RangeTuple]:
parsed_range: RangeTuple | None = None,
error_message: str | None = None,
) -> RangeTuple | None:
if parsed_range is not None:
return parsed_range
@ -176,11 +182,11 @@ def _create_range_filter(
end_parser: Converter,
within_parser: Converter,
attr_func: Where,
parsed_range: Optional[RangeTuple] = None,
default_before: Optional[Any] = None,
value_coercion_func: Optional[Converter] = None,
error_message: Optional[str] = None,
) -> Optional[Where]:
parsed_range: RangeTuple | None = None,
default_before: Any | None = None,
value_coercion_func: Converter | None = None,
error_message: str | None = None,
) -> Where | None:
"""
Handles:
- parsing the user input into values that are comparable to items the iterable returns
@ -272,17 +278,17 @@ def _create_range_filter(
def select_range(
itr: Iterator[ET],
*,
where: Optional[Where] = None,
order_key: Optional[str] = None,
order_value: Optional[Where] = None,
order_by_value_type: Optional[Type] = None,
unparsed_range: Optional[RangeTuple] = None,
where: Where | None = None,
order_key: str | None = None,
order_value: Where | None = None,
order_by_value_type: type | None = None,
unparsed_range: RangeTuple | None = None,
reverse: bool = False,
limit: Optional[int] = None,
limit: int | None = None,
drop_unsorted: bool = False,
wrap_unsorted: bool = False,
warn_exceptions: bool = False,
warn_func: Optional[Callable[[Exception], None]] = None,
warn_func: Callable[[Exception], None] | None = None,
drop_exceptions: bool = False,
raise_exceptions: bool = False,
) -> Iterator[ET]:
@ -317,9 +323,10 @@ def select_range(
drop_exceptions=drop_exceptions,
raise_exceptions=raise_exceptions,
warn_exceptions=warn_exceptions,
warn_func=warn_func)
warn_func=warn_func,
)
order_by_chosen: Optional[OrderFunc] = None
order_by_chosen: OrderFunc | None = None
# if the user didn't specify an attribute to order value, but specified a type
# we should search for on each value in the iterator
@ -345,7 +352,7 @@ Specify a type or a key to order the value by""")
# force drop_unsorted=True so we can use _create_range_filter
# sort the iterable by the generated order_by_chosen function
itr = select(itr, order_by=order_by_chosen, drop_unsorted=True)
filter_func: Optional[Where]
filter_func: Where | None
if order_by_value_type in [datetime, date]:
filter_func = _create_range_filter(
unparsed_range=unparsed_range,
@ -353,7 +360,8 @@ Specify a type or a key to order the value by""")
within_parser=parse_timedelta_float,
attr_func=order_by_chosen, # type: ignore[arg-type]
default_before=time.time(),
value_coercion_func=_datelike_to_float)
value_coercion_func=_datelike_to_float,
)
elif order_by_value_type in [int, float]:
# allow primitives to be converted using the default int(), float() callables
filter_func = _create_range_filter(
@ -362,7 +370,8 @@ Specify a type or a key to order the value by""")
within_parser=order_by_value_type,
attr_func=order_by_chosen, # type: ignore[arg-type]
default_before=None,
value_coercion_func=order_by_value_type)
value_coercion_func=order_by_value_type,
)
else:
# TODO: add additional kwargs to let the user sort by other values, by specifying the parsers?
# would need to allow passing the end_parser, within parser, default before and value_coercion_func...
@ -470,7 +479,7 @@ def test_range_predicate() -> None:
# filter from 0 to 5
rn: RangeTuple = RangeTuple("0", "5", None)
zero_to_five_filter: Optional[Where] = int_filter_func(unparsed_range=rn)
zero_to_five_filter: Where | None = int_filter_func(unparsed_range=rn)
assert zero_to_five_filter is not None
# this is just a Where function, given some input it return True/False if the value is allowed
assert zero_to_five_filter(3) is True
@ -483,6 +492,7 @@ def test_range_predicate() -> None:
rn = RangeTuple(None, 3, "3.5")
assert list(filter(int_filter_func(unparsed_range=rn, attr_func=identity), src())) == ["0", "1", "2"]
def test_parse_range() -> None:
from functools import partial