1
- import functools
2
1
import sys
2
+ import types as python_types
3
3
import typing
4
- from enum import Enum
5
- from functools import partial
6
- from pathlib import PurePath
7
4
from typing import Any
8
- from typing import Callable
9
5
from typing import Dict
6
+ from typing import FrozenSet
10
7
from typing import Iterable
11
8
from typing import List
12
9
from typing import Literal
13
- from typing import Optional
10
+ from typing import Mapping
11
+ from typing import Protocol
14
12
from typing import Tuple
15
13
from typing import Type
16
14
from typing import Union
17
15
18
- if sys .version_info >= (3 , 12 ):
16
+ if sys .version_info >= (3 , 10 ):
19
17
from typing import TypeAlias
20
18
else :
21
19
from typing_extensions import TypeAlias
22
20
23
- import attr
21
+ import dataclasses
22
+ import functools
23
+ from dataclasses import MISSING as DATACLASSES_MISSING
24
+ from dataclasses import fields as get_dataclasses_fields
25
+ from dataclasses import is_dataclass as is_dataclasses_class
26
+ from enum import Enum
27
+ from functools import partial
28
+ from pathlib import PurePath
29
+ from typing import TYPE_CHECKING
30
+ from typing import Callable
31
+ from typing import Optional
32
+ from typing import TypeVar
24
33
25
34
import fgpyo .util .types as types
26
35
36
+ attr : Optional [python_types .ModuleType ]
37
+ MISSING : FrozenSet [Any ]
38
+
39
+ try :
40
+ import attr
41
+
42
+ _use_attr = True
43
+ from attr import fields as get_attr_fields
44
+ from attr import fields_dict as get_attr_fields_dict
45
+
46
+ Attribute : TypeAlias = attr .Attribute # type: ignore[name-defined, no-redef]
47
+ # dataclasses and attr have internal tokens for missing values, join into a set so that we can
48
+ # check if a value is missing without knowing the type of backing class
49
+ MISSING = frozenset ({DATACLASSES_MISSING , attr .NOTHING })
50
+ except ImportError : # pragma: no cover
51
+ _use_attr = False
52
+ attr = None
53
+ Attribute : TypeAlias = TypeVar ("Attribute" , bound = object ) # type: ignore[misc, assignment, no-redef] # noqa: E501
54
+
55
+ # define empty placeholders for getting attr fields as a tuple or dict. They will never be
56
+ # called because the import failed; but they're here to ensure that the function is defined in
57
+ # sections of code that don't know if the import was successful or not.
58
+
59
+ def get_attr_fields (cls : type ) -> Tuple [dataclasses .Field , ...]: # type: ignore[misc]
60
+ """Get tuple of fields for attr class. attrs isn't imported so return empty tuple."""
61
+ return ()
62
+
63
+ def get_attr_fields_dict (cls : type ) -> Dict [str , dataclasses .Field ]: # type: ignore[misc]
64
+ """Get dict of name->field for attr class. attrs isn't imported so return empty dict."""
65
+ return {}
66
+
67
+ # for consistency with successful import of attr, create a set for missing values
68
+ MISSING = frozenset ({DATACLASSES_MISSING })
69
+
70
+ if TYPE_CHECKING : # pragma: no cover
71
+ from _typeshed import DataclassInstance as DataclassesProtocol
72
+ else :
73
+
74
+ class DataclassesProtocol (Protocol ):
75
+ __dataclasses_fields__ : Dict [str , dataclasses .Field ]
76
+
77
+
78
+ if TYPE_CHECKING and _use_attr : # pragma: no cover
79
+ from attr import AttrsInstance
80
+ else :
81
+
82
+ class AttrsInstance (Protocol ): # type: ignore[no-redef]
83
+ __attrs_attrs__ : Dict [str , Any ]
84
+
85
+
86
+ def is_attr_class (cls : type ) -> bool : # type: ignore[arg-type]
87
+ """Return True if the class is an attr class, and False otherwise"""
88
+ return hasattr (cls , "__attrs_attrs__" )
89
+
90
+
91
+ _MISSING_OR_NONE : FrozenSet [Any ] = frozenset ({* MISSING , None })
92
+ """Set of values that are considered missing or None for dataclasses or attr classes"""
93
+ _DataclassesOrAttrClass : TypeAlias = Union [DataclassesProtocol , AttrsInstance ]
94
+ """
95
+ TypeAlias for dataclasses or attr classes. Mostly nonsense because they are not true types, they
96
+ are traits, but there is no python trait-tester.
97
+ """
98
+ FieldType : TypeAlias = Union [dataclasses .Field , Attribute ]
99
+ """
100
+ TypeAlias for dataclass Fields or attrs Attributes. It will correspond to the correct type for the
101
+ corresponding _DataclassesOrAttrClass
102
+ """
103
+
104
+
105
+ def _get_dataclasses_fields_dict (
106
+ class_or_instance : Union [DataclassesProtocol , Type [DataclassesProtocol ]],
107
+ ) -> Dict [str , dataclasses .Field ]:
108
+ """Get a dict from field name to Field for a dataclass class or instance."""
109
+ return {field .name : field for field in get_dataclasses_fields (class_or_instance )}
110
+
27
111
28
112
class ParserNotFoundException (Exception ):
29
113
pass
@@ -67,7 +151,7 @@ def split_at_given_level(
67
151
return out_vals
68
152
69
153
70
- NoneType = type (None )
154
+ NoneType : TypeAlias = type (None ) # type: ignore[no-redef]
71
155
72
156
73
157
def list_parser (
@@ -305,27 +389,58 @@ def get_parser() -> partial:
305
389
return parser
306
390
307
391
392
+ def get_fields_dict (
393
+ cls : Union [_DataclassesOrAttrClass , Type [_DataclassesOrAttrClass ]]
394
+ ) -> Mapping [str , FieldType ]:
395
+ """Get the fields dict from either a dataclasses or attr dataclass (or instance)"""
396
+ if is_dataclasses_class (cls ):
397
+ return _get_dataclasses_fields_dict (cls ) # type: ignore[arg-type]
398
+ elif is_attr_class (cls ): # type: ignore[arg-type]
399
+ return get_attr_fields_dict (cls ) # type: ignore[arg-type]
400
+ else :
401
+ raise TypeError ("cls must a dataclasses or attr class" )
402
+
403
+
404
+ def get_fields (
405
+ cls : Union [_DataclassesOrAttrClass , Type [_DataclassesOrAttrClass ]]
406
+ ) -> Tuple [FieldType , ...]:
407
+ """Get the fields tuple from either a dataclasses or attr dataclass (or instance)"""
408
+ if is_dataclasses_class (cls ):
409
+ return get_dataclasses_fields (cls ) # type: ignore[arg-type]
410
+ elif is_attr_class (cls ): # type: ignore[arg-type]
411
+ return get_attr_fields (cls ) # type: ignore[arg-type]
412
+ else :
413
+ raise TypeError ("cls must a dataclasses or attr class" )
414
+
415
+
416
+ _AttrFromType = TypeVar ("_AttrFromType" )
417
+ """TypeVar to allow attr_from to be used with either an attr class or a dataclasses class"""
418
+
419
+
308
420
def attr_from (
309
- cls : Type , kwargs : Dict [str , str ], parsers : Optional [Dict [type , Callable [[str ], Any ]]] = None
310
- ) -> Any :
311
- """Builds an attr class from key-word arguments
421
+ cls : Type [_AttrFromType ],
422
+ kwargs : Dict [str , str ],
423
+ parsers : Optional [Dict [type , Callable [[str ], Any ]]] = None ,
424
+ ) -> _AttrFromType :
425
+ """Builds an attr or dataclasses class from key-word arguments
312
426
313
427
Args:
314
- cls: the attr class to be built
428
+ cls: the attr or dataclasses class to be built
315
429
kwargs: a dictionary of keyword arguments
316
430
parsers: a dictionary of parser functions to apply to specific types
317
431
318
432
"""
319
433
return_values : Dict [str , Any ] = {}
320
- for attribute in attr . fields (cls ):
434
+ for attribute in get_fields (cls ): # type: ignore[arg-type]
321
435
return_value : Any
322
436
if attribute .name in kwargs :
323
437
str_value : str = kwargs [attribute .name ]
324
438
set_value : bool = False
325
439
326
440
# Use the converter if provided
327
- if attribute .converter is not None :
328
- return_value = attribute .converter (str_value )
441
+ converter = getattr (attribute , "converter" , None )
442
+ if converter is not None :
443
+ return_value = converter (str_value )
329
444
set_value = True
330
445
331
446
# try getting a known parser
@@ -352,26 +467,26 @@ def attr_from(
352
467
set_value
353
468
), f"Do not know how to convert string to { attribute .type } for value: { str_value } "
354
469
else : # no value, check for a default
355
- assert attribute .default is not None or attribute_is_optional (
470
+ assert attribute .default is not None or _attribute_is_optional (
356
471
attribute
357
472
), f"No value given and no default for attribute `{ attribute .name } `"
358
473
return_value = attribute .default
359
474
# when the default is attr.NOTHING, just use None
360
- if return_value is attr . NOTHING :
475
+ if return_value in MISSING :
361
476
return_value = None
362
477
363
478
return_values [attribute .name ] = return_value
364
479
365
480
return cls (** return_values )
366
481
367
482
368
- def attribute_is_optional (attribute : attr . Attribute ) -> bool :
483
+ def _attribute_is_optional (attribute : FieldType ) -> bool :
369
484
"""Returns True if the attribute is optional, False otherwise"""
370
485
return typing .get_origin (attribute .type ) is Union and isinstance (
371
486
None , typing .get_args (attribute .type )
372
487
)
373
488
374
489
375
- def attribute_has_default (attribute : attr . Attribute ) -> bool :
490
+ def _attribute_has_default (attribute : FieldType ) -> bool :
376
491
"""Returns True if the attribute has a default value, False otherwise"""
377
- return attribute .default != attr . NOTHING or attribute_is_optional (attribute )
492
+ return attribute .default not in _MISSING_OR_NONE or _attribute_is_optional (attribute )
0 commit comments