dycw-utilities 0.166.30__py3-none-any.whl → 0.185.8__py3-none-any.whl

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 (96) hide show
  1. dycw_utilities-0.185.8.dist-info/METADATA +33 -0
  2. dycw_utilities-0.185.8.dist-info/RECORD +90 -0
  3. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +17 -10
  7. utilities/asyncio.py +50 -72
  8. utilities/atools.py +9 -11
  9. utilities/cachetools.py +16 -11
  10. utilities/click.py +76 -19
  11. utilities/concurrent.py +1 -1
  12. utilities/constants.py +492 -0
  13. utilities/contextlib.py +23 -30
  14. utilities/contextvars.py +1 -23
  15. utilities/core.py +2581 -0
  16. utilities/dataclasses.py +16 -119
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +1 -1
  19. utilities/errors.py +2 -16
  20. utilities/fastapi.py +5 -5
  21. utilities/fpdf2.py +2 -1
  22. utilities/functions.py +34 -265
  23. utilities/http.py +2 -3
  24. utilities/hypothesis.py +84 -29
  25. utilities/importlib.py +17 -1
  26. utilities/iterables.py +39 -575
  27. utilities/jinja2.py +145 -0
  28. utilities/jupyter.py +5 -3
  29. utilities/libcst.py +1 -1
  30. utilities/lightweight_charts.py +4 -6
  31. utilities/logging.py +24 -24
  32. utilities/math.py +1 -36
  33. utilities/more_itertools.py +4 -6
  34. utilities/numpy.py +2 -1
  35. utilities/operator.py +2 -2
  36. utilities/orjson.py +42 -43
  37. utilities/os.py +4 -147
  38. utilities/packaging.py +129 -0
  39. utilities/parse.py +35 -15
  40. utilities/pathlib.py +3 -120
  41. utilities/platform.py +8 -90
  42. utilities/polars.py +38 -32
  43. utilities/postgres.py +37 -33
  44. utilities/pottery.py +20 -18
  45. utilities/pqdm.py +3 -4
  46. utilities/psutil.py +2 -3
  47. utilities/pydantic.py +25 -0
  48. utilities/pydantic_settings.py +87 -16
  49. utilities/pydantic_settings_sops.py +16 -3
  50. utilities/pyinstrument.py +4 -4
  51. utilities/pytest.py +96 -125
  52. utilities/pytest_plugins/pytest_regressions.py +2 -2
  53. utilities/pytest_regressions.py +32 -11
  54. utilities/random.py +2 -8
  55. utilities/redis.py +98 -94
  56. utilities/reprlib.py +11 -118
  57. utilities/shellingham.py +66 -0
  58. utilities/shutil.py +25 -0
  59. utilities/slack_sdk.py +13 -12
  60. utilities/sqlalchemy.py +57 -30
  61. utilities/sqlalchemy_polars.py +16 -25
  62. utilities/subprocess.py +2590 -0
  63. utilities/tabulate.py +32 -0
  64. utilities/testbook.py +8 -8
  65. utilities/text.py +24 -99
  66. utilities/throttle.py +159 -0
  67. utilities/time.py +18 -0
  68. utilities/timer.py +31 -14
  69. utilities/traceback.py +16 -23
  70. utilities/types.py +42 -2
  71. utilities/typing.py +26 -14
  72. utilities/uuid.py +1 -1
  73. utilities/version.py +202 -45
  74. utilities/whenever.py +53 -150
  75. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  76. dycw_utilities-0.166.30.dist-info/RECORD +0 -98
  77. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  78. utilities/aeventkit.py +0 -388
  79. utilities/atomicwrites.py +0 -182
  80. utilities/cryptography.py +0 -41
  81. utilities/getpass.py +0 -8
  82. utilities/git.py +0 -19
  83. utilities/gzip.py +0 -31
  84. utilities/json.py +0 -70
  85. utilities/pickle.py +0 -25
  86. utilities/re.py +0 -156
  87. utilities/sentinel.py +0 -73
  88. utilities/socket.py +0 -8
  89. utilities/string.py +0 -20
  90. utilities/tempfile.py +0 -77
  91. utilities/typed_settings.py +0 -152
  92. utilities/tzdata.py +0 -11
  93. utilities/tzlocal.py +0 -28
  94. utilities/warnings.py +0 -65
  95. utilities/zipfile.py +0 -25
  96. utilities/zoneinfo.py +0 -133
