fields-metadata 1.3.2__tar.gz → 1.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/PKG-INFO +33 -1
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/README.md +32 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/__init__.py +1 -1
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/extractor.py +5 -2
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/type_utils.py +25 -1
- fields_metadata-1.3.3/tests/test_literal_types.py +89 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/.gitignore +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/LICENSE +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/annotations.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/exceptions.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/metadata.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/path.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/py.typed +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/pyproject.toml +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/__init__.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/conftest.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/fixtures/__init__.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/fixtures/dataclass_models.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_annotations.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_categorical.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_computed_fields.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_exceptions.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_annotations.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_basic.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_composite.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_dataclass.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_errors.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_extensions.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_hooks.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_final_field.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_final_types.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_metadata.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_multivalued_annotations.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_path.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_pydantic_comprehensive.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_pydantic_validators.py +0 -0
- {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_type_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fields-metadata
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: A Python library for extracting field metadata from dataclasses and Pydantic models
|
|
5
5
|
Project-URL: Homepage, https://github.com/Kencho1/fields-metadata
|
|
6
6
|
Project-URL: Repository, https://github.com/Kencho1/fields-metadata
|
|
@@ -238,6 +238,38 @@ metadata = extractor.extract(User)
|
|
|
238
238
|
assert metadata["email_domain"].computed is True
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
#### Literal Types
|
|
242
|
+
|
|
243
|
+
Literal types are automatically unwrapped to their underlying type:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from dataclasses import dataclass
|
|
247
|
+
from typing import Literal
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class Task:
|
|
251
|
+
name: str
|
|
252
|
+
status: Literal["pending", "in_progress", "done"]
|
|
253
|
+
priority: Literal[1, 2, 3]
|
|
254
|
+
score: Literal[1.0, 2.0, 3.0]
|
|
255
|
+
|
|
256
|
+
extractor = MetadataExtractor()
|
|
257
|
+
metadata = extractor.extract(Task)
|
|
258
|
+
|
|
259
|
+
# Literal types are unwrapped to their underlying type
|
|
260
|
+
assert metadata["status"].field_type == str
|
|
261
|
+
assert metadata["status"].effective_type == str
|
|
262
|
+
assert metadata["status"].categorical is True
|
|
263
|
+
|
|
264
|
+
assert metadata["priority"].field_type == int
|
|
265
|
+
assert metadata["priority"].numeric is True
|
|
266
|
+
assert metadata["priority"].categorical is True # int is categorical by default
|
|
267
|
+
|
|
268
|
+
assert metadata["score"].field_type == float
|
|
269
|
+
assert metadata["score"].numeric is True
|
|
270
|
+
assert metadata["score"].categorical is False # float is non-categorical
|
|
271
|
+
```
|
|
272
|
+
|
|
241
273
|
### Nested Structures
|
|
242
274
|
|
|
243
275
|
The library automatically traverses nested structures and builds dot-separated paths:
|
|
@@ -207,6 +207,38 @@ metadata = extractor.extract(User)
|
|
|
207
207
|
assert metadata["email_domain"].computed is True
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
#### Literal Types
|
|
211
|
+
|
|
212
|
+
Literal types are automatically unwrapped to their underlying type:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from dataclasses import dataclass
|
|
216
|
+
from typing import Literal
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class Task:
|
|
220
|
+
name: str
|
|
221
|
+
status: Literal["pending", "in_progress", "done"]
|
|
222
|
+
priority: Literal[1, 2, 3]
|
|
223
|
+
score: Literal[1.0, 2.0, 3.0]
|
|
224
|
+
|
|
225
|
+
extractor = MetadataExtractor()
|
|
226
|
+
metadata = extractor.extract(Task)
|
|
227
|
+
|
|
228
|
+
# Literal types are unwrapped to their underlying type
|
|
229
|
+
assert metadata["status"].field_type == str
|
|
230
|
+
assert metadata["status"].effective_type == str
|
|
231
|
+
assert metadata["status"].categorical is True
|
|
232
|
+
|
|
233
|
+
assert metadata["priority"].field_type == int
|
|
234
|
+
assert metadata["priority"].numeric is True
|
|
235
|
+
assert metadata["priority"].categorical is True # int is categorical by default
|
|
236
|
+
|
|
237
|
+
assert metadata["score"].field_type == float
|
|
238
|
+
assert metadata["score"].numeric is True
|
|
239
|
+
assert metadata["score"].categorical is False # float is non-categorical
|
|
240
|
+
```
|
|
241
|
+
|
|
210
242
|
### Nested Structures
|
|
211
243
|
|
|
212
244
|
The library automatically traverses nested structures and builds dot-separated paths:
|
|
@@ -19,7 +19,7 @@ from fields_metadata.extractor import MetadataExtractor
|
|
|
19
19
|
from fields_metadata.metadata import FieldMetadata
|
|
20
20
|
from fields_metadata.path import FieldsMetadataMap, FieldsPath
|
|
21
21
|
|
|
22
|
-
__version__ = "1.3.
|
|
22
|
+
__version__ = "1.3.3"
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"MetadataExtractor",
|
|
@@ -30,6 +30,7 @@ from fields_metadata.type_utils import (
|
|
|
30
30
|
is_numeric_type,
|
|
31
31
|
is_optional_type,
|
|
32
32
|
unwrap_annotated,
|
|
33
|
+
unwrap_literal_type,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
TMetadata = TypeVar("TMetadata", bound=FieldMetadata)
|
|
@@ -373,8 +374,6 @@ class MetadataExtractor(Generic[TMetadata]):
|
|
|
373
374
|
parent_field_path = field_meta.parent_field
|
|
374
375
|
if parent_field_path in metadata:
|
|
375
376
|
parent_meta = metadata[parent_field_path]
|
|
376
|
-
# Set the full path to the field (can be used as lookup key)
|
|
377
|
-
# e.g., for "department.location.building_id", set "department.location.building_id"
|
|
378
377
|
parent_meta.extra["suggested_human_sorting_field"] = field_path
|
|
379
378
|
|
|
380
379
|
def _get_fields(self, obj_type: type[Any]) -> list[tuple[str, type[Any]]]:
|
|
@@ -516,6 +515,10 @@ class MetadataExtractor(Generic[TMetadata]):
|
|
|
516
515
|
|
|
517
516
|
clean_type = unwrap_annotated(base_type)
|
|
518
517
|
|
|
518
|
+
literal_underlying_type = unwrap_literal_type(clean_type)
|
|
519
|
+
if literal_underlying_type is not None:
|
|
520
|
+
clean_type = literal_underlying_type
|
|
521
|
+
|
|
519
522
|
is_multivalued = is_multivalued_type(clean_type)
|
|
520
523
|
items_type = get_items_type(clean_type) if is_multivalued else None
|
|
521
524
|
effective_type = items_type if is_multivalued else clean_type
|
|
@@ -4,7 +4,7 @@ import dataclasses
|
|
|
4
4
|
import typing
|
|
5
5
|
from datetime import date, datetime, time, timedelta
|
|
6
6
|
from decimal import Decimal
|
|
7
|
-
from typing import Any, cast, get_args, get_origin
|
|
7
|
+
from typing import Any, Literal, cast, get_args, get_origin
|
|
8
8
|
|
|
9
9
|
from fields_metadata.exceptions import InvalidTypeUnionError, NoneTypeFieldError
|
|
10
10
|
|
|
@@ -196,6 +196,30 @@ def is_numeric_type(type_hint: type[Any]) -> bool:
|
|
|
196
196
|
return False
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
def unwrap_literal_type(type_hint: type[Any]) -> type[Any] | None:
|
|
200
|
+
"""
|
|
201
|
+
Extract the underlying type from a Literal type.
|
|
202
|
+
|
|
203
|
+
For Literal types, returns the type of the literal value(s).
|
|
204
|
+
For example: Literal['abc'] -> str, Literal[1, 2, 3] -> int
|
|
205
|
+
|
|
206
|
+
:param type_hint: The type to check
|
|
207
|
+
:return: The underlying type if it's a Literal, None otherwise
|
|
208
|
+
"""
|
|
209
|
+
origin = get_origin(type_hint)
|
|
210
|
+
|
|
211
|
+
# Check if this is a Literal type
|
|
212
|
+
# In Python 3.8+, Literal's origin is typing.Literal
|
|
213
|
+
if origin is Literal:
|
|
214
|
+
args = get_args(type_hint)
|
|
215
|
+
if args:
|
|
216
|
+
# Get the type of the first literal value
|
|
217
|
+
# All values in a Literal should be of the same type or compatible types
|
|
218
|
+
return type(args[0])
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
199
223
|
def is_computed_field(obj_class: type[Any], field_name: str) -> bool:
|
|
200
224
|
"""
|
|
201
225
|
Check if a field is computed (property or pydantic computed_field).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tests for Literal type handling."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from fields_metadata.extractor import MetadataExtractor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_literal_string_unwrapped_to_str():
|
|
10
|
+
"""Test that Literal['value'] is unwrapped to str."""
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Example:
|
|
14
|
+
status: Literal["active", "inactive"]
|
|
15
|
+
|
|
16
|
+
extractor = MetadataExtractor()
|
|
17
|
+
metadata = extractor.extract(Example)
|
|
18
|
+
|
|
19
|
+
assert metadata["status"].field_type == str
|
|
20
|
+
assert metadata["status"].effective_type == str
|
|
21
|
+
assert metadata["status"].original_annotation == Literal["active", "inactive"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_literal_int_unwrapped_to_int():
|
|
25
|
+
"""Test that Literal[1, 2, 3] is unwrapped to int."""
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Example:
|
|
29
|
+
priority: Literal[1, 2, 3]
|
|
30
|
+
|
|
31
|
+
extractor = MetadataExtractor()
|
|
32
|
+
metadata = extractor.extract(Example)
|
|
33
|
+
|
|
34
|
+
assert metadata["priority"].field_type == int
|
|
35
|
+
assert metadata["priority"].effective_type == int
|
|
36
|
+
assert metadata["priority"].numeric is True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_literal_bool_unwrapped_to_bool():
|
|
40
|
+
"""Test that Literal[True, False] is unwrapped to bool."""
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Example:
|
|
44
|
+
flag: Literal[True, False]
|
|
45
|
+
|
|
46
|
+
extractor = MetadataExtractor()
|
|
47
|
+
metadata = extractor.extract(Example)
|
|
48
|
+
|
|
49
|
+
assert metadata["flag"].field_type == bool
|
|
50
|
+
assert metadata["flag"].effective_type == bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_optional_literal_unwrapped():
|
|
54
|
+
"""Test that Optional[Literal['value']] is unwrapped to str."""
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Example:
|
|
58
|
+
status: Literal["pending", "done"] | None = None
|
|
59
|
+
|
|
60
|
+
extractor = MetadataExtractor()
|
|
61
|
+
metadata = extractor.extract(Example)
|
|
62
|
+
|
|
63
|
+
assert metadata["status"].field_type == str
|
|
64
|
+
assert metadata["status"].effective_type == str
|
|
65
|
+
assert metadata["status"].optional is True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_literal_categorical_behavior():
|
|
69
|
+
"""Test that Literal fields have correct categorical behavior."""
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Example:
|
|
73
|
+
status: Literal["active", "inactive"]
|
|
74
|
+
priority: Literal[1, 2, 3]
|
|
75
|
+
score: Literal[1.0, 2.0, 3.0]
|
|
76
|
+
|
|
77
|
+
extractor = MetadataExtractor()
|
|
78
|
+
metadata = extractor.extract(Example)
|
|
79
|
+
|
|
80
|
+
# String literals should be categorical
|
|
81
|
+
assert metadata["status"].categorical is True
|
|
82
|
+
|
|
83
|
+
# Int literals should be categorical (int is categorical by default)
|
|
84
|
+
assert metadata["priority"].categorical is True
|
|
85
|
+
assert metadata["priority"].numeric is True
|
|
86
|
+
|
|
87
|
+
# Float literals should be non-categorical (float is inherently non-categorical)
|
|
88
|
+
assert metadata["score"].categorical is False
|
|
89
|
+
assert metadata["score"].numeric is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|