Skip to content

Commit 6c1c41e

Browse files
authored
Use dateutil parser (#213)
Switch to using `isoparse` from `dateutil.parser` instead of `datetime.fromisoformat` for more robust parsing of dates in from_dict.
1 parent 9e68819 commit 6c1c41e

File tree

4 files changed

+107
-13
lines changed

4 files changed

+107
-13
lines changed

poetry.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ packages = [
1313

1414
[tool.poetry.dependencies]
1515
python = "^3.6"
16-
backports-datetime-fromisoformat = { version = "^1.0.0", python = "<3.7" }
1716
black = { version = ">=19.3b0", optional = true }
1817
dataclasses = { version = "^0.7", python = ">=3.6, <3.7" }
1918
grpclib = "^0.4.1"
2019
jinja2 = { version = "^2.11.2", optional = true }
2120
protobuf = { version = "^3.12.2", optional = true }
21+
python-dateutil = "^2.8"
2222

2323
[tool.poetry.dev-dependencies]
2424
black = "^20.8b1"

src/betterproto/__init__.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from abc import ABC
99
from base64 import b64decode, b64encode
1010
from datetime import datetime, timedelta, timezone
11+
from dateutil.parser import isoparse
1112
from typing import (
1213
Any,
1314
Callable,
@@ -26,12 +27,6 @@
2627
from .casing import camel_case, safe_snake_case, snake_case
2728
from .grpc.grpclib_client import ServiceStub
2829

29-
if sys.version_info[:2] < (3, 7):
30-
# Apply backport of datetime.fromisoformat from 3.7
31-
from backports.datetime_fromisoformat import MonkeyPatch
32-
33-
MonkeyPatch.patch_fromisoformat()
34-
3530

3631
# Proto 3 data types
3732
TYPE_ENUM = "enum"
@@ -1051,10 +1046,7 @@ def from_dict(self: T, value: Dict[str, Any]) -> T:
10511046
if isinstance(v, list):
10521047
cls = self._betterproto.cls_by_field[field_name]
10531048
if cls == datetime:
1054-
v = [
1055-
datetime.fromisoformat(item.replace("Z", "+00:00"))
1056-
for item in value[key]
1057-
]
1049+
v = [isoparse(item) for item in value[key]]
10581050
elif cls == timedelta:
10591051
v = [
10601052
timedelta(seconds=float(item[:-1]))
@@ -1063,7 +1055,7 @@ def from_dict(self: T, value: Dict[str, Any]) -> T:
10631055
else:
10641056
v = [cls().from_dict(item) for item in value[key]]
10651057
elif isinstance(v, datetime):
1066-
v = datetime.fromisoformat(value[key].replace("Z", "+00:00"))
1058+
v = isoparse(value[key])
10671059
setattr(self, field_name, v)
10681060
elif isinstance(v, timedelta):
10691061
v = timedelta(seconds=float(value[key][:-1]))

tests/test_features.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import betterproto
22
from dataclasses import dataclass
33
from typing import Optional, List, Dict
4+
from datetime import datetime, timedelta
45

56

67
def test_has_field():
@@ -395,3 +396,70 @@ class Truthy(betterproto.Message):
395396
assert t
396397
t.bar = 0
397398
assert not t
399+
400+
401+
# valid ISO datetimes according to https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
402+
iso_candidates = """2009-12-12T12:34
403+
2009
404+
2009-05-19
405+
2009-05-19
406+
20090519
407+
2009123
408+
2009-05
409+
2009-123
410+
2009-222
411+
2009-001
412+
2009-W01-1
413+
2009-W51-1
414+
2009-W33
415+
2009W511
416+
2009-05-19
417+
2009-05-19 00:00
418+
2009-05-19 14
419+
2009-05-19 14:31
420+
2009-05-19 14:39:22
421+
2009-05-19T14:39Z
422+
2009-W21-2
423+
2009-W21-2T01:22
424+
2009-139
425+
2009-05-19 14:39:22-06:00
426+
2009-05-19 14:39:22+0600
427+
2009-05-19 14:39:22-01
428+
20090621T0545Z
429+
2007-04-06T00:00
430+
2007-04-05T24:00
431+
2010-02-18T16:23:48.5
432+
2010-02-18T16:23:48,444
433+
2010-02-18T16:23:48,3-06:00
434+
2010-02-18T16:23:00.4
435+
2010-02-18T16:23:00,25
436+
2010-02-18T16:23:00.33+0600
437+
2010-02-18T16:00:00.23334444
438+
2010-02-18T16:00:00,2283
439+
2009-05-19 143922
440+
2009-05-19 1439""".split(
441+
"\n"
442+
)
443+
444+
445+
def test_iso_datetime():
446+
@dataclass
447+
class Envelope(betterproto.Message):
448+
ts: datetime = betterproto.message_field(1)
449+
450+
msg = Envelope()
451+
452+
for _, candidate in enumerate(iso_candidates):
453+
msg.from_dict({"ts": candidate})
454+
assert isinstance(msg.ts, datetime)
455+
456+
457+
def test_iso_datetime_list():
458+
@dataclass
459+
class Envelope(betterproto.Message):
460+
timestamps: List[datetime] = betterproto.message_field(1)
461+
462+
msg = Envelope()
463+
464+
msg.from_dict({"timestamps": iso_candidates})
465+
assert all([isinstance(item, datetime) for item in msg.timestamps])

0 commit comments

Comments
 (0)