diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6218e8e..cfe4b9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest] # todo windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] # vvv just an example of excluding stuff from matrix # exclude: [{platform: macos-latest, python-version: '3.6'}] diff --git a/pyproject.toml b/pyproject.toml index 668c47f..507fcdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,8 @@ dependencies = [ "appdirs" , # default cache dir "sqlalchemy>=1.0", # cache DB interaction "orjson", # fast json serialization - "pytz", # used to properly marshall pytz datatimes ] -requires-python = ">=3.8" +requires-python = ">=3.9" ## these need to be set if you're planning to upload to pypi # description = "TODO" @@ -32,6 +31,8 @@ Homepage = "https://github.com/karlicoss/cachew" [project.optional-dependencies] testing = [ + "pytz", "types-pytz", # optional runtime only dependency + "pytest", "more-itertools", "patchy", # for injecting sleeps and testing concurrent behaviour diff --git a/src/cachew/legacy.py b/src/cachew/legacy.py index 96091f5..1bf9b42 100644 --- a/src/cachew/legacy.py +++ b/src/cachew/legacy.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import date, datetime from itertools import chain, islice +from pathlib import Path from typing import ( Any, Generic, @@ -487,3 +488,20 @@ def test_ntbinder_primitive(tp, val) -> None: row = b.to_row(val) vv = b.from_row(list(row)) assert vv == val + + +def test_unique_columns(tmp_path: Path) -> None: + class Job(NamedTuple): + company: str + title: Optional[str] + + class Breaky(NamedTuple): + job_title: int + job: Optional[Job] + + assert [c.name for c in NTBinder.make(Breaky).columns] == [ + 'job_title', + '_job_is_null', + 'job_company', + '_job_title', + ] diff --git a/src/cachew/marshall/cachew.py b/src/cachew/marshall/cachew.py index 5ce98ee..da11aaf 100644 --- a/src/cachew/marshall/cachew.py +++ b/src/cachew/marshall/cachew.py @@ -22,6 +22,8 @@ get_type_hints, ) +from zoneinfo import ZoneInfo + from ..utils import TypeNotSupported, is_namedtuple from .common import ( AbstractMarshall, @@ -249,20 +251,6 @@ def make_tz_pytz(zone: str): make_tz_pytz = pytz.timezone -if sys.version_info[:2] >= (3, 9): - import zoneinfo - - ZoneInfo = zoneinfo.ZoneInfo - make_tz_zoneinfo = ZoneInfo -else: - # dummy, this is only needed for isinstance check below - class ZoneInfo: - key: str - - def make_tz_zoneinfo(zone: str): - raise RuntimeError(f"Need to use python3.9+ to deserialize {zone}") - - # just ints to avoid inflating db size # for now, we try to preserve actual timezone object just in case since they do have somewhat incompatible apis _TZTAG_ZONEINFO = 1 @@ -293,7 +281,7 @@ def load(self, dct: tuple): if zone is None: return dt - make_tz = make_tz_zoneinfo if zone_tag == _TZTAG_ZONEINFO else make_tz_pytz + make_tz = ZoneInfo if zone_tag == _TZTAG_ZONEINFO else make_tz_pytz tz = make_tz(zone) return dt.astimezone(tz) @@ -488,16 +476,14 @@ def test_serialize_and_deserialize() -> None: helper([1, 2, 3], Sequence[int], expected=(1, 2, 3)) helper((1, 2, 3), Sequence[int]) helper((1, 2, 3), Tuple[int, int, int]) - if sys.version_info[:2] >= (3, 9): - # TODO test with from __future__ import annotations.. - helper([1, 2, 3], list[int]) - helper((1, 2, 3), tuple[int, int, int]) + # TODO test with from __future__ import annotations.. + helper([1, 2, 3], list[int]) + helper((1, 2, 3), tuple[int, int, int]) # dicts helper({'a': 'aa', 'b': 'bb'}, Dict[str, str]) helper({'a': None, 'b': 'bb'}, Dict[str, Optional[str]]) - if sys.version_info[:2] >= (3, 9): - helper({'a': 'aa', 'b': 'bb'}, dict[str, str]) + helper({'a': 'aa', 'b': 'bb'}, dict[str, str]) # compounds of simple types helper(['1', 2, '3'], List[Union[str, int]]) @@ -559,19 +545,16 @@ class WithJson: dsummer_tz, ] - if sys.version_info[:2] >= (3, 9): - from zoneinfo import ZoneInfo - - tz_sydney = ZoneInfo('Australia/Sydney') - ## these will have same local time (2025-04-06 02:01:00) in Sydney due to DST shift! - ## the second one will have fold=1 set to disambiguate - utc_before_shift = datetime.fromisoformat('2025-04-05T15:01:00+00:00') - utc_after__shift = datetime.fromisoformat('2025-04-05T16:01:00+00:00') - ## - sydney_before = utc_before_shift.astimezone(tz_sydney) - sydney__after = utc_after__shift.astimezone(tz_sydney) + tz_sydney = ZoneInfo('Australia/Sydney') + ## these will have same local time (2025-04-06 02:01:00) in Sydney due to DST shift! + ## the second one will have fold=1 set to disambiguate + utc_before_shift = datetime.fromisoformat('2025-04-05T15:01:00+00:00') + utc_after__shift = datetime.fromisoformat('2025-04-05T16:01:00+00:00') + ## + sydney_before = utc_before_shift.astimezone(tz_sydney) + sydney__after = utc_after__shift.astimezone(tz_sydney) - dates_tz.extend([sydney_before, sydney__after]) + dates_tz.extend([sydney_before, sydney__after]) dates = [ *dates_tz, @@ -591,9 +574,8 @@ class WithJson: assert helper(dsummer_tz, datetime)[0] == ('2020-08-03T01:02:03+01:00', 'Europe/London', _TZTAG_PYTZ) assert helper(dwinter, datetime)[0] == ('2020-02-03T01:02:03', None, None) - if sys.version_info[:2] >= (3, 9): - assert helper(sydney_before, datetime)[0] == ('2025-04-06T02:01:00+11:00', 'Australia/Sydney', _TZTAG_ZONEINFO) - assert helper(sydney__after, datetime)[0] == ('2025-04-06T02:01:00+10:00', 'Australia/Sydney', _TZTAG_ZONEINFO) + assert helper(sydney_before, datetime)[0] == ('2025-04-06T02:01:00+11:00', 'Australia/Sydney', _TZTAG_ZONEINFO) + assert helper(sydney__after, datetime)[0] == ('2025-04-06T02:01:00+10:00', 'Australia/Sydney', _TZTAG_ZONEINFO) assert helper(dwinter.date(), date)[0] == '2020-02-03' diff --git a/src/cachew/tests/marshall.py b/src/cachew/tests/marshall.py index ab6db77..5d8753e 100644 --- a/src/cachew/tests/marshall.py +++ b/src/cachew/tests/marshall.py @@ -13,7 +13,6 @@ import orjson import pytest -import pytz from ..marshall.cachew import CachewMarshall from ..marshall.common import Json @@ -215,6 +214,8 @@ def test_datetimes(impl: Impl, count: int, gc_control, request) -> None: if impl == 'cattrs': pytest.skip('TODO support datetime with pytz for cattrs') + import pytz + def factory(*, count: int): tzs = [ pytz.timezone('Europe/Berlin'), diff --git a/src/cachew/tests/test_cachew.py b/src/cachew/tests/test_cachew.py index a64ee38..045f8f4 100644 --- a/src/cachew/tests/test_cachew.py +++ b/src/cachew/tests/test_cachew.py @@ -30,7 +30,6 @@ import patchy import pytest -import pytz from more_itertools import ilen, last, one, unique_everseen from .. import ( @@ -563,35 +562,6 @@ def make_people_data(count: int) -> Iterator[Person]: ) -def test_unique_columns(tmp_path: Path) -> None: - # TODO remove this test? it's for legacy stuff.. - from ..legacy import NTBinder - - class Breaky(NamedTuple): - job_title: int - job: Optional[Job] - - assert [c.name for c in NTBinder.make(Breaky).columns] == [ - 'job_title', - '_job_is_null', - 'job_company', - '_job_title', - ] - - b = Breaky( - job_title=123, - job=Job(company='123', title='whatever'), - ) - - @cachew(cache_path=tmp_path) - def iter_breaky() -> Iterator[Breaky]: - yield b - yield b - - assert list(iter_breaky()) == [b, b] - assert list(iter_breaky()) == [b, b] - - def test_stats(tmp_path: Path) -> None: cache_file = tmp_path / 'cache' @@ -648,13 +618,15 @@ class Dates: def test_dates(tmp_path: Path) -> None: - tz = pytz.timezone('Europe/London') + from zoneinfo import ZoneInfo + + tz = ZoneInfo('Europe/London') dwinter = datetime.strptime('20200203 01:02:03', '%Y%m%d %H:%M:%S') dsummer = datetime.strptime('20200803 01:02:03', '%Y%m%d %H:%M:%S') x = Dates( - d1=tz.localize(dwinter), - d2=tz.localize(dsummer), + d1=dwinter.replace(tzinfo=tz), + d2=dsummer.replace(tzinfo=tz), d3=dwinter, d4=dsummer, d5=dsummer.replace(tzinfo=timezone.utc), @@ -696,6 +668,8 @@ class AllTypes: def test_types(tmp_path: Path) -> None: + import pytz + tz = pytz.timezone('Europe/Berlin') # fmt: off obj = AllTypes( diff --git a/src/cachew/tests/test_future_annotations.py b/src/cachew/tests/test_future_annotations.py index 9d688c7..85dc1fe 100644 --- a/src/cachew/tests/test_future_annotations.py +++ b/src/cachew/tests/test_future_annotations.py @@ -25,9 +25,6 @@ class NewStyleTypes1: def test_types1(tmp_path: Path) -> None: - if sys.version_info[:2] <= (3, 8): - pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway") - # fmt: off obj = NewStyleTypes1( a_str = 'abac', @@ -86,9 +83,6 @@ def test_future_annotations( Checks handling of postponed evaluation of annotations (from __future__ import annotations) """ - if sys.version_info[:2] <= (3, 8): - pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway") - # NOTE: to avoid weird interactions with existing interpreter in which pytest is running # , we compose a program and running in python directly instead # (also not sure if it's even possible to tweak postponed annotations without doing that) diff --git a/tox.ini b/tox.ini index bd3444e..ecd9c21 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ commands = deps = -e .[testing,optional] commands = - {envpython} -m mypy --install-types --non-interactive \ + {envpython} -m mypy \ -p {[testenv]package_name} \ # txt report is a bit more convenient to view on CI --txt-report .coverage.mypy \