Skip to content

Commit 46f8289

Browse files
authored
Python: add datetime support + pass mypy --strict + rename JSON-related methods (#14)
* Python: add datetime support + pass mypy --strict * Fix rfc3339 regex
1 parent d07cf75 commit 46f8289

File tree

49 files changed

+2812
-1129
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2812
-1129
lines changed

crates/target_csharp_system_text/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,7 @@ impl jtd_codegen::target::Target for Target {
160160
None
161161
}
162162

163-
target::Item::Postamble => {
164-
None
165-
}
163+
target::Item::Postamble => None,
166164

167165
target::Item::Alias {
168166
metadata,

crates/target_go/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,7 @@ impl jtd_codegen::target::Target for Target {
157157
None
158158
}
159159

160-
target::Item::Postamble => {
161-
None
162-
}
160+
target::Item::Postamble => None,
163161

164162
target::Item::Alias {
165163
metadata,

crates/target_java_jackson/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,7 @@ impl jtd_codegen::target::Target for Target {
198198
None
199199
}
200200

201-
target::Item::Postamble => {
202-
None
203-
}
201+
target::Item::Postamble => None,
204202

205203
target::Item::Alias {
206204
metadata,

crates/target_python/docker/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ FROM python:3.9
22

33
ARG MAIN
44

5+
RUN pip3 install mypy
6+
57
WORKDIR /work
68
COPY /main.py /work/main.py
79

810
COPY /gen /work/gen
911
RUN sed -i -e "s/MAIN/$MAIN/g" /work/main.py
1012

13+
RUN mypy --strict .
14+
1115
ENTRYPOINT python3 -u main.py

crates/target_python/docker/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
from gen import MAIN
44

55
for line in sys.stdin:
6-
value = MAIN.from_json(json.loads(line))
7-
print(json.dumps(value.to_json()))
6+
value = MAIN.from_json_data(json.loads(line))
7+
print(json.dumps(value.to_json_data()))
Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,114 @@
11
# Code generated by jtd-codegen for Python v0.2.0
22

3+
import re
34
from dataclasses import dataclass
4-
from typing import Any, Union, get_args, get_origin
5+
from datetime import datetime, timedelta, timezone
6+
from typing import Any, Dict, Type, Union, get_args, get_origin
57

6-
def _from_json(cls, data):
7-
if data is None or cls in [bool, int, float, str, object] or cls is Any:
8-
return data
9-
if get_origin(cls) is Union:
10-
return _from_json(get_args(cls)[0], data)
11-
if get_origin(cls) is list:
12-
return [_from_json(get_args(cls)[0], d) for d in data]
13-
if get_origin(cls) is dict:
14-
return { k: _from_json(get_args(cls)[1], v) for k, v in data.items() }
15-
return cls.from_json(data)
16-
17-
def _to_json(data):
18-
if data is None or type(data) in [bool, int, float, str, object]:
19-
return data
20-
if type(data) is list:
21-
return [_to_json(d) for d in data]
22-
if type(data) is dict:
23-
return { k: _to_json(v) for k, v in data.items() }
24-
return data.to_json()
258

269
@dataclass
2710
class Root:
2811
foo: 'str'
2912

3013
@classmethod
31-
def from_json(cls, data) -> 'Root':
32-
return {
14+
def from_json_data(cls, data: Any) -> 'Root':
15+
variants: Dict[str, Type[Root]] = {
3316
"BAR_BAZ": RootBarBaz,
3417
"QUUX": RootQuux,
35-
}[data["foo"]].from_json(data)
18+
}
3619

37-
def to_json(self):
20+
return variants[data["foo"]].from_json_data(data)
21+
22+
def to_json_data(self) -> Any:
3823
pass
3924

4025
@dataclass
4126
class RootBarBaz(Root):
4227
baz: 'str'
4328

4429
@classmethod
45-
def from_json(cls, data) -> 'RootBarBaz':
30+
def from_json_data(cls, data: Any) -> 'RootBarBaz':
4631
return cls(
4732
"BAR_BAZ",
48-
_from_json(str, data.get("baz")),
33+
_from_json_data(str, data.get("baz")),
4934
)
5035

51-
def to_json(self):
36+
def to_json_data(self) -> Any:
5237
data = { "foo": "BAR_BAZ" }
53-
data["baz"] = _to_json(self.baz)
38+
data["baz"] = _to_json_data(self.baz)
5439
return data
5540

5641
@dataclass
5742
class RootQuux(Root):
5843
quuz: 'str'
5944

6045
@classmethod
61-
def from_json(cls, data) -> 'RootQuux':
46+
def from_json_data(cls, data: Any) -> 'RootQuux':
6247
return cls(
6348
"QUUX",
64-
_from_json(str, data.get("quuz")),
49+
_from_json_data(str, data.get("quuz")),
6550
)
6651

67-
def to_json(self):
52+
def to_json_data(self) -> Any:
6853
data = { "foo": "QUUX" }
69-
data["quuz"] = _to_json(self.quuz)
54+
data["quuz"] = _to_json_data(self.quuz)
55+
return data
56+
57+
def _from_json_data(cls: Any, data: Any) -> Any:
58+
if data is None or cls in [bool, int, float, str, object] or cls is Any:
59+
return data
60+
if cls is datetime:
61+
return _parse_rfc3339(data)
62+
if get_origin(cls) is Union:
63+
return _from_json_data(get_args(cls)[0], data)
64+
if get_origin(cls) is list:
65+
return [_from_json_data(get_args(cls)[0], d) for d in data]
66+
if get_origin(cls) is dict:
67+
return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() }
68+
return cls.from_json_data(data)
69+
70+
def _to_json_data(data: Any) -> Any:
71+
if data is None or type(data) in [bool, int, float, str, object]:
7072
return data
73+
if type(data) is datetime:
74+
return data.isoformat()
75+
if type(data) is list:
76+
return [_to_json_data(d) for d in data]
77+
if type(data) is dict:
78+
return { k: _to_json_data(v) for k, v in data.items() }
79+
return data.to_json_data()
80+
81+
def _parse_rfc3339(s: str) -> datetime:
82+
datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$'
83+
match = re.match(datetime_re, s)
84+
if not match:
85+
raise ValueError('Invalid RFC3339 date/time', s)
86+
87+
(year, month, day, hour, minute, second, frac_seconds, offset,
88+
*tz) = match.groups()
89+
90+
frac_seconds_parsed = None
91+
if frac_seconds:
92+
frac_seconds_parsed = int(float(frac_seconds) * 1_000_000)
93+
else:
94+
frac_seconds_parsed = 0
95+
96+
tzinfo = None
97+
if offset == 'Z':
98+
tzinfo = timezone.utc
99+
else:
100+
hours = int(tz[2])
101+
minutes = int(tz[3])
102+
sign = 1 if tz[1] == '+' else -1
103+
104+
if minutes not in range(60):
105+
raise ValueError('minute offset must be in 0..59')
106+
107+
tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes)))
108+
109+
second_parsed = int(second)
110+
if second_parsed == 60:
111+
second_parsed = 59
112+
113+
return datetime(int(year), int(month), int(day), int(hour), int(minute),
114+
second_parsed, frac_seconds_parsed, tzinfo)
Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,77 @@
11
# Code generated by jtd-codegen for Python v0.2.0
22

