diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb7fdb5..0c70d66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.12", "3.11", "3.10", "3.9", "3.8", "3.7"] + python: ["3.13", "3.12", "3.11", "3.10", "3.9"] TOX_ENV: ["lint", "py", "mypy"] exclude: - TOX_ENV: "lint" diff --git a/.gitignore b/.gitignore index dce73e9..3165552 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build +dist .tox \ No newline at end of file diff --git a/expected_mypy.txt b/expected_mypy.txt index dbf6a2d..1f8eb77 100644 --- a/expected_mypy.txt +++ b/expected_mypy.txt @@ -43,4 +43,5 @@ tryit.py:53: note: def timetz(self) -> Time[None] tryit.py:53: note: Got: tryit.py:53: note: def timetz(self) -> Time[timezone] tryit.py:53: note: tzinfo: expected "None", got "timezone" -Found 9 errors in 1 file (checked 1 source file) +tryit.py:56: error: Incompatible types in assignment (expression has type "NaiveTime", variable has type "DateTime[timezone]") [assignment] +Found 10 errors in 1 file (checked 1 source file) diff --git a/expected_mypy_37.txt b/expected_mypy_37.txt index bc5ac05..386e580 100644 --- a/expected_mypy_37.txt +++ b/expected_mypy_37.txt @@ -29,4 +29,5 @@ tryit.py:53: note: def timetz(self) -> Time[None] tryit.py:53: note: Got: tryit.py:53: note: def timetz(self) -> Time[timezone] tryit.py:53: note: tzinfo: expected "None", got "timezone" -Found 7 errors in 1 file (checked 1 source file) +tryit.py:56: error: Incompatible types in assignment (expression has type "NaiveTime", variable has type "DateTime[timezone]") [assignment] +Found 8 errors in 1 file (checked 1 source file) diff --git a/pyproject.toml b/pyproject.toml index 8d340e6..4b55c63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ classifiers = [ "Development Status :: 5 - Production/Stable", # Indicate who your project is intended for "Intended Audience :: Developers", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.10", @@ -21,6 +22,9 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.7", ] +dependencies = [ + "typing_extensions ; python_version < '3.8'" +] [build-system] build-backend = "setuptools.build_meta" diff --git a/src/datetype/__init__.py b/src/datetype/__init__.py index 1edced5..d367e07 100644 --- a/src/datetype/__init__.py +++ b/src/datetype/__init__.py @@ -341,7 +341,7 @@ def utctimetuple(self) -> struct_time: ... def date(self) -> Date: ... - def time(self) -> NaiveDateTime: ... + def time(self) -> NaiveTime: ... @overload def replace( @@ -591,7 +591,13 @@ def aware(t: _time, tztype: Type[_FuncTZ]) -> Time[_FuncTZ]: ... def aware( t: Union[_datetime, _time], tztype: Optional[Type[_FuncTZ]] = None ) -> Union[DateTime[_FuncTZ], Time[_FuncTZ]]: - tzcheck: Type[_tzinfo] = tztype if tztype is not None else _tzinfo + tzcheck: Type[_tzinfo] + if tztype is not None: + tzcheck = tztype + else: + # tzinfo works just fine with isinstance checks, which is why we care + # about matching against type[...] here, so we can cast it away safely + tzcheck = _tzinfo # type:ignore if not isinstance(t.tzinfo, tzcheck): raise TypeError(f"{t} is naive, not aware") return t # type: ignore[return-value] diff --git a/src/datetype/test/test_datetype.py b/src/datetype/test/test_datetype.py index a64d782..352a800 100644 --- a/src/datetype/test/test_datetype.py +++ b/src/datetype/test/test_datetype.py @@ -1,16 +1,15 @@ -from datetime import date, datetime, time, timezone -from os import popen +from datetime import date, datetime, time, timedelta, timezone +from os import chdir, getcwd, popen +from pathlib import Path from sys import version_info -from unittest import TestCase +from unittest import TestCase, skipIf -from datetype import ( - AwareDateTime, - NaiveDateTime, - NaiveTime, - Time, - aware, - naive, -) +from datetype import AwareDateTime, NaiveDateTime, NaiveTime, Time, aware, naive + +TEST_DATA = (Path(__file__) / "..").resolve() +while not (TEST_DATA / ".git").is_dir(): + TEST_DATA = TEST_DATA / ".." +TEST_DATA = TEST_DATA.resolve() class DateTypeTests(TestCase): @@ -57,15 +56,34 @@ def test_mypy_output(self) -> None: Make sure that we get expected mypy errors. """ mypy_command = "mypy" - expected_file_name = "expected_mypy" + expected_file_name = ( + TEST_DATA / f"expected_mypy{'_37' if (version_info < (3, 8)) else ''}" + ) if version_info < (3, 9): mypy_command += " --ignore-missing-imports" # zoneinfo - if version_info[:2] == (3, 7): - expected_file_name += "_37" - with popen(f"{mypy_command} tryit.py") as f: + cwd = getcwd() + try: + chdir(TEST_DATA) + it = popen(f"{mypy_command} tryit.py") + finally: + chdir(cwd) + with it as f: actual = f.read() - with open(f"{expected_file_name}.txt") as f: + with expected_file_name.with_suffix(".txt").open() as f: expected = f.read() self.maxDiff = 9999 self.assertEqual(expected, actual) + + @skipIf(version_info < (3, 9), "ZoneInfo") + def test_none_aware(self) -> None: + """ + L{aware} with no argument will produce a ZoneInfo. + """ + from zoneinfo import ZoneInfo + + zi = ZoneInfo("US/Pacific") + stddt = datetime(2025, 2, 13, 15, 35, 13, 574354, tzinfo=zi) + awareified = aware(stddt) + self.assertIs(awareified.tzinfo, zi) + self.assertEqual(awareified.tzinfo.dst(stddt), timedelta(0)) diff --git a/tryit.py b/tryit.py index 46a7c8c..0a796e0 100644 --- a/tryit.py +++ b/tryit.py @@ -52,3 +52,5 @@ not_naive: NaiveDateTime = a.astimezone() is_aware: DateTime[timezone] = a.astimezone(None) + +not_aware_method_time: DateTime[timezone] = a.time() # nope; actually naive