ddutils 0.2.3__tar.gz → 0.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddutils
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Domain Driven Utils Library
5
5
  Keywords: python,ddutils
6
6
  Author: davyddd
@@ -84,16 +84,23 @@ is_subclass(List[str], dict) # False
84
84
 
85
85
  ### is_complex_sequence
86
86
 
87
- Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`.
87
+ Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`
88
+ and their subclasses (a `str`-based `Enum` is not a complex sequence).
88
89
 
89
90
  ```python
91
+ from enum import Enum
90
92
  from typing import List, Tuple
91
93
  from ddutils.annotation_helpers import is_complex_sequence
92
94
 
93
95
 
96
+ class Kind(str, Enum):
97
+ A = 'a'
98
+
99
+
94
100
  is_complex_sequence(List[int]) # True
95
101
  is_complex_sequence(Tuple[str, ...]) # True
96
102
  is_complex_sequence(str) # False
103
+ is_complex_sequence(Kind) # False
97
104
  is_complex_sequence(dict) # False
98
105
  ```
99
106
 
@@ -330,6 +337,36 @@ delta = timedelta(seconds=5, milliseconds=500)
330
337
  convert_timedelta_to_milliseconds(delta) # 5500
331
338
  ```
332
339
 
340
+ ### convert_to_base_value
341
+
342
+ Casts a value to its base value type (see [get_base_value_type](#get_base_value_type)):
343
+ a `str` subclass becomes a plain `str`, a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime`
344
+ with the same `tzinfo` and `fold`, an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
345
+ Raises `TypeError` for a value without a base type.
346
+
347
+ Normalize a value once, then dispatch on the result with plain `isinstance` checks: subclasses, `Enum` members
348
+ and third-party types such as a `pendulum.DateTime` all land on the stdlib type.
349
+
350
+ ```python
351
+ from enum import Enum
352
+ from uuid import UUID
353
+ from ddutils.convertors import convert_to_base_value
354
+
355
+
356
+ class Kind(str, Enum):
357
+ A = 'a'
358
+
359
+
360
+ class ProfileId(UUID):
361
+ pass
362
+
363
+
364
+ convert_to_base_value(None) # None
365
+ convert_to_base_value(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
366
+ convert_to_base_value(ProfileId('12345678-1234-5678-1234-567812345678')) # UUID('1234...'), exact type UUID
367
+ convert_to_base_value(object()) # TypeError
368
+ ```
369
+
333
370
  ## Datetime Helpers
334
371
 
335
372
  Utilities for working with dates and times.
@@ -491,6 +528,74 @@ AST-based introspection for extracting exceptions from function source code.
491
528
  Parses a function’s source code to find all `raise` statements and identify the exception types they use.
492
529
  Returns an iterator of `ExceptionInfo` objects describing each raised exception.
493
530
 