3+
import re
4+
from datetime import datetime, timedelta, timezone
35
from enum import Enum
46
from typing import Any, Union, get_args, get_origin
57

6-
def _from_json(cls, data):
8+
9+
class Root(Enum):
10+
BAR = "Bar"
11+
BAZ = "Baz"
12+
FOO = "Foo"
13+
@classmethod
14+
def from_json_data(cls, data: Any) -> 'Root':
15+
return cls(data)
16+
17+
def to_json_data(self) -> Any:
18+
return self.value
19+
20+
def _from_json_data(cls: Any, data: Any) -> Any:
721
if data is None or cls in [bool, int, float, str, object] or cls is Any:
822
return data
23+
if cls is datetime:
24+
return _parse_rfc3339(data)
925
if get_origin(cls) is Union:
10-
return _from_json(get_args(cls)[0], data)
26+
return _from_json_data(get_args(cls)[0], data)
1127
if get_origin(cls) is list:
12-
return [_from_json(get_args(cls)[0], d) for d in data]
28+
return [_from_json_data(get_args(cls)[0], d) for d in data]
1329
if get_origin(cls) is dict:
14-
return { k: _from_json(get_args(cls)[1], v) for k, v in data.items() }
15-
return cls.from_json(data)
30+
return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() }
31+
return cls.from_json_data(data)
1632

17-
def _to_json(data):
33+
def _to_json_data(data: Any) -> Any:
1834
if data is None or type(data) in [bool, int, float, str, object]:
1935
return data
36+
if type(data) is datetime:
37+
return data.isoformat()
2038
if type(data) is list:
21-
return [_to_json(d) for d in data]
39+
return [_to_json_data(d) for d in data]
2240
if type(data) is dict:
23-
return { k: _to_json(v) for k, v in data.items() }
24-
return data.to_json()
41+
return { k: _to_json_data(v) for k, v in data.items() }
42+
return data.to_json_data()
2543

26-
class Root(Enum):
27-
BAR = "Bar"
28-
BAZ = "Baz"
29-
FOO = "Foo"
30-
@classmethod
31-
def from_json(cls, data) -> 'Root':
32-
return cls(data)
44+
def _parse_rfc3339(s: str) -> datetime:
45+
datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$'
46+
match = re.match(datetime_re, s)
47+
if not match:
48+
raise ValueError('Invalid RFC3339 date/time', s)
3349

34-
def to_json(self):
35-
return self.value
50+
(year, month, day, hour, minute, second, frac_seconds, offset,
51+
*tz) = match.groups()
52+
53+
frac_seconds_parsed = None
54+
if frac_seconds:
55+
frac_seconds_parsed = int(float(frac_seconds) * 1_000_000)
56+
else:
57+
frac_seconds_parsed = 0
58+
59+
tzinfo = None
60+
if offset == 'Z':
61+
tzinfo = timezone.utc
62+
else:
63+
hours = int(tz[2])
64+
minutes = int(tz[3])
65+
sign = 1 if tz[1] == '+' else -1
66+
67+
if minutes not in range(60):
68+
raise ValueError('minute offset must be in 0..59')
69+
70+
tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes)))
71+
72+
second_parsed = int(second)
73+
if second_parsed == 60:
74+
second_parsed = 59
75+
76+
return datetime(int(year), int(month), int(day), int(hour), int(minute),
77+
second_parsed, frac_seconds_parsed, tzinfo)

0 commit comments

Comments
 (0)