Skip to content

Commit 360cc11

Browse files
committedAug 26, 2024·
Complete typing with strict type-checking
1 parent 4de58e9 commit 360cc11

File tree

7 files changed

+50
-34
lines changed

7 files changed

+50
-34
lines changed
 

‎.github/workflows/tests.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: run the tests
1919
run: python3 tests.py
2020
- name: verify type hints
21-
run: mypy datemath
21+
run: mypy . --strict
2222
- name: verify package install
2323
run: python3 setup.py install --user
2424
- name: verify we can import

‎datemath/__init__.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
from .helpers import parse, DateMathException
1+
from __future__ import annotations
22

3-
def dm(expr, **kwargs):
3+
from datetime import datetime
4+
from typing import TYPE_CHECKING
5+
6+
from arrow import Arrow
7+
8+
if TYPE_CHECKING:
9+
from typing_extensions import Unpack
10+
11+
from .helpers import ParseParams, parse as parse, DateMathException as DateMathException
12+
13+
def dm(expr: str | int, **kwargs: Unpack[ParseParams]) -> Arrow:
414
''' does our datemath and returns an arrow object '''
515
return parse(expr, **kwargs)
616

7-
def datemath(expr, **kwargs):
17+
def datemath(expr: str | int, **kwargs: Unpack[ParseParams]) -> datetime:
818
''' does our datemath and returns a datetime object '''
919
return parse(expr, **kwargs).datetime

‎datemath/helpers.py

+22-17
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,14 @@
3838
3939
'''
4040

41+
from __future__ import annotations
42+
43+
import os
44+
import re
45+
from typing import TypedDict, cast
46+
4147
import arrow
4248
from arrow import Arrow
43-
from datetime import datetime
44-
import re
45-
import os
46-
from dateutil import tz
47-
import dateutil
48-
import sys
49-
from pprint import pprint
50-
from typing import Any, Optional
5149

5250
debug = True if os.environ.get('DATEMATH_DEBUG') else False
5351

@@ -78,7 +76,13 @@ def unitMap(c: str) -> str:
7876
else:
7977
raise DateMathException("Not a valid timeunit: {0}".format(c))
8078

81-
def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, roundDown: bool = True) -> Arrow:
79+
class ParseParams(TypedDict, total=False):
80+
now: Arrow | None
81+
tz: str
82+
type: str | None
83+
roundDown: bool
84+
85+
def parse(expression: str | int, now: Arrow | None = None, tz: str = 'UTC', type: str | None = None, roundDown: bool = True) -> Arrow:
8286
'''
8387
the main meat and potatoes of this this whole thing
8488
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
101105
if debug: print("parse() - will now convert tz to {0}".format(tz))
102106
now = now.to(tz)
103107

108+
expression = str(expression)
104109
if expression == 'now':
105110
if debug: print("parse() - Now, no dm: {0}".format(now))
106111
if type:
107-
return getattr(now, type)
112+
return cast(Arrow, getattr(now, type))
108113
else:
109114
return now
110-
elif re.match(r'\d{10,}', str(expression)):
115+
elif re.match(r'\d{10,}', expression):
111116
if debug: print('parse() - found an epoch timestamp')
112-
if len(str(expression)) == 13:
117+
if len(expression) == 13:
113118
raise DateMathException('Unable to parse epoch timestamps in millis, please convert to the nearest second to continue - i.e. 1451610061 / 1000')
114119
ts = arrow.get(int(expression))
115120
ts = ts.replace(tzinfo=tz)
@@ -142,7 +147,7 @@ def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, r
142147
rettime = evaluate(math, time, tz, roundDown)
143148

144149
if type:
145-
return getattr(rettime, type)
150+
return cast(Arrow, getattr(rettime, type))
146151
else:
147152
return rettime
148153

@@ -158,7 +163,7 @@ def parseTime(timestamp: str, timezone: str = 'UTC') -> Arrow:
158163
if debug: print("parseTime() - timezone that came in = {}".format(timezone))
159164

160165
if ts.tzinfo:
161-
import dateutil
166+
import dateutil.tz
162167
if isinstance(ts.tzinfo, dateutil.tz.tz.tzoffset):
163168
# this means our TZ probably came in via our datetime string
164169
# then lets set our tz to whatever tzoffset is
@@ -175,14 +180,14 @@ def parseTime(timestamp: str, timezone: str = 'UTC') -> Arrow:
175180
if debug: print('parseTime() - Doesnt look like we have a valid timestamp, raise an exception. timestamp={}'.format(timestamp))
176181
raise DateMathException('Valid length timestamp not provide, you gave me a timestamp of "{}", but I need something that has a len() >= 4'.format(timestamp))
177182