531
+ ## Value Helpers
532
+
533
+ Runtime counterparts of the annotation helpers: they inspect values, not type hints.
534
+
535
+ ### get_base_value_type
536
+
537
+ Returns the base value type a value should be treated as, looking past subclasses. The candidates, in order,
538
+ are `BASE_VALUE_TYPES`: `NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`,
539
+ `timedelta`, `UUID`, `Decimal` (`bool` is checked before `int`, `datetime` before `date`). An `Enum` member is looked at
540
+ through its `.value`. Raises `TypeError` for any other value, a collection or a mapping included. Useful for APIs that dispatch on the exact type
541
+ (serializers, query builders).
542
+
543
+ ```python
544
+ from datetime import datetime
545
+ from enum import Enum
546
+ from uuid import UUID
547
+ from ddutils.value_helpers import get_base_value_type
548
+
549
+
550
+ class Kind(str, Enum):
551
+ A = 'a'
552
+
553
+
554
+ class ProfileId(UUID):
555
+ pass
556
+
557
+
558
+ get_base_value_type(None) # <class 'NoneType'>
559
+ get_base_value_type(True) # <class 'bool'>
560
+ get_base_value_type(Kind.A) # <class 'str'>
561
+ get_base_value_type(ProfileId('12345678-1234-5678-1234-567812345678')) # <class 'uuid.UUID'>
562
+ get_base_value_type(datetime.now()) # <class 'datetime.datetime'>
563
+ get_base_value_type(object()) # TypeError
564
+ ```
565
+
566
+ ### unwrap_enum
567
+
568
+ Returns the `.value` of an `Enum` member and any other value as is. `str(Kind.A)` of a `class Kind(str, Enum)`
569
+ is `'Kind.A'`, so a member has to be unwrapped before it is formatted or cast.
570
+
571
+ ```python
572
+ from enum import Enum
573
+ from ddutils.value_helpers import unwrap_enum
574
+
575
+
576
+ class Kind(str, Enum):
577
+ A = 'a'
578
+
579
+
580
+ unwrap_enum(Kind.A) # 'a'
581
+ unwrap_enum('a') # 'a'
582
+ ```
583
+
584
+ ### is_complex_sequence_value
585
+
586
+ Checks whether a value is a sequence other than `str`, `bytes` or `bytearray` (subclasses included).
587
+ Sets and mappings are not sequences. The runtime counterpart of [is_complex_sequence](#is_complex_sequence).
588
+
589
+ ```python
590
+ from ddutils.value_helpers import is_complex_sequence_value
591
+
592
+
593
+ is_complex_sequence_value([1, 2]) # True
594
+ is_complex_sequence_value((1, 2)) # True
595
+ is_complex_sequence_value('abc') # False
596
+ is_complex_sequence_value({1, 2}) # False
597
+ ```
598
+
494
599
  ## Sequence Helpers
495
600
 
496
601
  Safe operations on sequence types.
@@ -66,16 +66,23 @@ is_subclass(List[str], dict) # False
66
66
 
67
67
  ### is_complex_sequence
68
68
 
69
- Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`.
69
+ Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`
70
+ and their subclasses (a `str`-based `Enum` is not a complex sequence).
70
71
 
71
72
  ```python
73
+ from enum import Enum
72
74
  from typing import List, Tuple
73
75
  from ddutils.annotation_helpers import is_complex_sequence
74
76
 
75
77
 
78
+ class Kind(str, Enum):
79
+ A = 'a'
80
+
81
+
76
82
  is_complex_sequence(List[int]) # True
77
83
  is_complex_sequence(Tuple[str, ...]) # True
78
84
  is_complex_sequence(str) # False
85
+ is_complex_sequence(Kind) # False
79
86
  is_complex_sequence(dict) # False
80
87
  ```
81
88
 
@@ -312,6 +319,36 @@ delta = timedelta(seconds=5, milliseconds=500)
312
319
  convert_timedelta_to_milliseconds(delta) # 5500
313
320
  ```
314
321
 
322
+ ### convert_to_base_value
323
+
324
+ Casts a value to its base value type (see [get_base_value_type](#get_base_value_type)):
325
+ a `str` subclass becomes a plain `str`, a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime`
326
+ with the same `tzinfo` and `fold`, an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
327
+ Raises `TypeError` for a value without a base type.
328
+
329
+ Normalize a value once, then dispatch on the result with plain `isinstance` checks: subclasses, `Enum` members
330
+ and third-party types such as a `pendulum.DateTime` all land on the stdlib type.
331
+
332
+ ```python
333
+ from enum import Enum
334
+ from uuid import UUID
335
+ from ddutils.convertors import convert_to_base_value
336
+
337
+
338
+ class Kind(str, Enum):
339
+ A = 'a'
340
+
341
+
342
+ class ProfileId(UUID):
343
+ pass
344
+
345
+
346
+ convert_to_base_value(None) # None
347
+ convert_to_base_value(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
348
+ convert_to_base_value(ProfileId('12345678-1234-5678-1234-567812345678')) # UUID('1234...'), exact type UUID
349
+ convert_to_base_value(object()) # TypeError
350
+ ```
351
+
315
352
  ## Datetime Helpers
316
353
 
317
354
  Utilities for working with dates and times.
@@ -473,6 +510,74 @@ AST-based introspection for extracting exceptions from function source code.
473
510
  Parses a function’s source code to find all `raise` statements and identify the exception types they use.
474
511
  Returns an iterator of `ExceptionInfo` objects describing each raised exception.
475
512
 
513
+ ## Value Helpers
514
+
515
+ Runtime counterparts of the annotation helpers: they inspect values, not type hints.
516
+
517
+ ### get_base_value_type
518
+
519
+ Returns the base value type a value should be treated as, looking past subclasses. The candidates, in order,
520
+ are `BASE_VALUE_TYPES`: `NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`,
521
+ `timedelta`, `UUID`, `Decimal` (`bool` is checked before `int`, `datetime` before `date`). An `Enum` member is looked at
522
+ through its `.value`. Raises `TypeError` for any other value, a collection or a mapping included. Useful for APIs that dispatch on the exact type
523
+ (serializers, query builders).
524
+
525
+ ```python
526
+ from datetime import datetime
527
+ from enum import Enum
528
+ from uuid import UUID
529
+ from ddutils.value_helpers import get_base_value_type
530
+
531
+
532
+ class Kind(str, Enum):
533
+ A = 'a'
534
+
535
+
536
+ class ProfileId(UUID):
537
+ pass
538
+
539
+
540
+ get_base_value_type(None) # <class 'NoneType'>
541
+ get_base_value_type(True) # <class 'bool'>
542
+ get_base_value_type(Kind.A) # <class 'str'>
543
+ get_base_value_type(ProfileId('12345678-1234-5678-1234-567812345678')) # <class 'uuid.UUID'>
544
+ get_base_value_type(datetime.now()) # <class 'datetime.datetime'>
545
+ get_base_value_type(object()) # TypeError
546
+ ```
547
+
548
+ ### unwrap_enum
549
+
550
+ Returns the `.value` of an `Enum` member and any other value as is. `str(Kind.A)` of a `class Kind(str, Enum)`
551
+ is `'Kind.A'`, so a member has to be unwrapped before it is formatted or cast.
552
+
553
+ ```python
554
+ from enum import Enum
555
+ from ddutils.value_helpers import unwrap_enum
556
+
557
+
558
+ class Kind(str, Enum):
559
+ A = 'a'
560
+
561
+
562
+ unwrap_enum(Kind.A) # 'a'
563
+ unwrap_enum('a') # 'a'
564
+ ```
565
+
566
+ ### is_complex_sequence_value
567
+
568
+ Checks whether a value is a sequence other than `str`, `bytes` or `bytearray` (subclasses included).
569
+ Sets and mappings are not sequences. The runtime counterpart of [is_complex_sequence](#is_complex_sequence).
570
+
571
+ ```python
572
+ from ddutils.value_helpers import is_complex_sequence_value
573
+
574
+
575
+ is_complex_sequence_value([1, 2]) # True
576
+ is_complex_sequence_value((1, 2)) # True
577
+ is_complex_sequence_value('abc') # False
578
+ is_complex_sequence_value({1, 2}) # False
579
+ ```
580
+
476
581
  ## Sequence Helpers
477
582
 
478
583
  Safe operations on sequence types.
@@ -10,6 +10,7 @@ from . import (
10
10
  safe_decorators,
11
11
  scoped_registry,
12
12
  sequence_helpers,
13
+ value_helpers,
13
14
  )
14
15
 
15
16
  __all__ = (
@@ -24,4 +25,5 @@ __all__ = (
24
25
  'safe_decorators',
25
26
  'scoped_registry',
26
27
  'sequence_helpers',
28
+ 'value_helpers',
27
29
  )
@@ -34,7 +34,7 @@ def is_subclass(annotation: Any, *base_types: Any) -> bool:
34
34
 
35
35
  def is_complex_sequence(annotation: Any) -> bool:
36
36
  annotation_origin = get_annotation_origin(annotation)
37
- return annotation_origin not in NON_COMPLEX_SEQUENCE_TYPES and is_subclass(annotation_origin, Sequence)
37
+ return not is_subclass(annotation_origin, *NON_COMPLEX_SEQUENCE_TYPES) and is_subclass(annotation_origin, Sequence)
38
38
 
39
39
 
40
40
  def get_complex_sequence_element_annotation(annotation: Any) -> Any:
@@ -0,0 +1,86 @@
1
+ from collections.abc import Callable
2
+ from datetime import date, datetime, time, timedelta
3
+ from decimal import Decimal
4
+ from types import NoneType
5
+ from typing import Any, Protocol
6
+ from uuid import UUID
7
+
8
+ from ddutils.value_helpers import BASE_VALUE_TYPES, get_base_value_type, unwrap_enum
9
+
10
+
11
+ class ClassInstanceWithDict(Protocol):
12
+ __dict__: dict
13
+
14
+
15
+ def convert_camel_case_to_snake_case(string: str) -> str:
16
+ return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
17
+
18
+
19
+ def convert_to_repr(obj: ClassInstanceWithDict) -> str:
20
+ class_name = obj.__class__.__name__
21
+ if not hasattr(obj, '__dict__'):
22
+ raise TypeError(f'Instance of {class_name} class does not have a `__dict__` attribute.')
23
+
24
+ attrs = ', '.join(f'{key}={value!r}' for key, value in obj.__dict__.items() if not key.startswith('_'))
25
+ return f'{class_name}({attrs})'
26
+
27
+
28
+ def convert_timedelta_to_milliseconds(delta: timedelta) -> int:
29
+ return int(delta.total_seconds() * 1000)
30
+
31
+
32
+ def _convert_to_datetime(value: datetime) -> datetime:
33
+ return datetime(
34
+ value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond,
35
+ value.tzinfo, fold=value.fold,
36
+ ) # fmt: skip
37
+
38
+
39
+ def _convert_to_date(value: date) -> date:
40
+ return date(value.year, value.month, value.day)
41
+
42
+
43
+ def _convert_to_time(value: time) -> time:
44
+ return time(value.hour, value.minute, value.second, value.microsecond, value.tzinfo, fold=value.fold)
45
+
46
+
47
+ def _convert_to_timedelta(value: timedelta) -> timedelta:
48
+ # not `total_seconds()`: it goes through a float and loses microseconds on large values
49
+ return timedelta(days=value.days, seconds=value.seconds, microseconds=value.microseconds)
50
+
51
+
52
+ def _convert_to_uuid(value: UUID) -> UUID:
53
+ return UUID(int=value.int)
54
+
55
+
56
+ # A cast for every type of `BASE_VALUE_TYPES`: the constructor where it accepts an instance of a subclass,
57
+ # a function building the value from its fields otherwise
58
+ BASE_VALUE_CONVERTORS: dict[type, Callable[[Any], Any]] = {
59
+ NoneType: lambda _: None,
60
+ bool: bool,
61
+ int: int,
62
+ float: float,
63
+ str: str,
64
+ bytes: bytes,
65
+ bytearray: bytearray,
66
+ datetime: _convert_to_datetime,
67
+ date: _convert_to_date,
68
+ time: _convert_to_time,
69
+ timedelta: _convert_to_timedelta,
70
+ UUID: _convert_to_uuid,
71
+ Decimal: Decimal,
72
+ }
73
+
74
+ if set(BASE_VALUE_CONVERTORS) != set(BASE_VALUE_TYPES):
75
+ raise RuntimeError('`BASE_VALUE_CONVERTORS` must define a convertor for exactly the types of `BASE_VALUE_TYPES`')
76
+
77
+
78
+ def convert_to_base_value(value: Any) -> Any:
79
+ """
80
+ Cast a value to its base value type (see `get_base_value_type`): a `str` subclass becomes a plain `str`,
81
+ a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime` with the same `tzinfo` and `fold`,
82
+ an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
83
+ Raises `TypeError` for a value without a base type.
84
+ """
85
+ value = unwrap_enum(value)
86
+ return BASE_VALUE_CONVERTORS[get_base_value_type(value)](value)
@@ -0,0 +1,37 @@
1
+ from collections.abc import Sequence
2
+ from datetime import date, datetime, time, timedelta
3
+ from decimal import Decimal
4
+ from enum import Enum
5
+ from types import NoneType
6
+ from typing import Any
7
+ from uuid import UUID
8
+
9
+ from ddutils.annotation_helpers import NON_COMPLEX_SEQUENCE_TYPES
10
+
11
+ # The order matters, a value is matched against the first type it is an instance of:
12
+ # `bool` is a subclass of `int`, `datetime` is a subclass of `date`
13
+ BASE_VALUE_TYPES = (NoneType, bool, int, float, str, bytes, bytearray, datetime, date, time, timedelta, UUID, Decimal)
14
+
15
+
16
+ def unwrap_enum(value: Any) -> Any:
17
+ """Return the `.value` of an `Enum` member, any other value as is."""
18
+ return value.value if isinstance(value, Enum) else value
19
+
20
+
21
+ def get_base_value_type(value: Any) -> type:
22
+ """
23
+ Return the base value type a value should be treated as, looking past subclasses: one of `BASE_VALUE_TYPES`
24
+ (`NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`, `timedelta`, `UUID`,
25
+ `Decimal`). An `Enum` member is looked at through its `.value`. Raises `TypeError` for any other value.
26
+ """
27
+ value = unwrap_enum(value)
28
+ for base_type in BASE_VALUE_TYPES:
29
+ if isinstance(value, base_type):
30
+ return base_type
31
+
32
+ raise TypeError(f'Value of type `{type(value).__name__}` has no base value type')
33
+
34
+
35
+ def is_complex_sequence_value(value: Any) -> bool:
36
+ """Return whether a value is a sequence other than `str`, `bytes` or `bytearray`; subclasses are handled."""
37
+ return isinstance(value, Sequence) and not isinstance(value, NON_COMPLEX_SEQUENCE_TYPES)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.3"
4
+ version = "0.3.0"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
7
7
  readme = "README.md"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.3"
4
+ version = "0.3.0"
5
5
  authors = [{ name = "davyddd" }]
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
@@ -1,23 +0,0 @@
1
- from datetime import timedelta
2
- from typing import Protocol
3
-
4
-
5
- class ClassInstanceWithDict(Protocol):
6
- __dict__: dict
7
-
8
-
9
- def convert_camel_case_to_snake_case(string: str) -> str:
10
- return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
11
-
12
-
13
- def convert_to_repr(obj: ClassInstanceWithDict) -> str:
14
- class_name = obj.__class__.__name__
15
- if not hasattr(obj, '__dict__'):
16
- raise TypeError(f'Instance of {class_name} class does not have a `__dict__` attribute.')
17
-
18
- attrs = ', '.join(f'{key}={value!r}' for key, value in obj.__dict__.items() if not key.startswith('_'))
19
- return f'{class_name}({attrs})'
20
-
21
-
22
- def convert_timedelta_to_milliseconds(delta: timedelta) -> int:
23
- return int(delta.total_seconds() * 1000)
File without changes
File without changes