Skip to content

Commit a8b0a9c

Browse files
committed
Document and add a test for user-defined types
Also modifies the handling of exceptions to explicitly chain from exceptions caused while trying to instantiate a user-defined type.
1 parent c33804e commit a8b0a9c

File tree

5 files changed

+75
-5
lines changed

5 files changed

+75
-5
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Dataclass CSV makes working with CSV files easier and much better than working w
1313

1414
- Use `dataclasses` instead of dictionaries to represent the rows in the CSV file.
1515
- Take advantage of the `dataclass` properties type annotation. `DataclassReader` use the type annotation to perform validation of the data of the CSV file.
16-
- Automatic type conversion. `DataclassReader` supports `str`, `int`, `float`, `complex`, `datetime` and `bool`.
16+
- Automatic type conversion. `DataclassReader` supports `str`, `int`, `float`, `complex`, `datetime` and `bool`, as well as any type whose constructor accepts a string as its single argument.
1717
- Helps you troubleshoot issues with the data in the CSV file. `DataclassReader` will show exactly in which line of the CSV file contain errors.
1818
- Extract only the data you need. It will only parse the properties defined in the `dataclass`
1919
- Familiar syntax. The `DataclassReader` is used almost the same way as the `DictReader` in the standard library.
@@ -279,6 +279,27 @@ class User:
279279
created_at: datetime
280280
```
281281

282+
## User-defined types
283+
284+
You can use any type for a field as long as its constructor accepts a string:
285+
286+
```python
287+
class SSN:
288+
def __init__(self, val):
289+
if re.match(r"\d{9}", val):
290+
self.val = f"{val[0:3]}-{val[3:5]}-{val[5:9]}"
291+
elif re.match(r"\d{3}-\d{2}-\d{4}", val):
292+
self.val = val
293+
else:
294+
raise ValueError(f"Invalid SSN: {val!r}")
295+
296+
297+
@dataclasses.dataclass
298+
class User:
299+
name: str
300+
ssn: SSN
301+
```
302+
282303
## Copyright and License
283304

284305
Copyright (c) 2018 Daniel Furtado. Code released under BSD 3-clause license

dataclass_csv/dataclass_reader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,14 @@ def _process_row(self, row):
195195

196196
try:
197197
transformed_value = field_type(value)
198-
except ValueError:
198+
except ValueError as e:
199199
raise CsvValueError(
200200
(
201201
f'The field `{field.name}` is defined as {field.type} '
202202
f'but received a value of type {type(value)}.'
203203
),
204204
line_number=self.reader.line_num,
205-
) from None
205+
) from e
206206
else:
207207
values.append(transformed_value)
208208
return self.cls(*values)

tests/mocks.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import re
23

34
from datetime import datetime
45

@@ -90,4 +91,20 @@ class UserWithOptionalAge:
9091
@dataclasses.dataclass
9192
class UserWithDefaultDatetimeField:
9293
name: str
93-
birthday: datetime = datetime.now()
94+
birthday: datetime = datetime.now()
95+
96+
97+
class SSN:
98+
def __init__(self, val):
99+
if re.match(r"\d{9}", val):
100+
self.val = f"{val[0:3]}-{val[3:5]}-{val[5:9]}"
101+
elif re.match(r"\d{3}-\d{2}-\d{4}", val):
102+
self.val = val
103+
else:
104+
raise ValueError(f"Invalid SSN: {val!r}")
105+
106+
107+
@dataclasses.dataclass
108+
class UserWithSSN:
109+
name: str
110+
ssn: SSN

tests/test_csv_data_validation.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclass_csv import DataclassReader, CsvValueError
44

5-
from .mocks import User, UserWithDateFormatDecorator
5+
from .mocks import User, UserWithDateFormatDecorator, UserWithSSN
66

77

88
def test_should_raise_error_str_to_int_prop(create_csv):
@@ -75,3 +75,15 @@ def test_csv_header_items_with_spaces_with_prop_with_wrong_type(create_csv):
7575
with pytest.raises(CsvValueError):
7676
reader = DataclassReader(f, User)
7777
items = list(reader)
78+
79+
80+
def test_passes_through_exceptions_from_user_defined_types(create_csv):
81+
csv_file = create_csv({'name': 'User1', 'ssn': '123-45-678'})
82+
83+
with csv_file.open() as f:
84+
with pytest.raises(CsvValueError) as exc_info:
85+
reader = DataclassReader(f, UserWithSSN)
86+
items = list(reader)
87+
cause = exc_info.value.__cause__
88+
assert isinstance(cause, ValueError)
89+
assert "Invalid SSN" in str(cause)

tests/test_dataclass_reader.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
UserWithInitFalse,
1313
UserWithInitFalseAndDefaultValue,
1414
UserWithDefaultDatetimeField,
15+
UserWithSSN,
16+
SSN,
1517
)
1618

1719

@@ -178,3 +180,21 @@ def test_reader_with_datetime_default_value(create_csv):
178180
items = list(reader)
179181
assert len(items) > 0
180182
assert isinstance(items[0].birthday, datetime)
183+
184+
185+
def test_should_parse_user_defined_types(create_csv):
186+
csv_file = create_csv([
187+
{'name': 'User1', 'ssn': '123-45-6789'},
188+
{'name': 'User1', 'ssn': '123456789'},
189+
])
190+
191+
with csv_file.open() as f:
192+
reader = DataclassReader(f, UserWithSSN)
193+
items = list(reader)
194+
assert len(items) == 2
195+
196+
assert isinstance(items[0].ssn, SSN)
197+
assert items[0].ssn.val == '123-45-6789'
198+
199+
assert isinstance(items[1].ssn, SSN)
200+
assert items[1].ssn.val == '123-45-6789'

0 commit comments

Comments
 (0)