178-
def roundDate(now: Any, unit: str, tz: str = 'UTC', roundDown: bool = True) -> Arrow:
183+
def roundDate(now: Arrow, unit: str, tz: str = 'UTC', roundDown: bool = True) -> Arrow:
179184
'''
180185
rounds our date object
181186
'''
182187
if roundDown:
183-
now = now.floor(unit)
188+
now = now.floor(unit) # type: ignore[arg-type]
184189
else:
185-
now = now.ceil(unit)
190+
now = now.ceil(unit) # type: ignore[arg-type]
186191
if debug: print("roundDate() Now: {0}".format(now))
187192
return now
188193

‎datemath/py.typed

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Marker file for PEP 561. The python-datemath package uses inline types.

‎requirements-3.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ docutils==0.15.2
99
freezegun==1.2.2
1010
idna==2.7
1111
linecache2==1.0.0
12-
mypy==1.5.1
13-
mypy-extensions==1.0.0
12+
mypy==1.7.1
1413
packaging==16.8
1514
pkginfo==1.4.2
1615
Pygments==2.7.4
@@ -24,9 +23,10 @@ six==1.10.0
2423
tqdm==4.36.1
2524
traceback2==1.4.0
2625
twine==2.0.0
27-
types-python-dateutil==2.8.19.14
26+
types-python-dateutil==2.8.19.20240311
27+
types-setuptools==73.0.0.20240822
28+
types-pytz==2023.3.1.1
2829
typing_extensions==4.7.1
2930
tzdata==2024.1
30-
unittest2==1.1.0
3131
urllib3==1.24.3
3232
webencodings==0.5.1

‎tests-legacy.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import unittest2 as unittest
1+
import unittest
22
import arrow
33
from datetime import datetime as pydatetime
44
from datemath import dm
@@ -8,7 +8,7 @@
88
iso8601 = 'YYYY-MM-DDTHH:mm:ssZZ'
99
class TestDM(unittest.TestCase):
1010

11-
def testParse(self):
11+
def testParse(self) -> None:
1212

1313
# Baisc dates
1414
self.assertEqual(dm('2016.01.02').format(iso8601), '2016-01-02T00:00:00-00:00')

‎tests.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class TestDM(unittest.TestCase):
1717

1818

19-
def testBasic(self):
19+
def testBasic(self) -> None:
2020
# Make sure our helpers return the correct objects
2121
self.assertIsInstance(datemath('now'), pydatetime)
2222
self.assertIsInstance(dm('now'), arrow.arrow.Arrow)
@@ -27,7 +27,7 @@ def testBasic(self):
2727
self.assertEqual(dm('2016-01-02 01:00:00').format(iso8601), '2016-01-02T01:00:00+00:00')
2828

2929

30-
def testRounding(self):
30+
def testRounding(self) -> None:
3131
# Rounding Tests
3232
self.assertEqual(dm('2016-01-01||/d').format('YYYY-MM-DDTHH:mm:ssZZ'), '2016-01-01T00:00:00+00:00')
3333
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):
4242
self.assertEqual(dm('2016-01-01||/d', roundDown=False).format('YYYY-MM-DDTHH:mm:ssZZ'), '2016-01-01T23:59:59+00:00')
4343
self.assertEqual(dm('2014-11-18||/y', roundDown=False).format('YYYY-MM-DDTHH:mm:ssZZ'), '2014-12-31T23:59:59+00:00')
4444

45-
def testTimezone(self):
45+
def testTimezone(self) -> None:
4646
# Timezone Tests
4747
with freeze_time(datemath('now/d', tz='US/Pacific')):
4848
self.assertEqual(datemath('now/d', tz='US/Pacific'), pydatetime.now(tz=pytz.timezone("US/Pacific")))
@@ -75,7 +75,7 @@ def testTimezone(self):
7575
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))))
7676

7777

78-
def testRelativeFormats(self):
78+
def testRelativeFormats(self) -> None:
7979
# relitive formats
8080

8181
# addition
@@ -135,7 +135,7 @@ def testRelativeFormats(self):
135135
self.assertEqual(dm('now-29d/d').format(iso8601), arrow.utcnow().shift(days=-29).floor('day').format(iso8601))
136136

137137

138-
def testFuture(self):
138+
def testFuture(self) -> None:
139139
# future
140140
self.assertEqual(dm('+1s').format(iso8601), arrow.utcnow().shift(seconds=+1).format(iso8601))
141141
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):
155155
self.assertEqual(dm('-3w-2d-22h-36s').format(iso8601), arrow.utcnow().shift(weeks=-3, days=-2, hours=-22, seconds=-36).format(iso8601))
156156
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))
157157

158-
def testOther(self):
158+
def testOther(self) -> None:
159159
import datetime
160160
delta = datetime.timedelta(seconds=1)
161161
# datetime objects
@@ -179,7 +179,7 @@ def testOther(self):
179179
self.assertTrue('Unable to parse epoch timestamps in millis' in str(e))
180180

181181

182-
def testExceptions(self):
182+
def testExceptions(self) -> None:
183183
# Catch invalid timeunits
184184
self.assertRaises(DateMathException, dm, '+1,')
185185
self.assertRaises(DateMathException, dm, '+1.')

0 commit comments

Comments
 (0)