Skip to content

Commit 02ce1af

Browse files
authored
refactor: move IntegerField to pydantic (#1759)
Integrated pydantic logic to IntegerField. All tests pass (apart from 5 console ones not related to modifications) Fixed a few issued: - examples should enable Any type, not only strings - descriptor was broken during the merge of two kinds
1 parent e88d66f commit 02ce1af

10 files changed

Lines changed: 319 additions & 304 deletions

frictionless/fields/array_descriptor.py

Whitespace-only changes.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""base_field_descriptor.py provides the base Pydantic model for all field descriptors"""
2+
3+
from __future__ import annotations
4+
5+
from pydantic import BaseModel, Field as PydanticField, model_validator
6+
from typing import Any, Dict, List, Optional
7+
from typing_extensions import Self
8+
9+
10+
class BaseFieldDescriptor(BaseModel):
11+
"""Data model of a (unspecialised) field descriptor"""
12+
13+
name: str
14+
"""
15+
The field descriptor MUST contain a name property.
16+
"""
17+
18+
title: Optional[str] = None
19+
"""
20+
A human readable label or title for the field
21+
"""
22+
23+
description: Optional[str] = None
24+
"""
25+
A description for this field e.g. "The recipient of the funds"
26+
"""
27+
28+
missing_values: Optional[List[str]] = PydanticField(
29+
default=None, alias="missingValues"
30+
)
31+
"""
32+
A list of field values to consider as null values
33+
"""
34+
35+
example: Optional[Any] = None
36+
"""
37+
An example of a value for the field.
38+
"""
39+
40+
@model_validator(mode="before")
41+
@classmethod
42+
def compat(cls, data: Dict[str, Any]) -> Dict[str, Any]:
43+
# Backward compatibility for field.format
44+
45+
format_ = data.get("format")
46+
if format_:
47+
if format_.startswith("fmt:"):
48+
data["format"] = format_[4:]
49+
50+
return data
51+
52+
@model_validator(mode="after")
53+
def validate_example(self) -> Self:
54+
"""Validate that the example value can be converted using read_value() if available"""
55+
if self.example is not None:
56+
if hasattr(self, "read_value"):
57+
read_value_method = getattr(self, "read_value")
58+
result = read_value_method(self.example)
59+
if result is None:
60+
raise ValueError(
61+
f'example value "{self.example}" for field "{self.name}" is not valid'
62+
)
63+
64+
return self
65+

frictionless/fields/boolean.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from __future__ import annotations
12
from ..schema.field import Field
23

3-
44
class BooleanField(Field):
55
### TEMP Only required for Metadata compatibility
66
### This is required because "metadata_import" makes a distinction based
77
### on the "type" property (`is_typed_class`)
88
type = "boolean"
9+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import Any, ClassVar, List, Literal, Optional
2+
3+
from pydantic import Field as PydanticField, AliasChoices
4+
5+
from .. import settings
6+
from .base_field_descriptor import BaseFieldDescriptor
7+
from .field_constraints import BaseConstraints
8+
9+
class BooleanFieldDescriptor(BaseFieldDescriptor):
10+
"""The field contains boolean (true/false) data."""
11+
12+
type: ClassVar[Literal["boolean"]] = "boolean"
13+
14+
format: Optional[Literal["default"]] = None
15+
constraints: Optional[BaseConstraints[bool]] = None
16+
17+
true_values: Optional[List[str]] = PydanticField(
18+
default=settings.DEFAULT_TRUE_VALUES,
19+
alias="trueValues",
20+
validation_alias=AliasChoices("trueValues", "true_values"),
21+
)
22+
"""
23+
Values to be interpreted as "true" for boolean fields
24+
"""
25+
26+
false_values: Optional[List[str]] = PydanticField(
27+
default=settings.DEFAULT_FALSE_VALUES,
28+
alias="falseValues",
29+
validation_alias=AliasChoices("falseValues", "false_values"),
30+
)
31+
"""
32+
Values to be interpreted as "false" for boolean fields
33+
"""
34+
35+
def read_value(self, cell: Any) -> Optional[bool]:
36+
if isinstance(cell, bool):
37+
return cell
38+
39+
if isinstance(cell, str):
40+
if self.true_values and cell in self.true_values:
41+
return True
42+
if self.false_values and cell in self.false_values:
43+
return False
44+
45+
return None
46+
47+
def write_value(self, cell: Optional[bool]) -> Optional[str]:
48+
if self.true_values and self.false_values:
49+
return self.true_values[0] if cell else self.false_values[0]
50+
return None

frictionless/fields/date.py

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
from __future__ import annotations
22

3-
from datetime import date, datetime
4-
from typing import Any
5-
63
import attrs
7-
8-
from .. import settings
9-
from ..platform import platform
104
from ..schema import Field
115

12-
136
@attrs.define(kw_only=True, repr=False)
147
class DateField(Field):
158
type = "date"
@@ -21,49 +14,3 @@ class DateField(Field):
2114
"enum",
2215
]
2316

24-
# Read
25-
26-
# TODO: use different value_readers based on format (see string)
27-
def create_value_reader(self):
28-
# Create reader
29-
def value_reader(cell: Any):
30-
if isinstance(cell, datetime):
31-
value_time = cell.time()
32-
if (
33-
value_time.hour == 0
34-
and value_time.minute == 0
35-
and value_time.second == 0
36-
):
37-
return datetime(cell.year, cell.month, cell.day).date()
38-
else:
39-
return None
40-
if isinstance(cell, date):
41-
return cell
42-
if not isinstance(cell, str):
43-
return None
44-
try:
45-
if self.format == "default":
46-
cell = datetime.strptime(cell, settings.DEFAULT_DATE_PATTERN).date()
47-
elif self.format == "any":
48-
cell = platform.dateutil_parser.parse(cell).date()
49-
else:
50-
cell = datetime.strptime(cell, self.format).date()
51-
except Exception:
52-
return None
53-
return cell
54-
55-
return value_reader
56-
57-
# Write
58-
59-
def create_value_writer(self):
60-
# Create format
61-
format = self.format
62-
if format == settings.DEFAULT_FIELD_FORMAT:
63-
format = settings.DEFAULT_DATE_PATTERN
64-
65-
# Create writer
66-
def value_writer(cell: Any):
67-
return cell.strftime(format)
68-
69-
return value_writer
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import datetime
2+
from typing import Any, Literal, Optional
3+
4+
5+
from .. import settings
6+
from .base_field_descriptor import BaseFieldDescriptor
7+
from .field_constraints import ValueConstraints
8+
9+
10+
class DateFieldDescriptor(BaseFieldDescriptor):
11+
"""The field contains a date without a time."""
12+
13+
type: Literal["date"] = "date"
14+
format: Optional[str] = None
15+
constraints: Optional[ValueConstraints[str]] = None
16+
17+
def read_value(self, cell: Any) -> Optional[datetime.date]:
18+
from datetime import date, datetime
19+
from ..platform import platform
20+
21+
if isinstance(cell, datetime):
22+
value_time = cell.time()
23+
if (
24+
value_time.hour == 0
25+
and value_time.minute == 0
26+
and value_time.second == 0
27+
):
28+
return datetime(cell.year, cell.month, cell.day).date()
29+
else:
30+
return None
31+
if isinstance(cell, date):
32+
return cell
33+
if not isinstance(cell, str):
34+
return None
35+
try:
36+
format_value = self.format or "default"
37+
if format_value == "default":
38+
cell = datetime.strptime(cell, settings.DEFAULT_DATE_PATTERN).date()
39+
elif format_value == "any":
40+
cell = platform.dateutil_parser.parse(cell).date()
41+
else:
42+
cell = datetime.strptime(cell, format_value).date()
43+
except Exception:
44+
return None
45+
return cell
46+
47+
def write_value(self, cell: Optional[datetime.date]) -> Optional[str]:
48+
if cell is None:
49+
return None
50+
format_value = self.format or "default"
51+
if format_value == settings.DEFAULT_FIELD_FORMAT:
52+
format_value = settings.DEFAULT_DATE_PATTERN
53+
return cell.strftime(format_value)

0 commit comments

Comments
 (0)