Skip to content

Commit 03a2953

Browse files
authored
feat: use msgspec JSON encoder (#6)
1 parent 4bf0d29 commit 03a2953

File tree

4 files changed

+53
-42
lines changed

4 files changed

+53
-42
lines changed

tests/test_reader.py

+18-18
Original file line numberDiff line numberDiff line change
@@ -121,28 +121,28 @@ def test_reader_will_write_a_complicated_record(tmp_path: Path) -> None:
121121
},
122122
field10=True,
123123
field11=None,
124-
field12=1,
124+
field12=0.2,
125125
)
126126
with TsvRecordWriter.from_path(tmp_path / "test.txt", ComplexMetric) as writer:
127127
assert (tmp_path / "test.txt").read_text() == ""
128128
writer.write(metric)
129-
assert (tmp_path / "test.txt").read_text() == "\t".join([
130-
"1",
131-
"'my\tname'",
132-
"0.2",
133-
"[1, 2, 3]",
134-
"[3, 4, 5]",
135-
"[5, 6, 7]",
136-
'{"field1": 1, "field2": 2}',
137-
'{"field1": 10, "field2": "hi-mom", "field3": null}',
138-
", ".join([
139-
r'{"first": {"field1": 2, "field2": "hi-dad", "field3": 0.2}',
140-
r'"second": {"field1": 3, "field2": "hi-all", "field3": 0.3}}',
141-
]),
142-
"true",
143-
"null",
144-
"1\n",
145-
])
129+
130+
expected: str = (
131+
"1"
132+
+ "\t'my\tname'"
133+
+ "\t0.2"
134+
+ "\t[1,2,3]"
135+
+ "\t[3,4,5]"
136+
+ "\t[5,6,7]"
137+
+ '\t{"field1":1,"field2":2}'
138+
+ '\t{"field1":10,"field2":"hi-mom","field3":null}'
139+
+ '\t{"first":{"field1":2,"field2":"hi-dad","field3":0.2}'
140+
+ ',"second":{"field1":3,"field2":"hi-all","field3":0.3}}'
141+
+ "\ttrue"
142+
+ "\tnull"
143+
+ "\t0.2\n"
144+
)
145+
assert (tmp_path / "test.txt").read_text() == expected
146146

147147
with TsvRecordReader.from_path(tmp_path / "test.txt", ComplexMetric, header=False) as reader:
148148
assert list(reader) == [metric]

tests/test_writer.py

+16-17
Original file line numberDiff line numberDiff line change
@@ -96,23 +96,22 @@ def test_writer_will_write_a_complicated_record(tmp_path: Path) -> None:
9696
with TsvRecordWriter.from_path(tmp_path / "test.txt", ComplexMetric) as writer:
9797
assert (tmp_path / "test.txt").read_text() == ""
9898
writer.write(metric)
99-
assert (tmp_path / "test.txt").read_text() == "\t".join([
100-
"1",
101-
"'my\tname'",
102-
"0.2",
103-
"[1, 2, 3]",
104-
"[3, 4, 5]",
105-
"[5, 6, 7]",
106-
'{"field1": 1, "field2": 2}',
107-
'{"field1": 10, "field2": "hi-mom", "field3": null}',
108-
", ".join([
109-
r'{"first": {"field1": 2, "field2": "hi-dad", "field3": 0.2}',
110-
r'"second": {"field1": 3, "field2": "hi-all", "field3": 0.3}}',
111-
]),
112-
"true",
113-
"null",
114-
"0.2\n",
115-
])
99+
expected: str = (
100+
"1"
101+
+ "\t'my\tname'"
102+
+ "\t0.2"
103+
+ "\t[1,2,3]"
104+
+ "\t[3,4,5]"
105+
+ "\t[5,6,7]"
106+
+ '\t{"field1":1,"field2":2}'
107+
+ '\t{"field1":10,"field2":"hi-mom","field3":null}'
108+
+ '\t{"first":{"field1":2,"field2":"hi-dad","field3":0.2}'
109+
+ ',"second":{"field1":3,"field2":"hi-all","field3":0.3}}'
110+
+ "\ttrue"
111+
+ "\tnull"
112+
+ "\t0.2\n"
113+
)
114+
assert (tmp_path / "test.txt").read_text() == expected
116115

117116

118117
def test_writer_can_write_with_a_custom_callback(tmp_path: Path) -> None:

typeline/_reader.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def from_path(
211211
"""Construct a delimited data reader from a file path.
212212
213213
Args:
214-
path: the pat to the file to read delimited data from.
214+
path: the path to the file to read delimited data from.
215215
record_type: the type of the object we will be writing.
216216
header: whether we expect the first line to be a header or not.
217217
comment_prefixes: skip lines that have any of these string prefixes.

typeline/_writer.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import csv
2-
import json
32
from abc import ABC
43
from abc import abstractmethod
54
from contextlib import AbstractContextManager
@@ -16,6 +15,7 @@
1615
from typing import final
1716

1817
from msgspec import to_builtins
18+
from msgspec.json import Encoder as JSONEncoder
1919
from typing_extensions import Self
2020
from typing_extensions import override
2121

@@ -39,12 +39,18 @@ def __init__(self, handle: TextIOWrapper, record_type: type[RecordType]) -> None
3939
if not is_dataclass(record_type):
4040
raise ValueError("record_type is not a dataclass but must be!")
4141

42+
# Initialize and save internal attributes of this class.
4243
self._handle: TextIOWrapper = handle
4344
self._record_type: type[RecordType] = record_type
4445

46+
# Inspect the record type and save the fields, field names, and field types.
4547
self._fields: tuple[Field[Any], ...] = fields_of(record_type)
4648
self._header: list[str] = [field.name for field in fields_of(record_type)]
4749

50+
# Build a JSON encoder for intermediate data conversion (after dataclass, before delimited).
51+
self._encoder: JSONEncoder = JSONEncoder()
52+
53+
# Build the delimited dictionary reader, filtering out any comment lines along the way.
4854
self._writer: DictWriter[str] = DictWriter(
4955
handle,
5056
fieldnames=self._header,
@@ -90,11 +96,12 @@ def write(self, record: RecordType) -> None:
9096
)
9197

9298
encoded = {name: self._encode(getattr(record, name)) for name in self._header}
93-
builtin = {
94-
name: (json.dumps(value) if not isinstance(value, str) else value)
95-
for name, value in cast(dict[str, Any], to_builtins(encoded, str_keys=True)).items()
99+
builtin = cast(dict[str, Any], to_builtins(encoded, str_keys=True))
100+
as_dict = {
101+
name: value if isinstance(value, str) else self._encoder.encode(value).decode("utf-8")
102+
for name, value in builtin.items()
96103
}
97-
self._writer.writerow(builtin)
104+
self._writer.writerow(as_dict)
98105

99106
return None
100107

@@ -112,7 +119,12 @@ def close(self) -> None:
112119
def from_path(
113120
cls, path: Path | str, record_type: type[RecordType]
114121
) -> "DelimitedRecordWriter[RecordType]":
115-
"""Construct a delimited struct writer from a file path."""
122+
"""Construct a delimited data writer from a file path.
123+
124+
Args:
125+
path: the path to the file to write delimited data to.
126+
record_type: the type of the object we will be writing.
127+
"""
116128
writer = cls(Path(path).open("w"), record_type)
117129
return writer
118130

0 commit comments

Comments
 (0)