dycw-utilities 0.146.2__py3-none-any.whl → 0.178.1__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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (89) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +129 -50
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +33 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +131 -93
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/packaging.py +115 -0
  39. utilities/parse.py +2 -2
  40. utilities/pathlib.py +66 -34
  41. utilities/permissions.py +298 -0
  42. utilities/platform.py +5 -4
  43. utilities/polars.py +934 -420
  44. utilities/polars_ols.py +1 -1
  45. utilities/postgres.py +317 -153
  46. utilities/pottery.py +10 -86
  47. utilities/pqdm.py +3 -3
  48. utilities/pwd.py +28 -0
  49. utilities/pydantic.py +4 -51
  50. utilities/pydantic_settings.py +240 -0
  51. utilities/pydantic_settings_sops.py +76 -0
  52. utilities/pyinstrument.py +5 -5
  53. utilities/pytest.py +100 -126
  54. utilities/pytest_plugins/pytest_randomly.py +1 -1
  55. utilities/pytest_plugins/pytest_regressions.py +7 -3
  56. utilities/pytest_regressions.py +27 -8
  57. utilities/random.py +11 -6
  58. utilities/re.py +1 -1
  59. utilities/redis.py +101 -64
  60. utilities/sentinel.py +10 -0
  61. utilities/shelve.py +4 -1
  62. utilities/shutil.py +25 -0
  63. utilities/slack_sdk.py +9 -4
  64. utilities/sqlalchemy.py +422 -352
  65. utilities/sqlalchemy_polars.py +28 -52
  66. utilities/string.py +1 -1
  67. utilities/subprocess.py +1977 -0
  68. utilities/tempfile.py +112 -4
  69. utilities/testbook.py +50 -0
  70. utilities/text.py +174 -42
  71. utilities/throttle.py +158 -0
  72. utilities/timer.py +2 -2
  73. utilities/traceback.py +59 -38
  74. utilities/types.py +68 -22
  75. utilities/typing.py +479 -19
  76. utilities/uuid.py +42 -5
  77. utilities/version.py +27 -26
  78. utilities/whenever.py +663 -178
  79. utilities/zoneinfo.py +80 -22
  80. dycw_utilities-0.146.2.dist-info/METADATA +0 -41
  81. dycw_utilities-0.146.2.dist-info/RECORD +0 -99
  82. dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
  83. dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
  84. utilities/aiolimiter.py +0 -25
  85. utilities/eventkit.py +0 -388
  86. utilities/period.py +0 -237
  87. utilities/python_dotenv.py +0 -101
  88. utilities/streamlit.py +0 -105
  89. utilities/typed_settings.py +0 -144