utilities/functions.py CHANGED
@@ -1,43 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable, Iterable, Iterator
4
- from dataclasses import asdict, dataclass
5
- from functools import _lru_cache_wrapper, cached_property, partial, reduce, wraps
3
+ from dataclasses import dataclass
4
+ from functools import wraps
6
5
  from inspect import getattr_static
7
6
  from pathlib import Path
8
- from re import findall
9
- from types import (
10
- BuiltinFunctionType,
11
- FunctionType,
12
- MethodDescriptorType,
13
- MethodType,
14
- MethodWrapperType,
15
- WrapperDescriptorType,
16
- )
17
- from typing import TYPE_CHECKING, Any, Literal, TypeGuard, cast, overload, override
7
+ from typing import TYPE_CHECKING, Any, Literal, assert_never, overload, override
18
8
 
19
9
  from whenever import Date, PlainDateTime, Time, TimeDelta, ZonedDateTime
20
10
 
21
- from utilities.reprlib import get_repr, get_repr_and_class
22
- from utilities.sentinel import Sentinel, is_sentinel, sentinel
23
- from utilities.types import Dataclass, Number, SupportsRichComparison, TypeLike
11
+ from utilities.constants import SECOND
12
+ from utilities.core import get_class_name, repr_, repr_str
24
13
 
25
14
  if TYPE_CHECKING:
26
- from collections.abc import Container
15
+ from collections.abc import Callable, Container, Iterable, Iterator
27
16
 
28
-
29
- def apply_decorators[F1: Callable, F2: Callable](
30
- func: F1, /, *decorators: Callable[[F2], F2]
31
- ) -> F1:
32
- """Apply a set of decorators to a function."""
33
- return reduce(_apply_decorators_one, decorators, func)
34
-
35
-
36
- def _apply_decorators_one[F: Callable](acc: F, el: Callable[[Any], Any], /) -> F:
37
- return el(acc)
38
-
39
-
40
- ##
17
+ from utilities.types import Duration, Number, TypeLike
41
18
 
42
19
 
43
20
  @overload
@@ -274,7 +251,7 @@ class EnsureMemberError(Exception):
274
251
  @override
275
252
  def __str__(self) -> str:
276
253
  return _make_error_msg(
277
- self.obj, f"a member of {get_repr(self.container)}", nullable=self.nullable
254
+ self.obj, f"a member of {repr_(self.container)}", nullable=self.nullable
278
255
  )
279
256
 
280
257
 
@@ -485,207 +462,31 @@ class EnsureZonedDateTimeError(Exception):
485
462
  ##
486
463
 
487
464
 
488
- def first[T](pair: tuple[T, Any], /) -> T:
489
- """Get the first element in a pair."""
490
- return pair[0]
491
-
492
-
493
- ##
494
-
495
-
496
- @overload
497
- def get_class[T](obj: type[T], /) -> type[T]: ...
498
- @overload
499
- def get_class[T](obj: T, /) -> type[T]: ...
500
- def get_class[T](obj: T | type[T], /) -> type[T]:
501
- """Get the class of an object, unless it is already a class."""
502
- return obj if isinstance(obj, type) else type(obj)
503
-
504
-
505
- ##
506
-
507
-
508
- def get_class_name(obj: Any, /, *, qual: bool = False) -> str:
509
- """Get the name of the class of an object, unless it is already a class."""
510
- cls = get_class(obj)
511
- return f"{cls.__module__}.{cls.__qualname__}" if qual else cls.__name__
465
+ def in_milli_seconds(duration: Duration, /) -> float:
466
+ """Convert a duration to milli-seconds."""
467
+ return 1e3 * in_seconds(duration)
512
468
 
513
469
 
