diff --git a/my/core/common.py b/my/core/common.py index 9874bed4..460a6587 100644 --- a/my/core/common.py +++ b/my/core/common.py @@ -14,7 +14,6 @@ Iterable, Iterator, List, - NoReturn, Optional, Sequence, TYPE_CHECKING, @@ -70,17 +69,6 @@ def import_dir(path: PathIsh, extra: str='') -> types.ModuleType: K = TypeVar('K') V = TypeVar('V') -# TODO deprecate? more_itertools.one should be used -def the(l: Iterable[T]) -> T: - it = iter(l) - try: - first = next(it) - except StopIteration: - raise RuntimeError('Empty iterator?') - assert all(e == first for e in it) - return first - - # TODO more_itertools.bucket? def group_by_key(l: Iterable[T], key: Callable[[T], K]) -> Dict[K, List[T]]: res: Dict[K, List[T]] = {} @@ -322,14 +310,6 @@ def __get__(self, obj, cls) -> _R: datetime_aware = datetime -# TODO deprecate -tzdatetime = datetime_aware - - -# TODO deprecate (although could be used in modules) -from .compat import fromisoformat as isoparse - - import re # https://stackoverflow.com/a/295466/706389 def get_valid_filename(s: str) -> str: @@ -554,7 +534,7 @@ def test_guess_datetime() -> None: from dataclasses import dataclass from typing import NamedTuple - dd = isoparse('2021-02-01T12:34:56Z') + dd = compat.fromisoformat('2021-02-01T12:34:56Z') # ugh.. https://github.com/python/mypy/issues/7281 A = NamedTuple('A', [('x', int)]) @@ -690,15 +670,41 @@ def unique_everseen( return more_itertools.unique_everseen(iterable=iterable, key=key) -## legacy imports, keeping them here for backwards compatibility +### legacy imports, keeping them here for backwards compatibility ## hiding behind TYPE_CHECKING so it works in runtime ## in principle, warnings.deprecated decorator should cooperate with mypy, but doesn't look like it works atm? ## perhaps it doesn't work when it's used from typing_extensions if not TYPE_CHECKING: - assert_never = deprecated('use my.core.compat.assert_never instead')(compat.assert_never) -# TODO wrap in deprecated decorator as well? -from functools import cached_property as cproperty -from typing import Literal -from .cachew import mcachew -## + @deprecated('use my.core.compat.assert_never instead') + def assert_never(*args, **kwargs): + return compat.assert_never(*args, **kwargs) + + @deprecated('use my.core.compat.fromisoformat instead') + def isoparse(*args, **kwargs): + return compat.fromisoformat(*args, **kwargs) + + @deprecated('use more_itertools.one instead') + def the(*args, **kwargs): + import more_itertools + + return more_itertools.one(*args, **kwargs) + + @deprecated('use functools.cached_property instead') + def cproperty(*args, **kwargs): + import functools + + return functools.cached_property(*args, **kwargs) + + # todo wrap these in deprecated decorator as well? + from .cachew import mcachew # noqa: F401 + + from typing import Literal # noqa: F401 + + # TODO hmm how to deprecate it in runtime? tricky cause it's actually a class? + tzdatetime = datetime_aware +else: + from .compat import Never + + tzdatetime = Never # makes it invalid as a type while working in runtime +### diff --git a/my/core/compat.py b/my/core/compat.py index 2c1687da..d6ba0087 100644 --- a/my/core/compat.py +++ b/my/core/compat.py @@ -2,56 +2,58 @@ Contains backwards compatibility helpers for different python versions. If something is relevant to HPI itself, please put it in .hpi_compat instead ''' -import os + import sys from typing import TYPE_CHECKING -windows = os.name == 'nt' +if sys.version_info[:2] >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated # keeping just for backwards compatibility, used to have compat implementation for 3.6 -import sqlite3 -def sqlite_backup(*, source: sqlite3.Connection, dest: sqlite3.Connection, **kwargs) -> None: - source.backup(dest, **kwargs) +if not TYPE_CHECKING: + import sqlite3 + + @deprecated('use .backup method on sqlite3.Connection directly instead') + def sqlite_backup(*, source: sqlite3.Connection, dest: sqlite3.Connection, **kwargs) -> None: + # TODO warn here? + source.backup(dest, **kwargs) # can remove after python3.9 (although need to keep the method itself for bwd compat) def removeprefix(text: str, prefix: str) -> str: if text.startswith(prefix): - return text[len(prefix):] + return text[len(prefix) :] return text -## used to have compat function before 3.8 for these -from functools import cached_property -from typing import Literal, Protocol, TypedDict +## used to have compat function before 3.8 for these, keeping for runtime back compatibility +if not TYPE_CHECKING: + from functools import cached_property + from typing import Literal, Protocol, TypedDict +else: + from typing_extensions import Literal, Protocol, TypedDict ## if sys.version_info[:2] >= (3, 10): from typing import ParamSpec else: - if TYPE_CHECKING: - from typing_extensions import ParamSpec - else: - from typing import NamedTuple, Any - # erm.. I guess as long as it's not crashing, whatever... - class _ParamSpec: - def __call__(self, args): - class _res: - args = None - kwargs = None - return _res - ParamSpec = _ParamSpec() + from typing_extensions import ParamSpec # bisect_left doesn't have a 'key' parameter (which we use) # till python3.10 if sys.version_info[:2] <= (3, 9): from typing import List, TypeVar, Any, Optional, Callable + X = TypeVar('X') + # copied from python src + # fmt: off def bisect_left(a: List[Any], x: Any, lo: int=0, hi: Optional[int]=None, *, key: Optional[Callable[..., Any]]=None) -> int: if lo < 0: raise ValueError('lo must be non-negative') @@ -74,19 +76,22 @@ def bisect_left(a: List[Any], x: Any, lo: int=0, hi: Optional[int]=None, *, key: else: hi = mid return lo + # fmt: on + else: from bisect import bisect_left from datetime import datetime + if sys.version_info[:2] >= (3, 11): fromisoformat = datetime.fromisoformat else: + # fromisoformat didn't support Z as "utc" before 3.11 + # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat + def fromisoformat(date_string: str) -> datetime: - # didn't support Z as "utc" before 3.11 if date_string.endswith('Z'): - # NOTE: can be removed from 3.11? - # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat date_string = date_string[:-1] + '+00:00' return datetime.fromisoformat(date_string) @@ -94,6 +99,7 @@ def fromisoformat(date_string: str) -> datetime: def test_fromisoformat() -> None: from datetime import timezone + # fmt: off # feedbin has this format assert fromisoformat('2020-05-01T10:32:02.925961Z') == datetime( 2020, 5, 1, 10, 32, 2, 925961, timezone.utc, @@ -108,6 +114,7 @@ def test_fromisoformat() -> None: assert fromisoformat('2020-11-30T00:53:12Z') == datetime( 2020, 11, 30, 0, 53, 12, 0, timezone.utc, ) + # fmt: on # arbtt has this format (sometimes less/more than 6 digits in milliseconds) # TODO doesn't work atm, not sure if really should be supported... @@ -123,13 +130,13 @@ def test_fromisoformat() -> None: NoneType = type(None) -if sys.version_info[:2] >= (3, 13): - from warnings import deprecated +if sys.version_info[:2] >= (3, 11): + from typing import assert_never else: - from typing_extensions import deprecated + from typing_extensions import assert_never if sys.version_info[:2] >= (3, 11): - from typing import assert_never + from typing import Never else: from typing_extensions import assert_never diff --git a/my/core/tests/test_cachew.py b/my/core/tests/test_cachew.py index 86344fd4..5f7dd658 100644 --- a/my/core/tests/test_cachew.py +++ b/my/core/tests/test_cachew.py @@ -10,7 +10,7 @@ def test_cachew() -> None: settings.ENABLE = True # by default it's off in tests (see conftest.py) - from my.core.common import mcachew + from my.core.cachew import mcachew called = 0 @@ -36,7 +36,7 @@ def test_cachew_dir_none() -> None: settings.ENABLE = True # by default it's off in tests (see conftest.py) from my.core.cachew import cache_dir - from my.core.common import mcachew + from my.core.cachew import mcachew from my.core.core_config import _reset_config as reset with reset() as cc: diff --git a/my/core/tests/test_get_files.py b/my/core/tests/test_get_files.py index e9f216ac..52e43f82 100644 --- a/my/core/tests/test_get_files.py +++ b/my/core/tests/test_get_files.py @@ -6,7 +6,6 @@ import zipfile from ..common import get_files -from ..compat import windows from ..kompress import CPath, ZipPath import pytest @@ -56,8 +55,9 @@ def test_single_file() -> None: ''' assert get_files('/tmp/hpi_test/file.ext') == (Path('/tmp/hpi_test/file.ext'),) + is_windows = os.name == 'nt' "if the path starts with ~, we expand it" - if not windows: # windows doesn't have bashrc.. ugh + if not is_windows: # windows doesn't have bashrc.. ugh assert get_files('~/.bashrc') == (Path('~').expanduser() / '.bashrc',) diff --git a/my/rss/feedbin.py b/my/rss/feedbin.py index 6160abc7..16d44174 100644 --- a/my/rss/feedbin.py +++ b/my/rss/feedbin.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Sequence -from ..core.common import listify, get_files -from ..core.compat import fromisoformat +from my.core.common import listify, get_files +from my.core.compat import fromisoformat from .common import Subscription @@ -33,12 +33,10 @@ def parse_file(f: Path): from typing import Iterable from .common import SubscriptionState def states() -> Iterable[SubscriptionState]: - # meh - from dateutil.parser import isoparse for f in inputs(): # TODO ugh. depends on my naming. not sure if useful? dts = f.stem.split('_')[-1] - dt = isoparse(dts) + dt = fromisoformat(dts) subs = parse_file(f) yield dt, subs diff --git a/my/time/tz/common.py b/my/time/tz/common.py index e2c428d6..107410aa 100644 --- a/my/time/tz/common.py +++ b/my/time/tz/common.py @@ -1,7 +1,7 @@ from datetime import datetime -from typing import Callable, cast +from typing import Callable, Literal, cast -from ...core.common import tzdatetime, Literal +from my.core.common import datetime_aware ''' @@ -30,7 +30,11 @@ def default_policy() -> TzPolicy: return 'keep' -def localize_with_policy(lfun: Callable[[datetime], tzdatetime], dt: datetime, policy: TzPolicy=default_policy()) -> tzdatetime: +def localize_with_policy( + lfun: Callable[[datetime], datetime_aware], + dt: datetime, + policy: TzPolicy=default_policy() +) -> datetime_aware: tz = dt.tzinfo if tz is None: return lfun(dt) diff --git a/my/time/tz/main.py b/my/time/tz/main.py index 624d7aa4..6180160d 100644 --- a/my/time/tz/main.py +++ b/my/time/tz/main.py @@ -2,10 +2,10 @@ Timezone data provider, used to localize timezone-unaware timestamps for other modules ''' from datetime import datetime -from ...core.common import tzdatetime +from my.core.common import datetime_aware # todo hmm, kwargs isn't mypy friendly.. but specifying types would require duplicating default args. uhoh -def localize(dt: datetime, **kwargs) -> tzdatetime: +def localize(dt: datetime, **kwargs) -> datetime_aware: # todo document patterns for combining multiple data sources # e.g. see https://github.com/karlicoss/HPI/issues/89#issuecomment-716495136 from . import via_location as L