utilities/logging.py CHANGED
@@ -8,6 +8,7 @@ from logging import (
8
8
  Formatter,
9
9
  Handler,
10
10
  Logger,
11
+ LoggerAdapter,
11
12
  LogRecord,
12
13
  StreamHandler,
13
14
  basicConfig,
@@ -18,9 +19,11 @@ from logging import (
18
19
  from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
19
20
  from pathlib import Path
20
21
  from re import Pattern
22
+ from socket import gethostname
21
23
  from typing import (
22
24
  TYPE_CHECKING,
23
25
  Any,
26
+ Concatenate,
24
27
  Literal,
25
28
  NotRequired,
26
29
  Self,
@@ -30,13 +33,13 @@ from typing import (
30
33
  override,
31
34
  )
32
35
 
33
- from whenever import PlainDateTime, ZonedDateTime
36
+ from whenever import ZonedDateTime
34
37
 
35
38
  from utilities.atomicwrites import move_many
36
39
  from utilities.dataclasses import replace_non_sentinel
37
40
  from utilities.errors import ImpossibleCaseError
38
41
  from utilities.iterables import OneEmptyError, always_iterable, one
39
- from utilities.pathlib import ensure_suffix, get_path
42
+ from utilities.pathlib import ensure_suffix, to_path
40
43
  from utilities.re import (
41
44
  ExtractGroupError,
42
45
  ExtractGroupsError,
@@ -44,31 +47,28 @@ from utilities.re import (
44
47
  extract_groups,
45
48
  )
46
49
  from utilities.sentinel import Sentinel, sentinel
47
- from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
48
50
  from utilities.whenever import (
49
51
  WheneverLogRecord,
50
52
  format_compact,
51
53
  get_now_local,
52
- to_local_plain,
54
+ to_zoned_date_time,
53
55
  )
54
56
 
55
57
  if TYPE_CHECKING:
56
- from collections.abc import Callable, Iterable, Mapping
58
+ from collections.abc import Callable, Iterable, Mapping, MutableMapping
57
59
  from datetime import time
58
60
  from logging import _FilterType
59
61
 
60
62
  from utilities.types import (
61
- LoggerOrName,
63
+ LoggerLike,
62
64
  LogLevel,
63
65
  MaybeCallablePathLike,
64
66
  MaybeIterable,
65
67
  PathLike,
68
+ StrMapping,
66
69
  )
67
70
 
68
71
 
69
- _DEFAULT_FORMAT = (
70
- "{zoned_datetime} | {name}:{funcName}:{lineno} | {levelname:8} | {message}"
71
- )
72
72
  _DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S"
73
73
  _DEFAULT_BACKUP_COUNT: int = 100
74
74
  _DEFAULT_MAX_BYTES: int = 10 * 1024**2
@@ -78,6 +78,35 @@ _DEFAULT_WHEN: _When = "D"
78
78
  ##
79
79
 
80
80
 
81
+ def add_adapter[**P](
82
+ logger: Logger,
83
+ process: Callable[Concatenate[str, P], str],
84
+ /,
85
+ *args: P.args,
86
+ **kwargs: P.kwargs,
87
+ ) -> LoggerAdapter:
88
+ """Add an adapter to a logger."""
89
+
90
+ class CustomAdapter(LoggerAdapter):
91
+ @override
92
+ def process(
93
+ self, msg: str, kwargs: MutableMapping[str, Any]
94
+ ) -> tuple[str, MutableMapping[str, Any]]:
95
+ extra = cast("_ArgsAndKwargs", self.extra)
96
+ new_msg = process(msg, *extra["args"], **extra["kwargs"])
97
+ return new_msg, kwargs
98
+
99
+ return CustomAdapter(logger, extra=_ArgsAndKwargs(args=args, kwargs=kwargs))
100
+
101
+
102
+ class _ArgsAndKwargs(TypedDict):
103
+ args: tuple[Any, ...]
104
+ kwargs: StrMapping
105
+
106
+
107
+ ##
108
+
109
+
81
110
  def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
82
111
  """Add a set of filters to a handler."""
83
112
  for filter_i in filters:
@@ -89,8 +118,10 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
89
118
 
90
119
  def basic_config(
91
120
  *,
92
- obj: LoggerOrName | Handler | None = None,
93
- format_: str = _DEFAULT_FORMAT,
121
+ obj: LoggerLike | Handler | None = None,
122
+ format_: str | None = None,
123
+ prefix: str | None = None,
124
+ hostname: bool = False,
94
125
  datefmt: str = _DEFAULT_DATEFMT,
95
126
  level: LogLevel = "INFO",
96
127
  filters: MaybeIterable[_FilterType] | None = None,
@@ -100,13 +131,19 @@ def basic_config(
100
131
  """Do the basic config."""
101
132
  match obj:
102
133
  case None:
103
- basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
134
+ if format_ is None:
135
+ format_use = get_format_str(prefix=prefix, hostname=hostname)
136
+ else:
137
+ format_use = format_
138
+ basicConfig(format=format_use, datefmt=datefmt, style="{", level=level)
104
139
  case Logger() as logger:
105
140
  logger.setLevel(level)
106
141
  logger.addHandler(handler := StreamHandler())
107
142
  basic_config(
108
143
  obj=handler,
109
144
  format_=format_,
145
+ prefix=prefix,
146
+ hostname=hostname,
110
147
  datefmt=datefmt,
111
148
  level=level,
112
149
  filters=filters,
@@ -115,8 +152,10 @@ def basic_config(
115
152
  )
116
153
  case str() as name:
117
154
  basic_config(
118
- obj=get_logger(logger=name),
155
+ obj=to_logger(name),
119
156
  format_=format_,
157
+ prefix=prefix,
158
+ hostname=hostname,
120
159
  datefmt=datefmt,
121
160
  level=level,
122
161
  filters=filters,
@@ -128,50 +167,32 @@ def basic_config(
128
167
  if filters is not None:
129
168
  add_filters(handler, *always_iterable(filters))
130
169
  formatter = get_formatter(
170
+ prefix=prefix,
131
171
  format_=format_,
172
+ hostname=hostname,
132
173
  datefmt=datefmt,
133
174
  plain=plain,
134
175
  color_field_styles=color_field_styles,
135
176
  )
136
177
  handler.setFormatter(formatter)
137
- case _ as never:
178
+ case never:
138
179
  assert_never(never)
139
180
 
140
181
 
141
182
  ##
142
183
 
143
184
 
144
- def filter_for_key(
145
- key: str, /, *, default: bool = False
146
- ) -> Callable[[LogRecord], bool]:
147
- """Make a filter for a given attribute."""
148
- if (key in _FILTER_FOR_KEY_BLACKLIST) or key.startswith("__"):
149
- raise FilterForKeyError(key=key)
150
-
151
- def filter_(record: LogRecord, /) -> bool:
152
- try:
153
- value = getattr(record, key)
154
- except AttributeError:
155
- return default
156
- return bool(value)
157
-
158
- return filter_
159
-
160
-
161
- # fmt: off
162
- _FILTER_FOR_KEY_BLACKLIST = {
163
- "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName"
164
- }
165
- # fmt: on
166
-
167
-
168
- @dataclass(kw_only=True, slots=True)
169
- class FilterForKeyError(Exception):
170
- key: str
171
-
172
- @override
173
- def __str__(self) -> str:
174
- return f"Invalid key: {self.key!r}"
185
+ def get_format_str(*, prefix: str | None = None, hostname: bool = False) -> str:
186
+ """Generate a format string."""
187
+ parts: list[str] = [
188
+ "{zoned_datetime}",
189
+ f"{gethostname()}:{{process}}" if hostname else "{process}",
190
+ "{name}:{funcName}:{lineno}",
191
+ "{levelname}",
192
+ "{message}",
193
+ ]
194
+ joined = " | ".join(parts)
195
+ return joined if prefix is None else f"{prefix} {joined}"
175
196
 
176
197
 
177
198
  ##
@@ -189,7 +210,9 @@ class _FieldStyleDict(TypedDict):
189
210
 
190
211
  def get_formatter(
191
212
  *,
192
- format_: str = _DEFAULT_FORMAT,
213
+ format_: str | None = None,
214
+ prefix: str | None = None,
215
+ hostname: bool = False,
193
216
  datefmt: str = _DEFAULT_DATEFMT,
194
217
  plain: bool = False,
195
218
  color_field_styles: Mapping[str, _FieldStyleKeys] | None = None,
@@ -197,40 +220,44 @@ def get_formatter(
197
220
  """Get the formatter; colored if available."""
198
221
  setLogRecordFactory(WheneverLogRecord)
199
222
  if plain:
200
- return _get_plain_formatter(format_=format_, datefmt=datefmt)
223
+ return _get_plain_formatter(
224
+ format_=format_, prefix=prefix, hostname=hostname, datefmt=datefmt
225
+ )
201
226
  try:
202
227
  from coloredlogs import DEFAULT_FIELD_STYLES, ColoredFormatter
203
228
  except ModuleNotFoundError: # pragma: no cover
204
- return _get_plain_formatter(format_=format_, datefmt=datefmt)
229
+ return _get_plain_formatter(
230
+ format_=format_, prefix=prefix, hostname=hostname, datefmt=datefmt
231
+ )
232
+ format_use = (
233
+ get_format_str(prefix=prefix, hostname=hostname) if format_ is None else format_
234
+ )
205
235
  default = cast("dict[_FieldStyleKeys, _FieldStyleDict]", DEFAULT_FIELD_STYLES)
206
236
  field_styles = {cast("str", k): v for k, v in default.items()}
207
237
  field_styles["zoned_datetime"] = default["asctime"]
238
+ field_styles["hostname"] = default["hostname"]
239
+ field_styles["process"] = default["hostname"]
240
+ field_styles["lineno"] = default["name"]
241
+ field_styles["funcName"] = default["name"]
208
242
  if color_field_styles is not None:
209
243
  field_styles.update({k: default[v] for k, v in color_field_styles.items()})
210
244
  return ColoredFormatter(
211
- fmt=format_, datefmt=datefmt, style="{", field_styles=field_styles
245
+ fmt=format_use, datefmt=datefmt, style="{", field_styles=field_styles
212
246
  )
213
247
 
214
248
 
215
249
  def _get_plain_formatter(
216
- *, format_: str = _DEFAULT_FORMAT, datefmt: str = _DEFAULT_DATEFMT
250
+ *,
251
+ format_: str | None = None,
252
+ prefix: str | None = None,
253
+ hostname: bool = False,
254
+ datefmt: str = _DEFAULT_DATEFMT,
217
255
  ) -> Formatter:
218
256
  """Get the plain formatter."""
219
- return Formatter(fmt=format_, datefmt=datefmt, style="{")
220
-
221
-
222
- ##
223
-
224
-
225
- def get_logger(*, logger: LoggerOrName | None = None) -> Logger:
226
- """Get a logger."""
227
- match logger:
228
- case Logger():
229
- return logger
230
- case str() | None:
231
- return getLogger(logger)
232
- case _ as never:
233
- assert_never(never)
257
+ format_use = (
258
+ get_format_str(prefix=prefix, hostname=hostname) if format_ is None else format_
259
+ )
260
+ return Formatter(fmt=format_use, datefmt=datefmt, style="{")
234
261
 
235
262
 
236
263
  ##
@@ -259,13 +286,13 @@ class GetLoggingLevelNumberError(Exception):
259
286
 
260
287
  def setup_logging(
261
288
  *,
262
- logger: LoggerOrName | None = None,
263
- format_: str = _DEFAULT_FORMAT,
289
+ logger: LoggerLike | None = None,
290
+ format_: str | None = None,
264
291
  datefmt: str = _DEFAULT_DATEFMT,
265
292
  console_level: LogLevel = "INFO",
266
293
  console_prefix: str = "❯", # noqa: RUF001
267
294
  console_filters: MaybeIterable[_FilterType] | None = None,
268
- files_dir: MaybeCallablePathLike | None = None,
295
+ files_dir: MaybeCallablePathLike = Path.cwd,
269
296
  files_max_bytes: int = _DEFAULT_MAX_BYTES,
270
297
  files_when: _When = _DEFAULT_WHEN,
271
298
  files_interval: int = 1,
@@ -275,20 +302,20 @@ def setup_logging(
275
302
  """Set up logger."""
276
303
  basic_config(
277
304
  obj=logger,
278
- format_=f"{console_prefix} {format_}",
305
+ prefix=console_prefix,
306
+ format_=format_,
279
307
  datefmt=datefmt,
280
308
  level=console_level,
281
309
  filters=console_filters,
282
310
  )
283
- logger_use = get_logger(logger=logger)
311
+ logger_use = to_logger(logger)
284
312
  name = logger_use.name
285
- dir_ = get_path(path=files_dir)
286
313
  levels: list[LogLevel] = ["DEBUG", "INFO", "ERROR"]
287
314
  for level in levels:
288
315
  lower = level.lower()
289
316
  for stem in [lower, f"{name}-{lower}"]:
290
317
  handler = SizeAndTimeRotatingFileHandler(
291
- dir_.joinpath(stem).with_suffix(".txt"),
318
+ to_path(files_dir).joinpath(stem).with_suffix(".txt"),
292
319
  maxBytes=files_max_bytes,
293
320
  when=files_when,
294
321
  interval=files_interval,
@@ -298,6 +325,7 @@ def setup_logging(
298
325
  basic_config(
299
326
  obj=handler,
300
327
  format_=format_,
328
+ hostname=True,
301
329
  datefmt=datefmt,
302
330
  level=level,
303
331
  filters=files_filters,
@@ -359,9 +387,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
359
387
  def emit(self, record: LogRecord) -> None:
360
388
  try:
361
389
  if (self._backup_count is not None) and self._should_rollover(record):
362
- self._do_rollover( # skipif-ci-and-windows
363
- backup_count=self._backup_count
364
- )
390
+ self._do_rollover(backup_count=self._backup_count)
365
391
  FileHandler.emit(self, record)
366
392
  except Exception: # noqa: BLE001 # pragma: no cover
367
393
  self.handleError(record)
@@ -371,23 +397,23 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
371
397
  self.stream.close()
372
398
  self.stream = None
373
399
 
374
- actions = _compute_rollover_actions( # skipif-ci-and-windows
400
+ actions = _compute_rollover_actions(
375
401
  self._directory,
376
402
  self._stem,
377
403
  self._suffix,
378
404
  patterns=self._patterns,
379
405
  backup_count=backup_count,
380
406
  )
381
- actions.do() # skipif-ci-and-windows
407
+ actions.do()
382
408
 
383
409
  if not self.delay: # pragma: no cover
384
410
  self.stream = self._open()
385
- self._time_handler.rolloverAt = ( # skipif-ci-and-windows
386
- self._time_handler.computeRollover(get_now_local().timestamp())
411
+ self._time_handler.rolloverAt = self._time_handler.computeRollover(
412
+ get_now_local().timestamp()
387
413
  )
388
414
 
389
415
  def _should_rollover(self, record: LogRecord, /) -> bool:
390
- if self._max_bytes is not None: # skipif-ci-and-windows
416
+ if self._max_bytes is not None:
391
417
  try:
392
418
  size = self._filename.stat().st_size
393
419
  except FileNotFoundError:
@@ -395,14 +421,14 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
395
421
  else:
396
422
  if size >= self._max_bytes:
397
423
  return True
398
- return bool(self._time_handler.shouldRollover(record)) # skipif-ci-and-windows
424
+ return bool(self._time_handler.shouldRollover(record))
399
425
 
400
426
 
401
427
  def _compute_rollover_patterns(stem: str, suffix: str, /) -> _RolloverPatterns:
402
428
  return _RolloverPatterns(
403
429
  pattern1=re.compile(rf"^{stem}\.(\d+){suffix}$"),
404
- pattern2=re.compile(rf"^{stem}\.(\d+)__([\dT]+?){suffix}$"),
405
- pattern3=re.compile(rf"^{stem}\.(\d+)__([\dT]+?)__([\dT]+?){suffix}$"),
430
+ pattern2=re.compile(rf"^{stem}\.(\d+)__(.+?){suffix}$"),
431
+ pattern3=re.compile(rf"^{stem}\.(\d+)__(.+?)__(.+?){suffix}$"),
406
432
  )
407
433
 
408
434
 
@@ -502,10 +528,8 @@ class _RotatingLogFile:
502
528
  stem=stem,
503
529
  suffix=suffix,
504
530
  index=int(index),
505
- start=PlainDateTime.parse_common_iso(start).assume_tz(
506
- LOCAL_TIME_ZONE_NAME
507
- ),
508
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
531
+ start=to_zoned_date_time(start),
532
+ end=to_zoned_date_time(end),
509
533
  )
510
534
  try:
511
535
  index, end = extract_groups(patterns.pattern2, path.name)
@@ -517,7 +541,7 @@ class _RotatingLogFile:
517
541
  stem=stem,
518
542
  suffix=suffix,
519
543
  index=int(index),
520
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
544
+ end=to_zoned_date_time(end),
521
545
  )
522
546
  try:
523
547
  index = extract_group(patterns.pattern1, path.name)
@@ -538,9 +562,9 @@ class _RotatingLogFile:
538
562
  case int() as index, None, None:
539
563
  tail = str(index)
540
564
  case int() as index, None, ZonedDateTime() as end:
541
- tail = f"{index}__{format_compact(to_local_plain(end))}"
565
+ tail = f"{index}__{format_compact(end, path=True)}"
542
566
  case int() as index, ZonedDateTime() as start, ZonedDateTime() as end:
543
- tail = f"{index}__{format_compact(to_local_plain(start))}__{format_compact(to_local_plain(end))}"
567
+ tail = f"{index}__{format_compact(start, path=True)}__{format_compact(end, path=True)}"
544
568
  case _: # pragma: no cover
545
569
  raise ImpossibleCaseError(
546
570
  case=[f"{self.index=}", f"{self.start=}", f"{self.end=}"]
@@ -578,14 +602,28 @@ class _Rotation:
578
602
  return self.file.replace(index=self.index, start=self.start, end=self.end).path
579
603
 
580
604
 
605
+ ##
606
+
607
+
608
+ def to_logger(logger: LoggerLike | None = None, /) -> Logger:
609
+ """Convert to a logger."""
610
+ match logger:
611
+ case Logger():
612
+ return logger
613
+ case str() | None:
614
+ return getLogger(logger)
615
+ case never:
616
+ assert_never(never)
617
+
618
+
581
619
  __all__ = [
582
- "FilterForKeyError",
583
620
  "GetLoggingLevelNumberError",
584
621
  "SizeAndTimeRotatingFileHandler",
622
+ "add_adapter",
585
623
  "add_filters",
586
624
  "basic_config",
587
- "filter_for_key",
588
- "get_logger",
625
+ "get_format_str",
589
626
  "get_logging_level_number",
590
627
  "setup_logging",
628
+ "to_logger",
591
629
  ]
utilities/math.py CHANGED
@@ -641,7 +641,10 @@ def _is_close(
641
641
  ##
642
642
 
643
643
 
644
- def number_of_decimals(x: float, /, *, max_decimals: int = 20) -> int:
644
+ MAX_DECIMALS = 10
645
+
646
+
647
+ def number_of_decimals(x: float, /, *, max_decimals: int = MAX_DECIMALS) -> int:
645
648
  """Get the number of decimals."""
646
649
  _, frac = divmod(x, 1)
647
650
  results = (
@@ -731,7 +734,7 @@ def round_(
731
734
  return 0
732
735
  case -1:
733
736
  return floor(x)
734
- case _ as never:
737
+ case never:
735
738
  assert_never(never)
736
739
  case "standard-tie-floor":
737
740
  return _round_tie_standard(x, "floor", rel_tol=rel_tol, abs_tol=abs_tol)
@@ -743,7 +746,7 @@ def round_(
743
746
  )
744
747
  case "standard-tie-away-zero":
745
748
  return _round_tie_standard(x, "away-zero", rel_tol=rel_tol, abs_tol=abs_tol)
746
- case _ as never:
749
+ case never:
747
750
  assert_never(never)
748
751
 
749
752
 
@@ -876,7 +879,7 @@ def sign(
876
879
  if is_negative(x, rel_tol=rel_tol, abs_tol=abs_tol):
877
880
  return -1
878
881
  return 0
879
- case _ as never:
882
+ case never:
880
883
  assert_never(never)
881
884
 
882
885
 
@@ -889,6 +892,7 @@ def significant_figures(x: float, /, *, n: int = 2) -> str:
889
892
 
890
893
 
891
894
  __all__ = [
895
+ "MAX_DECIMALS",
892
896
  "MAX_FLOAT32",
893
897
  "MAX_FLOAT64",
894
898
  "MAX_INT8",
@@ -21,7 +21,7 @@ from more_itertools import peekable as _peekable
21
21
  from utilities.functions import get_class_name
22
22
  from utilities.iterables import OneNonUniqueError, one
23
23
  from utilities.reprlib import get_repr
24
- from utilities.sentinel import Sentinel, sentinel
24
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from collections.abc import Iterable, Iterator, Mapping, Sequence
@@ -206,7 +206,7 @@ def bucket_mapping[T, U, UH: Hashable](
206
206
  return {k: frozenset(map(pre, v)) for k, v in mapping.items()}
207
207
  case Callable(), "unique":
208
208
  return _bucket_mapping_unique({k: map(pre, v) for k, v in mapping.items()})
209
- case _ as never:
209
+ case never:
210
210
  assert_never(never)
211
211
 
212
212
 
@@ -290,9 +290,7 @@ class peekable[T](_peekable): # noqa: N801
290
290
  def peek[U](self, *, default: U) -> T | U: ...
291
291
  @override
292
292
  def peek(self, *, default: Any = sentinel) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
293
- if isinstance(default, Sentinel):
294
- return super().peek()
295
- return super().peek(default=default)
293
+ return super().peek() if is_sentinel(default) else super().peek(default=default)
296
294
 
297
295
  def takewhile(self, predicate: Callable[[T], bool], /) -> Iterator[T]:
298
296
  while bool(self) and predicate(self.peek()):
@@ -374,7 +372,7 @@ def _yield_splits2[T](
374
372
  len_tail = max(len_win - head, 0)
375
373
  if len_tail >= 1:
376
374
  yield window, head, len_tail
377
- case _ as never:
375
+ case never:
378
376
  assert_never(never)
379
377
 
380
378
 
utilities/operator.py CHANGED
@@ -6,9 +6,9 @@ from dataclasses import asdict, dataclass
6
6
  from typing import TYPE_CHECKING, Any, cast, override
7
7
 
8
8
  import utilities.math
9
- from utilities.functions import is_dataclass_instance
10
9
  from utilities.iterables import SortIterableError, sort_iterable
11
10
  from utilities.reprlib import get_repr
11
+ from utilities.typing import is_dataclass_instance
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from utilities.types import Dataclass, Number