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.
Files changed (37) hide show
  1. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/PKG-INFO +33 -1
  2. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/README.md +32 -0
  3. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/__init__.py +1 -1
  4. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/extractor.py +5 -2
  5. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/type_utils.py +25 -1
  6. fields_metadata-1.3.3/tests/test_literal_types.py +89 -0
  7. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/.gitignore +0 -0
  8. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/LICENSE +0 -0
  9. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/annotations.py +0 -0
  10. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/exceptions.py +0 -0
  11. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/metadata.py +0 -0
  12. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/path.py +0 -0
  13. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/fields_metadata/py.typed +0 -0
  14. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/pyproject.toml +0 -0
  15. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/__init__.py +0 -0
  16. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/conftest.py +0 -0
  17. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/fixtures/__init__.py +0 -0
  18. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/fixtures/dataclass_models.py +0 -0
  19. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_annotations.py +0 -0
  20. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_categorical.py +0 -0
  21. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_computed_fields.py +0 -0
  22. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_exceptions.py +0 -0
  23. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_annotations.py +0 -0
  24. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_basic.py +0 -0
  25. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_composite.py +0 -0
  26. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_dataclass.py +0 -0
  27. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_errors.py +0 -0
  28. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_extensions.py +0 -0
  29. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_extractor_hooks.py +0 -0
  30. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_final_field.py +0 -0
  31. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_final_types.py +0 -0
  32. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_metadata.py +0 -0
  33. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_multivalued_annotations.py +0 -0
  34. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_path.py +0 -0
  35. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_pydantic_comprehensive.py +0 -0
  36. {fields_metadata-1.3.2 → fields_metadata-1.3.3}/tests/test_pydantic_validators.py +0 -0
  37. {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.2
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.2"
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