514
- ##
515
-
516
-
517
- def get_func_name(obj: Callable[..., Any], /) -> str:
518
- """Get the name of a callable."""
519
- if isinstance(obj, BuiltinFunctionType):
520
- return obj.__name__
521
- if isinstance(obj, FunctionType):
522
- name = obj.__name__
523
- pattern = r"^.+\.([A-Z]\w+\." + name + ")$"
524
- try:
525
- (full_name,) = findall(pattern, obj.__qualname__)
526
- except ValueError:
527
- return name
528
- return full_name
529
- if isinstance(obj, MethodType):
530
- return f"{get_class_name(obj.__self__)}.{obj.__name__}"
531
- if isinstance(
532
- obj,
533
- MethodType | MethodDescriptorType | MethodWrapperType | WrapperDescriptorType,
534
- ):
535
- return obj.__qualname__
536
- if isinstance(obj, _lru_cache_wrapper):
537
- return cast("Any", obj).__name__
538
- if isinstance(obj, partial):
539
- return get_func_name(obj.func)
540
- return get_class_name(obj)
541
-
542
-
543
- ##
470
+ def in_seconds(duration: Duration, /) -> float:
471
+ """Convert a duration to seconds."""
472
+ match duration:
473
+ case int() | float():
474
+ return duration
475
+ case TimeDelta():
476
+ return duration.in_seconds()
477
+ case never:
478
+ assert_never(never)
544
479
 
545
480
 
546
- def get_func_qualname(obj: Callable[..., Any], /) -> str:
547
- """Get the qualified name of a callable."""
548
- if isinstance(
549
- obj, BuiltinFunctionType | FunctionType | MethodType | _lru_cache_wrapper
550
- ):
551
- return f"{obj.__module__}.{obj.__qualname__}"
552
- if isinstance(
553
- obj, MethodDescriptorType | MethodWrapperType | WrapperDescriptorType
554
- ):
555
- return f"{obj.__objclass__.__module__}.{obj.__qualname__}"
556
- if isinstance(obj, partial):
557
- return get_func_qualname(obj.func)
558
- return f"{obj.__module__}.{get_class_name(obj)}"
559
-
560
-
561
- ##
562
-
563
-
564
- def identity[T](obj: T, /) -> T:
565
- """Return the object itself."""
566
- return obj
567
-
568
-
569
- ##
570
-
571
-
572
- def is_none(obj: Any, /) -> TypeGuard[None]:
573
- """Check if an object is `None`."""
574
- return obj is None
575
-
576
-
577
- ##
578
-
579
-
580
- def is_not_none(obj: Any, /) -> bool:
581
- """Check if an object is not `None`."""
582
- return obj is not None
583
-
584
-
585
- ##
586
-
587
-
588
- def map_object[T](
589
- func: Callable[[Any], Any], obj: T, /, *, before: Callable[[Any], Any] | None = None
590
- ) -> T:
591
- """Map a function over an object, across a variety of structures."""
592
- if before is not None:
593
- obj = before(obj)
594
- match obj:
595
- case dict():
596
- return type(obj)({
597
- k: map_object(func, v, before=before) for k, v in obj.items()
598
- })
599
- case frozenset() | list() | set() | tuple():
600
- return type(obj)(map_object(func, i, before=before) for i in obj)
601
- case Dataclass():
602
- return map_object(func, asdict(obj), before=before)
603
- case _:
604
- return func(obj)
605
-
606
-
607
- ##
608
-
609
-
610
- @overload
611
- def min_nullable[T: SupportsRichComparison](
612
- iterable: Iterable[T | None], /, *, default: Sentinel = ...
613
- ) -> T: ...
614
- @overload
615
- def min_nullable[T: SupportsRichComparison, U](
616
- iterable: Iterable[T | None], /, *, default: U = ...
617
- ) -> T | U: ...
618
- def min_nullable[T: SupportsRichComparison, U](
619
- iterable: Iterable[T | None], /, *, default: U | Sentinel = sentinel
620
- ) -> T | U:
621
- """Compute the minimum of a set of values; ignoring nulls."""
622
- values = (i for i in iterable if i is not None)
623
- if is_sentinel(default):
624
- try:
625
- return min(values)
626
- except ValueError:
627
- raise MinNullableError(values=values) from None
628
- return min(values, default=default)
629
-
630
-
631
- @dataclass(kw_only=True, slots=True)
632
- class MinNullableError[T: SupportsRichComparison](Exception):
633
- values: Iterable[T]
634
-
635
- @override
636
- def __str__(self) -> str:
637
- return "Minimum of an all-None iterable is undefined"
638
-
639
-
640
- @overload
641
- def max_nullable[T: SupportsRichComparison](
642
- iterable: Iterable[T | None], /, *, default: Sentinel = ...
643
- ) -> T: ...
644
- @overload
645
- def max_nullable[T: SupportsRichComparison, U](
646
- iterable: Iterable[T | None], /, *, default: U = ...
647
- ) -> T | U: ...
648
- def max_nullable[T: SupportsRichComparison, U](
649
- iterable: Iterable[T | None], /, *, default: U | Sentinel = sentinel
650
- ) -> T | U:
651
- """Compute the maximum of a set of values; ignoring nulls."""
652
- values = (i for i in iterable if i is not None)
653
- if is_sentinel(default):
654
- try:
655
- return max(values)
656
- except ValueError:
657
- raise MaxNullableError(values=values) from None
658
- return max(values, default=default)
659
-
660
-
661
- @dataclass(kw_only=True, slots=True)
662
- class MaxNullableError[TSupportsRichComparison](Exception):
663
- values: Iterable[TSupportsRichComparison]
664
-
665
- @override
666
- def __str__(self) -> str:
667
- return "Maximum of an all-None iterable is undefined"
668
-
669
-
670
- ##
671
-
672
-
673
- def not_func[**P](func: Callable[P, bool], /) -> Callable[P, bool]:
674
- """Lift a boolean-valued function to return its conjugation."""
675
-
676
- @wraps(func)
677
- def wrapped(*args: P.args, **kwargs: P.kwargs) -> bool:
678
- return not func(*args, **kwargs)
679
-
680
- return wrapped
681
-
682
-
683
- ##
684
-
685
-
686
- def second[U](pair: tuple[Any, U], /) -> U:
687
- """Get the second element in a pair."""
688
- return pair[1]
481
+ def in_timedelta(duration: Duration, /) -> TimeDelta:
482
+ """Convert a duration to a timedelta."""
483
+ match duration:
484
+ case int() | float():
485
+ return duration * SECOND
486
+ case TimeDelta():
487
+ return duration
488
+ case never:
489
+ assert_never(never)
689
490
 
