Skip to content

Commit 6b103a2

Browse files
kraktusdfurtado
andauthored
work from (#66)
Implementation of workflows for testing, introduction of uv for better dependency management and package creation, code improvements and fixes. --------- Co-authored-by: kraktus <[email protected]> Co-authored-by: Daniel Furtado <[email protected]>
1 parent b43d395 commit 6b103a2

26 files changed

+498
-278
lines changed

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Set update schedule for GitHub Actions
2+
3+
version: 2
4+
updates:
5+
6+
- package-ecosystem: "github-actions"
7+
directory: "/"
8+
schedule:
9+
# Check for updates to GitHub Actions every week
10+
interval: "weekly"

.github/workflows/test.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: ["master"]
6+
paths:
7+
- ".github/workflows/test.yml"
8+
- "**.py"
9+
- "uv.lock"
10+
- "pyproject.toml"
11+
pull_request:
12+
paths:
13+
- ".github/workflows/test.yml"
14+
- "**.py"
15+
- "uv.lock"
16+
- "pyproject.toml"
17+
18+
jobs:
19+
test:
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
24+
os: [ubuntu-latest, macos-latest, windows-latest]
25+
runs-on: ${{ matrix.os }}
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v4
29+
- name: Install uv and set the python version ${{ matrix.python-version }}
30+
uses: astral-sh/setup-uv@v5
31+
with:
32+
python-version: ${{ matrix.python-version }}
33+
- name: Install the project
34+
run: uv sync --all-extras --dev
35+
- name: Run tests
36+
run: uv run pytest

.gitignore

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
__pycache__
2-
*.pyc
3-
.idea
4-
env/
5-
*.swo
6-
*.swp
7-
*.*~
8-
*.egg
9-
*.egg-info
10-
.#*.*
11-
TAGS
12-
docs
13-
.mypy_cache
14-
.pytest_cache
15-
build
16-
dist
17-
.eggs
18-
.vscode
19-
gmon.out
20-
.vim
21-
pyproject.toml
1+
__pycache__
2+
*.pyc
3+
.idea
4+
env/
5+
*.swo
6+
*.swp
7+
*.*~
8+
*.egg
9+
*.egg-info
10+
.#*.*
11+
TAGS
12+
docs
13+
.mypy_cache
14+
.pytest_cache
15+
build
16+
dist
17+
.eggs
18+
.vscode
19+
gmon.out
20+
.vim
21+
.aider*

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

AUTHORS.md

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

33
## Development Lead
44

5-
* Daniel Furtado <[email protected]>
5+
* Daniel Furtado
66

77
## Contributors
88

9+
* Kraktus
910
* Nick Schober
1011
* Zoltan Ivanfi
1112
* Alec Benzer

MANIFEST.in

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ Dataclass CSV makes working with CSV files easier and much better than working w
2323
using a list of instances of a dataclass.
2424

2525

26+
## Thanks
27+
28+
Thank you to all the amazing contributors who have supported this project over the years, with special thanks to [@kraktus](https://github.com/kraktus) for setting up GitHub Actions, improving automation for package creation, and making numerous code enhancements.
29+
30+
2631
## Installation
2732

2833
```shell

dataclass_csv/__init__.pyi

Lines changed: 0 additions & 7 deletions
This file was deleted.

dataclass_csv/dataclass_reader.py

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def strtobool(value: str) -> bool:
2424
return value.lower() in trueValues
2525

2626

27+
T = TypeVar("T")
28+
2729
def _verify_duplicate_header_items(header):
2830
if header is not None and len(header) == 0:
2931
return
@@ -42,6 +44,17 @@ def _verify_duplicate_header_items(header):
4244
)
4345

4446

47+
def strtobool(value: str) -> bool:
48+
trueValues = ["true", "yes", "t", "y", "on", "1"]
49+
50+
validValues = ["false", "no", "f", "n", "off", "0", *trueValues]
51+
52+
if value.lower() not in validValues:
53+
raise ValueError(f"invalid boolean value {value}")
54+
55+
return value.lower() in trueValues
56+
57+
4558
def is_union_type(t):
4659
if hasattr(t, "__origin__") and t.__origin__ is Union:
4760
return True
@@ -60,7 +73,7 @@ class DataclassReader(Generic[T]):
6073
def __init__(
6174
self,
6275
f: Any,
63-
cls: Type[T],
76+
klass: Type[T],
6477
fieldnames: Optional[Sequence[str]] = None,
6578
restkey: Optional[str] = None,
6679
restval: Optional[Any] = None,
@@ -72,10 +85,10 @@ def __init__(
7285
if not f:
7386
raise ValueError("The f argument is required.")
7487

75-
if cls is None or not dataclasses.is_dataclass(cls):
76-
raise ValueError("cls argument needs to be a dataclass.")
88+
if klass is None or not dataclasses.is_dataclass(klass):
89+
raise ValueError("klass argument needs to be a dataclass.")
7790

78-
self._cls = cls
91+
self._cls = klass
7992
self._optional_fields = self._get_optional_fields()
8093
self._field_mapping: Dict[str, Dict[str, Any]] = {}
8194

@@ -88,7 +101,7 @@ def __init__(
88101
if validate_header:
89102
_verify_duplicate_header_items(self._reader.fieldnames)
90103

91-
self.type_hints = typing.get_type_hints(cls)
104+
self.type_hints = typing.get_type_hints(klass)
92105

93106
def _get_optional_fields(self):
94107
return [
@@ -120,53 +133,52 @@ def _get_possible_keys(self, fieldname, row):
120133
def _get_value(self, row, field):
121134
is_field_mapped = False
122135

123-
try:
124-
if field.name in self._field_mapping.keys():
125-
is_field_mapped = True
126-
key = self._field_mapping.get(field.name)
127-
else:
128-
key = field.name
136+
if field.name in self._field_mapping.keys():
137+
is_field_mapped = True
138+
key = self._field_mapping.get(field.name)
139+
else:
140+
key = field.name
129141

130-
if key in row.keys():
131-
value = row[key]
132-
else:
142+
if key in row.keys():
143+
value = row[key]
144+
else:
145+
try:
133146
possible_key = self._get_possible_keys(field.name, row)
134147
key = possible_key if possible_key else key
135148
value = row[key]
136-
137-
except KeyError:
138-
if field.name in self._optional_fields:
139-
return self._get_default_value(field)
140-
else:
141-
keyerror_message = f"The value for the column `{field.name}`"
142-
if is_field_mapped:
143-
keyerror_message = f"The value for the mapped column `{key}`"
144-
raise KeyError(f"{keyerror_message} is missing in the CSV file")
145-
else:
146-
if not value and field.name in self._optional_fields:
147-
return self._get_default_value(field)
148-
elif not value and field.name not in self._optional_fields:
149-
raise ValueError(f"The field `{field.name}` is required.")
150-
elif (
151-
value
152-
and field.type is str
153-
and not len(value.strip())
154-
and not self._get_metadata_option(field, "accept_whitespaces")
155-
):
156-
raise ValueError(
157-
(
158-
f"It seems like the value of `{field.name}` contains "
159-
"only white spaces. To allow white spaces to all "
160-
"string fields, use the @accept_whitespaces "
161-
"decorator. "
162-
"To allow white spaces specifically for the field "
163-
f"`{field.name}` change its definition to: "
164-
f"`{field.name}: str = field(metadata="
165-
"{'accept_whitespaces': True})`."
166-
)
149+
except KeyError:
150+
if field.name in self._optional_fields:
151+
return self._get_default_value(field)
152+
else:
153+
keyerror_message = f"The value for the column `{field.name}`"
154+
if is_field_mapped:
155+
keyerror_message = f"The value for the mapped column `{key}`"
156+
raise KeyError(f"{keyerror_message} is missing in the CSV file")
157+
158+
if not value and field.name in self._optional_fields:
159+
return self._get_default_value(field)
160+
elif not value and field.name not in self._optional_fields:
161+
raise ValueError(f"The field `{field.name}` is required.")
162+
elif (
163+
value
164+
and field.type is str
165+
and not len(value.strip())
166+
and not self._get_metadata_option(field, "accept_whitespaces")
167+
):
168+
raise ValueError(
169+
(
170+
f"It seems like the value of `{field.name}` contains "
171+
"only white spaces. To allow white spaces to all "
172+
"string fields, use the @accept_whitespaces "
173+
"decorator. "
174+
"To allow white spaces specifically for the field "
175+
f"`{field.name}` change its definition to: "
176+
f"`{field.name}: str = field(metadata="
177+
"{'accept_whitespaces': True})`."
167178
)
168-
else:
169-
return value
179+
)
180+
else:
181+
return value
170182

171183
def _parse_date_value(self, field, date_value, field_type):
172184
dateformat = self._get_metadata_option(field, "dateformat")
@@ -231,7 +243,7 @@ def _process_row(self, row) -> T:
231243
transformed_value = (
232244
value
233245
if isinstance(value, bool)
234-
else strtobool(str(value).strip()) == 1
246+
else strtobool(str(value).strip())
235247
)
236248
except ValueError as ex:
237249
raise CsvValueError(ex, line_number=self._reader.line_num) from None

dataclass_csv/dataclass_reader.pyi

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)