From 360cc11190bd60a544ef971546827b62d0ec9789 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 24 Aug 2024 23:20:04 -0400 Subject: [PATCH] Complete typing with strict type-checking --- .github/workflows/tests.yaml | 2 +- datemath/__init__.py | 16 ++++++++++++--- datemath/helpers.py | 39 ++++++++++++++++++++---------------- datemath/py.typed | 1 + requirements-3.txt | 8 ++++---- tests-legacy.py | 4 ++-- tests.py | 14 ++++++------- 7 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 datemath/py.typed diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 218bdb5..f979c69 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,7 +18,7 @@ jobs: - name: run the tests run: python3 tests.py - name: verify type hints - run: mypy datemath + run: mypy . --strict - name: verify package install run: python3 setup.py install --user - name: verify we can import diff --git a/datemath/__init__.py b/datemath/__init__.py index de15813..cc2cb78 100644 --- a/datemath/__init__.py +++ b/datemath/__init__.py @@ -1,9 +1,19 @@ -from .helpers import parse, DateMathException +from __future__ import annotations -def dm(expr, **kwargs): +from datetime import datetime +from typing import TYPE_CHECKING + +from arrow import Arrow + +if TYPE_CHECKING: + from typing_extensions import Unpack + +from .helpers import ParseParams, parse as parse, DateMathException as DateMathException + +def dm(expr: str | int, **kwargs: Unpack[ParseParams]) -> Arrow: ''' does our datemath and returns an arrow object ''' return parse(expr, **kwargs) -def datemath(expr, **kwargs): +def datemath(expr: str | int, **kwargs: Unpack[ParseParams]) -> datetime: ''' does our datemath and returns a datetime object ''' return parse(expr, **kwargs).datetime diff --git a/datemath/helpers.py b/datemath/helpers.py index 62e6b16..3ea145b 100644 --- a/datemath/helpers.py +++ b/datemath/helpers.py @@ -38,16 +38,14 @@ ''' +from __future__ import annotations + +import os +import re +from typing import TypedDict, cast + import arrow from arrow import Arrow -from datetime import datetime -import re -import os -from dateutil import tz -import dateutil -import sys -from pprint import pprint -from typing import Any, Optional debug = True if os.environ.get('DATEMATH_DEBUG') else False @@ -78,7 +76,13 @@ def unitMap(c: str) -> str: else: raise DateMathException("Not a valid timeunit: {0}".format(c)) -def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, roundDown: bool = True) -> Arrow: +class ParseParams(TypedDict, total=False): + now: Arrow | None + tz: str + type: str | None + roundDown: bool + +def parse(expression: str | int, now: Arrow | None = None, tz: str = 'UTC', type: str | None = None, roundDown: bool = True) -> Arrow: ''' the main meat and potatoes of this this whole thing takes our datemath expression and does our date math @@ -101,15 +105,16 @@ def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, r if debug: print("parse() - will now convert tz to {0}".format(tz)) now = now.to(tz) + expression = str(expression) if expression == 'now': if debug: print("parse() - Now, no dm: {0}".format(now)) if type: - return getattr(now, type) + return cast(Arrow, getattr(now, type)) else: return now - elif re.match(r'\d{10,}', str(expression)): + elif re.match(r'\d{10,}', expression): if debug: print('parse() - found an epoch timestamp') - if len(str(expression)) == 13: + if len(expression) == 13: raise DateMathException('Unable to parse epoch timestamps in millis, please convert to the nearest second to continue - i.e. 1451610061 / 1000') ts = arrow.get(int(expression)) ts = ts.replace(tzinfo=tz) @@ -142,7 +147,7 @@ def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, r rettime = evaluate(math, time, tz, roundDown) if type: - return getattr(rettime, type) + return cast(Arrow, getattr(rettime, type)) else: return rettime @@ -158,7 +163,7 @@ def parseTime(timestamp: str, timezone: str = 'UTC') -> Arrow: if debug: print("parseTime() - timezone that came in = {}".format(timezone)) if ts.tzinfo: - import dateutil + import dateutil.tz if isinstance(ts.tzinfo, dateutil.tz.tz.tzoffset): # this means our TZ probably came in via our datetime string # then lets set our tz to whatever tzoffset is @@ -175,14 +180,14 @@ def parseTime(timestamp: str, timezone: str = 'UTC') -> Arrow: if debug: print('parseTime() - Doesnt look like we have a valid timestamp, raise an exception. timestamp={}'.format(timestamp)) raise DateMathException('Valid length timestamp not provide, you gave me a timestamp of "{}", but I need something that has a len() >= 4'.format(timestamp)) -def roundDate(now: Any, unit: str, tz: str = 'UTC', roundDown: bool = True) -> Arrow: +def roundDate(now: Arrow, unit: str, tz: str = 'UTC', roundDown: bool = True) -> Arrow: ''' rounds our date object ''' if roundDown: - now = now.floor(unit) + now = now.floor(unit) # type: ignore[arg-type] else: - now = now.ceil(unit) + now = now.ceil(unit) # type: ignore[arg-type] if debug: print("roundDate() Now: {0}".format(now)) return now diff --git a/datemath/py.typed b/datemath/py.typed new file mode 100644 index 0000000..04ca0a2 --- /dev/null +++ b/datemath/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The python-datemath package uses inline types. diff --git a/requirements-3.txt b/requirements-3.txt index a4920a8..257a732 100644 --- a/requirements-3.txt +++ b/requirements-3.txt @@ -9,8 +9,7 @@ docutils==0.15.2 freezegun==1.2.2 idna==2.7 linecache2==1.0.0 -mypy==1.5.1 -mypy-extensions==1.0.0 +mypy==1.7.1 packaging==16.8 pkginfo==1.4.2 Pygments==2.7.4 @@ -24,9 +23,10 @@ six==1.10.0 tqdm==4.36.1 traceback2==1.4.0 twine==2.0.0 -types-python-dateutil==2.8.19.14 +types-python-dateutil==2.8.19.20240311 +types-setuptools==73.0.0.20240822 +types-pytz==2023.3.1.1 typing_extensions==4.7.1 tzdata==2024.1 -unittest2==1.1.0 urllib3==1.24.3 webencodings==0.5.1 diff --git a/tests-legacy.py b/tests-legacy.py index 3b0c6e1..8c13993 100644 --- a/tests-legacy.py +++ b/tests-legacy.py @@ -1,4 +1,4 @@ -import unittest2 as unittest +import unittest import arrow from datetime import datetime as pydatetime from datemath import dm @@ -8,7 +8,7 @@ iso8601 = 'YYYY-MM-DDTHH:mm:ssZZ' class TestDM(unittest.TestCase): - def testParse(self): + def testParse(self) -> None: # Baisc dates self.assertEqual(dm('2016.01.02').format(iso8601), '2016-01-02T00:00:00-00:00') diff --git a/tests.py b/tests.py index c0f9900..246bff7 100644 --- a/tests.py +++ b/tests.py @@ -16,7 +16,7 @@ class TestDM(unittest.TestCase): - def testBasic(self): + def testBasic(self) -> None: # Make sure our helpers return the correct objects self.assertIsInstance(datemath('now'), pydatetime) self.assertIsInstance(dm('now'), arrow.arrow.Arrow) @@ -27,7 +27,7 @@ def testBasic(self): self.assertEqual(dm('2016-01-02 01:00:00').format(iso8601), '2016-01-02T01:00:00+00:00') - def testRounding(self): + def testRounding(self) -> None: # Rounding Tests self.assertEqual(dm('2016-01-01||/d').format('YYYY-MM-DDTHH:mm:ssZZ'), '2016-01-01T00:00:00+00:00') self.assertEqual(dm('2014-11-18||/y').format('YYYY-MM-DDTHH:mm:ssZZ'), '2014-01-01T00:00:00+00:00') @@ -42,7 +42,7 @@ def testRounding(self): self.assertEqual(dm('2016-01-01||/d', roundDown=False).format('YYYY-MM-DDTHH:mm:ssZZ'), '2016-01-01T23:59:59+00:00') self.assertEqual(dm('2014-11-18||/y', roundDown=False).format('YYYY-MM-DDTHH:mm:ssZZ'), '2014-12-31T23:59:59+00:00') - def testTimezone(self): + def testTimezone(self) -> None: # Timezone Tests with freeze_time(datemath('now/d', tz='US/Pacific')): self.assertEqual(datemath('now/d', tz='US/Pacific'), pydatetime.now(tz=pytz.timezone("US/Pacific"))) @@ -75,7 +75,7 @@ def testTimezone(self): self.assertEqual(datemath('2016-01-01T16:20:00.6+12:00||+2d+1h', tz='US/Eastern'), pydatetime(2016, 1, 3, 17, 20, 0, 600000, tzinfo=tz.tzoffset(None, timedelta(hours=12)))) - def testRelativeFormats(self): + def testRelativeFormats(self) -> None: # relitive formats # addition @@ -135,7 +135,7 @@ def testRelativeFormats(self): self.assertEqual(dm('now-29d/d').format(iso8601), arrow.utcnow().shift(days=-29).floor('day').format(iso8601)) - def testFuture(self): + def testFuture(self) -> None: # future self.assertEqual(dm('+1s').format(iso8601), arrow.utcnow().shift(seconds=+1).format(iso8601)) self.assertEqual(dm('+1s+2m+3h').format(iso8601), arrow.utcnow().shift(seconds=+1, minutes=+2, hours=+3).format(iso8601)) @@ -155,7 +155,7 @@ def testFuture(self): self.assertEqual(dm('-3w-2d-22h-36s').format(iso8601), arrow.utcnow().shift(weeks=-3, days=-2, hours=-22, seconds=-36).format(iso8601)) self.assertEqual(dm('-6y-3w-2d-22h-36s').format(iso8601), arrow.utcnow().shift(years=-6, weeks=-3, days=-2, hours=-22, seconds=-36).format(iso8601)) - def testOther(self): + def testOther(self) -> None: import datetime delta = datetime.timedelta(seconds=1) # datetime objects @@ -179,7 +179,7 @@ def testOther(self): self.assertTrue('Unable to parse epoch timestamps in millis' in str(e)) - def testExceptions(self): + def testExceptions(self) -> None: # Catch invalid timeunits self.assertRaises(DateMathException, dm, '+1,') self.assertRaises(DateMathException, dm, '+1.')