690
491
 
691
492
  ##
@@ -693,7 +494,7 @@ def second[U](pair: tuple[Any, U], /) -> U:
693
494
 
694
495
  def skip_if_optimize[**P](func: Callable[P, None], /) -> Callable[P, None]:
695
496
  """Skip a function if we are in the optimized mode."""
696
- if __debug__:
497
+ if __debug__: # pragma: no cover
697
498
  return func
698
499
 
699
500
  @wraps(func)
@@ -726,25 +527,8 @@ def yield_object_attributes(
726
527
  ##
727
528
 
728
529
 
729
- def yield_object_properties(
730
- obj: Any, /, *, skip: Iterable[str] | None = None
731
- ) -> Iterator[tuple[str, Any]]:
732
- """Yield all the object properties."""
733
- yield from yield_object_attributes(obj, skip=skip, static_type=property)
734
-
735
-
736
- def yield_object_cached_properties(
737
- obj: Any, /, *, skip: Iterable[str] | None = None
738
- ) -> Iterator[tuple[str, Any]]:
739
- """Yield all the object cached properties."""
740
- yield from yield_object_attributes(obj, skip=skip, static_type=cached_property)
741
-
742
-
743
- ##
744
-
745
-
746
530
  def _make_error_msg(obj: Any, desc: str, /, *, nullable: bool = False) -> str:
747
- msg = f"{get_repr_and_class(obj)} must be {desc}"
531
+ msg = f"Object {repr_str(obj)} of type {get_class_name(obj)!r} must be {desc}"
748
532
  if nullable:
749
533
  msg += " or None"
750
534
  return msg
@@ -766,9 +550,6 @@ __all__ = [
766
550
  "EnsureTimeDeltaError",
767
551
  "EnsureTimeError",
768
552
  "EnsureZonedDateTimeError",
769
- "MaxNullableError",
770
- "MinNullableError",
771
- "apply_decorators",
772
553
  "ensure_bool",
773
554
  "ensure_bytes",
774
555
  "ensure_class",
@@ -784,21 +565,9 @@ __all__ = [
784
565
  "ensure_time",
785
566
  "ensure_time_delta",
786
567
  "ensure_zoned_date_time",
787
- "first",
788
- "get_class",
789
- "get_class_name",
790
- "get_func_name",
791
- "get_func_qualname",
792
- "identity",
793
- "is_none",
794
- "is_not_none",
795
- "map_object",
796
- "max_nullable",
797
- "min_nullable",
798
- "not_func",
799
- "second",
568
+ "in_milli_seconds",
569
+ "in_seconds",
570
+ "in_timedelta",
800
571
  "skip_if_optimize",
801
572
  "yield_object_attributes",
802
- "yield_object_cached_properties",
803
- "yield_object_properties",
804
573
  ]
utilities/http.py CHANGED
@@ -1,11 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from contextlib import contextmanager
3
4
  from http.client import HTTPSConnection
4
5
  from ipaddress import IPv4Address
5
6
  from typing import TYPE_CHECKING
6
7
 
7
- from utilities.contextlib import enhanced_context_manager
8
-
9
8
  if TYPE_CHECKING:
10
9
  from collections.abc import Iterator
11
10
 
@@ -22,7 +21,7 @@ def get_public_ip(*, timeout: float | None = None) -> IPv4Address:
22
21
  ##
23
22
 
24
23
 
25
- @enhanced_context_manager
24
+ @contextmanager
26
25
  def yield_connection(
27
26
  host: str, /, *, timeout: float | None = None
28
27
  ) -> Iterator[HTTPSConnection]:
utilities/hypothesis.py CHANGED
@@ -51,8 +51,13 @@ from whenever import (
51
51
  ZonedDateTime,
52
52
  )
53
53
 
54
- from utilities.functions import ensure_int, ensure_str, max_nullable, min_nullable
55
- from utilities.math import (
54
+ from utilities.constants import (
55
+ DATE_DELTA_MAX,
56
+ DATE_DELTA_MIN,
57
+ DATE_TIME_DELTA_MAX,
58
+ DATE_TIME_DELTA_MIN,
59
+ DAY,
60
+ IS_LINUX,
56
61
  MAX_FLOAT32,
57
62
  MAX_FLOAT64,
58
63
  MAX_INT8,
@@ -73,37 +78,42 @@ from utilities.math import (
73
78
  MIN_UINT16,
74
79
  MIN_UINT32,
75
80
  MIN_UINT64,
76
- is_zero,
81
+ TEMP_DIR,
82
+ TIME_DELTA_MAX,
83
+ TIME_DELTA_MIN,
84
+ UTC,
85
+ Sentinel,
86
+ sentinel,
87
+ )
88
+ from utilities.core import (
89
+ Permissions,
90
+ TemporaryDirectory,
91
+ get_env,
92
+ get_now,
93
+ is_sentinel,
94
+ max_nullable,
95
+ min_nullable,
96
+ to_zone_info,
97
+ yield_temp_cwd,
77
98
  )
78
- from utilities.os import get_env_var
79
- from utilities.pathlib import module_path, temp_cwd
80
- from utilities.platform import IS_LINUX, IS_WINDOWS
81
- from utilities.sentinel import Sentinel, is_sentinel, sentinel
82
- from utilities.tempfile import TEMP_DIR, TemporaryDirectory
83
- from utilities.version import Version
99
+ from utilities.functions import ensure_int, ensure_str
100
+ from utilities.math import is_zero
101
+ from utilities.pathlib import module_path
102
+ from utilities.version import Version2, Version3
84
103
  from utilities.whenever import (
85
- DATE_DELTA_MAX,
86
- DATE_DELTA_MIN,
87
104
  DATE_DELTA_PARSABLE_MAX,
88
105
  DATE_DELTA_PARSABLE_MIN,
89
- DATE_TIME_DELTA_MAX,
90
- DATE_TIME_DELTA_MIN,
91
106
  DATE_TIME_DELTA_PARSABLE_MAX,
92
107
  DATE_TIME_DELTA_PARSABLE_MIN,
93
108
  DATE_TWO_DIGIT_YEAR_MAX,
94
109
  DATE_TWO_DIGIT_YEAR_MIN,
95
- DAY,
96
- TIME_DELTA_MAX,
97
- TIME_DELTA_MIN,
98
110
  DatePeriod,
99
111
  TimePeriod,
100
112
  ZonedDateTimePeriod,
101
- get_now,
102
113
  to_date_time_delta,
103
114
  to_days,
104
115
  to_nanoseconds,
105
116
  )
106
- from utilities.zoneinfo import UTC, to_zone_info
107
117
 
108
118
  if TYPE_CHECKING:
109
119
  from collections.abc import Collection, Hashable, Iterable, Iterator
@@ -553,7 +563,7 @@ def floats_extra(
553
563
  @composite
554
564
  def git_repos(draw: DrawFn, /) -> Path:
555
565
  path = draw(temp_paths())
556
- with temp_cwd(path):
566
+ with yield_temp_cwd(path):
557
567
  _ = check_call(["git", "init", "-b", "master"])
558
568
  _ = check_call(["git", "config", "user.name", "User"])
559
569
  _ = check_call(["git", "config", "user.email", "a@z.com"])
@@ -864,6 +874,38 @@ def _path_parts(draw: DrawFn, /) -> str:
864
874
  ##
865
875
 
866
876
 
877
+ @composite
878
+ def permissions(
879
+ draw: DrawFn,
880
+ /,
881
+ *,
882
+ user_read: MaybeSearchStrategy[bool | None] = None,
883
+ user_write: MaybeSearchStrategy[bool | None] = None,
884
+ user_execute: MaybeSearchStrategy[bool | None] = None,
885
+ group_read: MaybeSearchStrategy[bool | None] = None,
886
+ group_write: MaybeSearchStrategy[bool | None] = None,
887
+ group_execute: MaybeSearchStrategy[bool | None] = None,
888
+ others_read: MaybeSearchStrategy[bool | None] = None,
889
+ others_write: MaybeSearchStrategy[bool | None] = None,
890
+ others_execute: MaybeSearchStrategy[bool | None] = None,
891
+ ) -> Permissions:
892
+ """Strategy for generating `Permissions`."""
893
+ return Permissions(
894
+ user_read=draw2(draw, user_read, booleans()),
895
+ user_write=draw2(draw, user_write, booleans()),
896
+ user_execute=draw2(draw, user_execute, booleans()),
897
+ group_read=draw2(draw, group_read, booleans()),
898
+ group_write=draw2(draw, group_write, booleans()),
899
+ group_execute=draw2(draw, group_execute, booleans()),
900
+ others_read=draw2(draw, others_read, booleans()),
901
+ others_write=draw2(draw, others_write, booleans()),
902
+ others_execute=draw2(draw, others_execute, booleans()),
903
+ )
904
+
905
+
906
+ ##
907
+
908
+
867
909
  @composite
868
910
  def plain_date_times(
869
911
  draw: DrawFn,
@@ -1007,7 +1049,7 @@ def setup_hypothesis_profiles(
1007
1049
  assert_never(never)
1008
1050
 
1009
1051
  phases = {Phase.explicit, Phase.reuse, Phase.generate, Phase.target}
1010
- if "HYPOTHESIS_NO_SHRINK" not in environ:
1052
+ if "HYPOTHESIS_NO_SHRINK" not in environ: # pragma: no cover
1011
1053
  phases.add(Phase.shrink)
1012
1054
  for profile in Profile:
1013
1055
  try:
@@ -1024,7 +1066,7 @@ def setup_hypothesis_profiles(
1024
1066
  suppress_health_check=suppress_health_check,
1025
1067
  verbosity=profile.verbosity,
1026
1068
  )
1027
- profile = get_env_var("HYPOTHESIS_PROFILE", default=Profile.default.name)
1069
+ profile = get_env("HYPOTHESIS_PROFILE", default=Profile.default.name)
1028
1070
  settings.load_profile(profile)
1029
1071
 
1030
1072
 
@@ -1128,9 +1170,7 @@ def temp_dirs(draw: DrawFn, /) -> TemporaryDirectory:
1128
1170
  """Search strategy for temporary directories."""
1129
1171
  _TEMP_DIR_HYPOTHESIS.mkdir(exist_ok=True)
1130
1172
  uuid = draw(uuids())
1131
- return TemporaryDirectory(
1132
- prefix=f"{uuid}__", dir=_TEMP_DIR_HYPOTHESIS, ignore_cleanup_errors=IS_WINDOWS
1133
- )
1173
+ return TemporaryDirectory(prefix=f"{uuid}__", dir=_TEMP_DIR_HYPOTHESIS)
1134
1174
 
1135
1175
 
1136
1176
  ##
@@ -1453,12 +1493,25 @@ def urls(
1453
1493
 
1454
1494
 
1455
1495
  @composite
1456
- def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> Version:
1457
- """Strategy for generating versions."""
1496
+ def version2s(
1497
+ draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False
1498
+ ) -> Version2:
1499
+ """Strategy for generating Version2 objects."""
1500
+ major, minor = draw(pairs(integers(min_value=0)))
1501
+ _ = assume((major >= 1) or (minor >= 1))
1502
+ suffix_use = draw(text_ascii(min_size=1)) if draw2(draw, suffix) else None
1503
+ return Version2(major=major, minor=minor, suffix=suffix_use)
1504
+
1505
+
1506
+ @composite
1507
+ def version3s(
1508
+ draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False
1509
+ ) -> Version3:
1510
+ """Strategy for generating Version3 objects."""
1458
1511
  major, minor, patch = draw(triples(integers(min_value=0)))
1459
1512
  _ = assume((major >= 1) or (minor >= 1) or (patch >= 1))
1460
1513
  suffix_use = draw(text_ascii(min_size=1)) if draw2(draw, suffix) else None
1461
- return Version(major=major, minor=minor, patch=patch, suffix=suffix_use)
1514
+ return Version3(major=major, minor=minor, patch=patch, suffix=suffix_use)
1462
1515
 
1463
1516
 
1464
1517
  ##
@@ -1570,7 +1623,7 @@ def zoned_date_times(
1570
1623
  with (
1571
1624
  assume_does_not_raise(RepeatedTime),
1572
1625
  assume_does_not_raise(SkippedTime),
1573
- assume_does_not_raise(ValueError, match=r"Resulting datetime is out of range"),
1626
+ assume_does_not_raise(ValueError, match=r"Resulting time is out of range"),
1574
1627
  ):
1575
1628
  zoned = plain.assume_tz(time_zone_.key, disambiguate="raise")
1576
1629
  with assume_does_not_raise(OverflowError, match=r"date value out of range"):
@@ -1613,6 +1666,7 @@ __all__ = [
1613
1666
  "numbers",
1614
1667
  "pairs",
1615
1668
  "paths",
1669
+ "permissions",
1616
1670
  "plain_date_times",
1617
1671
  "py_datetimes",
1618
1672
  "quadruples",
@@ -1639,7 +1693,8 @@ __all__ = [
1639
1693
  "uint32s",
1640
1694
  "uint64s",
1641
1695
  "urls",
1642
- "versions",
1696
+ "version2s",
1697
+ "version3s",
1643
1698
  "year_months",
1644
1699
  "zone_infos",
1645
1700
  "zoned_date_time_periods",
utilities/importlib.py CHANGED
@@ -1,7 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib.resources
3
4
  from importlib import import_module
4
5
  from importlib.util import find_spec
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from utilities.errors import ImpossibleCaseError
10
+
11
+ if TYPE_CHECKING:
12
+ from importlib.resources import Anchor
13
+
14
+
15
+ def files(*, anchor: Anchor | None = None) -> Path:
16
+ """Get the path for an anchor."""
17
+ path = importlib.resources.files(anchor)
18
+ if isinstance(path, Path):
19
+ return path
20
+ raise ImpossibleCaseError(case=[f"{path=}"]) # pragma: no cover
5
21
 
6
22
 
7
23
  def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
@@ -15,4 +31,4 @@ def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
15
31
  return hasattr(mod, name)
16
32
 
17
33
 
18
- __all__ = ["is_valid_import"]
34
+ __all__ = ["files", "is_valid_import"]