dycw-utilities 0.156.0__py3-none-any.whl → 0.157.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.156.0
3
+ Version: 0.157.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
- utilities/__init__.py,sha256=qnrT_UTHUY5w9TUoe2Wq56gzkzQRpi_K6c4hWJV6mLs,60
1
+ utilities/__init__.py,sha256=T1D5frjma6r3zBtDh4rvvHMOorqu5R_6WaB8xxZsqNU,60
2
2
  utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
3
- utilities/asyncio.py,sha256=QXkTtugXkqtYt7Do23zgYErqzdp6jwzPpV_SP9fJ1gI,16780
3
+ utilities/asyncio.py,sha256=PUedzQ5deqlSECQ33sam9cRzI9TnygHz3FdOqWJWPTM,15288
4
4
  utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
5
5
  utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
6
6
  utilities/cachetools.py,sha256=v1-9sXHLdOLiwmkq6NB0OUbxeKBuVVN6wmAWefWoaHI,2744
@@ -13,7 +13,7 @@ utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
13
13
  utilities/dataclasses.py,sha256=MXrvIPSZHlpV4msRdVVDRZZo7MC3gX5C9jDUSoNOdpE,32478
14
14
  utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
15
15
  utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
16
- utilities/eventkit.py,sha256=ddoleSwW9zdc2tjX5Ge0pMKtYwV_JMxhHYOxnWX2AGM,12609
16
+ utilities/eventkit.py,sha256=wjqrSlqHSe7chAcAROXt9h1IVt8grzX_8jp8PGoeRAs,12804
17
17
  utilities/fastapi.py,sha256=3wpd63Tw9paSyy7STpAD7GGe8fLkLaRC6TPCwIGm1BU,1361
18
18
  utilities/fpdf2.py,sha256=HgM8JSvoioDXrjC0UR3HVLjnMnnb_mML7nL2EmkTwGI,1854
19
19
  utilities/functions.py,sha256=RNVAoLeT_sl-gXaBv2VI_U_EB-d-nSVosYR4gTeeojE,28261
@@ -31,7 +31,7 @@ utilities/json.py,sha256=-WcGtSsCr9Y42wHZzAMnfvU6ihAfVftylFfRUORaDFo,2102
31
31
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
32
32
  utilities/libcst.py,sha256=TKgKN4bNmtBNEE-TUfhTyd1BrTncfsl_7tTuhpesGYY,5585
33
33
  utilities/lightweight_charts.py,sha256=YM3ojBvJxuCSUBu_KrhFBmaMCvRPvupKC3qkm-UVZq4,2751
34
- utilities/logging.py,sha256=ihbfQJgjc7t3Pds0oPvF_J1eigiqFKzxNOijzoee8U4,18064
34
+ utilities/logging.py,sha256=NBxAafc9q0KwCqZRHbxI_OEnYADiEGOleLxDsapS7g4,18379
35
35
  utilities/math.py,sha256=7ve4RxX3g-FGGVnWV0K9bBeGnKUEjnTbH13VxdvFtGE,26847
36
36
  utilities/memory_profiler.py,sha256=XzN56jDCa5aqXS_DxEjb_K4L6aIWh_5zyKi6OhcIxw0,853
37
37
  utilities/modules.py,sha256=iuvLluJya-hvl1Q25-Jk3dLgx2Es3ck4SjJiEkAlVTs,3195
@@ -40,7 +40,7 @@ utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
40
40
  utilities/operator.py,sha256=nhxn5q6CFNzUm1wpTwWPCu9JGCqVHSlaJf0o1-efoII,3616
41
41
  utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
42
42
  utilities/orjson.py,sha256=Ll0U172ITMqOJc3kjV90C0eI-EWzSXlMHSdUBaUSe80,41499
43
- utilities/os.py,sha256=mFvjydySvjtSXpk7tLStUJcndauAoujxUUmj_CO7LWY,3778
43
+ utilities/os.py,sha256=Zwznb1Y0cHHIPG7t0UfEWeo0VeDzCnBcwI0mTV-xh2M,3877
44
44
  utilities/parse.py,sha256=JcJn5yXKhIWXBCwgBdPsyu7Hvcuw6kyEdqvaebCaI9k,17951
45
45
  utilities/pathlib.py,sha256=qGuU8XPmdgGpy8tOMUgelfXx3kxI8h9IaV3TI_06QGE,8428
46
46
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
@@ -48,23 +48,23 @@ utilities/platform.py,sha256=pTn7gw6N4T6LdKrf0virwarof_mze9WtoQlrGMzhGVI,2798
48
48
  utilities/polars.py,sha256=JOZjSpj9jitDijX044mKc-N00C5N_On3TJYJKJRhdcE,78494
49
49
  utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
50
50
  utilities/postgres.py,sha256=ynCTTaF-bVEOSW-KEAR-dlLh_hYjeVVjm__-4pEU8Zk,12269
51
- utilities/pottery.py,sha256=HJ96oLRarTP37Vhg0WTyB3yAu2hETeg6HgRmpDIqyUs,6581
51
+ utilities/pottery.py,sha256=ggMN72Y7wx7Js8VN6eyNyodpm8TIYqZHGghkDPXIVWk,3949
52
52
  utilities/pqdm.py,sha256=idv2seRVP2f6NeSfpeEnT5A-tQezaHZKDyeu16g2-0E,3091
53
53
  utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
54
54
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  utilities/pyinstrument.py,sha256=NZCZz2nBo0BLJ9DTf7H_Q_KGxvsf2S2M3h0qYoYh2kw,804
56
- utilities/pytest.py,sha256=2HHfAWkzZeK2OAzL2F49EDKooMkfDoGqg8Ev4cHC_N8,7869
56
+ utilities/pytest.py,sha256=M-Om6b3hpF9W_bEB7UFY2IzBCubSxzVQleGrgRXHtxY,7741
57
57
  utilities/pytest_regressions.py,sha256=8by5DWEL89Y469TI5AzX1pMy3NJWVtjEg2xQdOOdYuM,4169
58
58
  utilities/random.py,sha256=hZlH4gnAtoaofWswuJYjcygejrY8db4CzP-z_adO2Mo,4165
59
59
  utilities/re.py,sha256=S4h-DLL6ScMPqjboZ_uQ1BVTJajrqV06r_81D--_HCE,4573
60
- utilities/redis.py,sha256=2fdveFbqL2pEAeyiVuN_Je8nSM_IZHeahPduMHhFRzY,28381
60
+ utilities/redis.py,sha256=pqzl5A08vaRS4Gfjxob3LWWH9c-vwlsKbvVMTjWMSh8,28364
61
61
  utilities/reprlib.py,sha256=ssYTcBW-TeRh3fhCJv57sopTZHF5FrPyyUg9yp5XBlo,3953
62
62
  utilities/scipy.py,sha256=wZJM7fEgBAkLSYYvSmsg5ac-QuwAI0BGqHVetw1_Hb0,947
63
63
  utilities/sentinel.py,sha256=A_p5jX2K0Yc5XBfoYHyBLqHsEWzE1ByOdDuzzA2pZnE,1434
64
64
  utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
65
65
  utilities/slack_sdk.py,sha256=ppFBvKgfg5IRWiIoKPtpTyzBtBF4XmwEvU3I5wLJikM,2140
66
66
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
67
- utilities/sqlalchemy.py,sha256=IJKzrKUd_eBOkyK6CucDlxtHwo2vYH3t-rV2_5rAxq8,40554
67
+ utilities/sqlalchemy.py,sha256=wtbIp6XDjKwfrvl-wfoY4FQXo_a9vSoHq5K_dYeBBeY,40541
68
68
  utilities/sqlalchemy_polars.py,sha256=5Q9HReETYg0qB6E6WQhFh4QAZlKE-IWlogj2BVif_-w,14246
69
69
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
70
70
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
@@ -87,8 +87,8 @@ utilities/zoneinfo.py,sha256=FBMcUQ4662Aq8SsuCL1OAhDQiyANmVjtb-C30DRrWoE,1966
87
87
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
88
88
  utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
89
89
  utilities/pytest_plugins/pytest_regressions.py,sha256=9v8kAXDM2ycIXJBimoiF4EgrwbUvxTycFWJiGR_GHhM,1466
90
- dycw_utilities-0.156.0.dist-info/METADATA,sha256=J71C9d7mzvc5tHFVLEmcOD0NLrPbS5D8zkpnRwq5AEg,1643
91
- dycw_utilities-0.156.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.156.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
93
- dycw_utilities-0.156.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
- dycw_utilities-0.156.0.dist-info/RECORD,,
90
+ dycw_utilities-0.157.0.dist-info/METADATA,sha256=I1kXqyTgNiYkQ5eip4vqyC8d-JA3uZCxNWB7Dxpn5JM,1643
91
+ dycw_utilities-0.157.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.157.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
93
+ dycw_utilities-0.157.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
+ dycw_utilities-0.157.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.156.0"
3
+ __version__ = "0.157.0"
utilities/asyncio.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import sys
5
4
  from asyncio import (
6
5
  Lock,
7
6
  Queue,
@@ -36,9 +35,8 @@ from typing import (
36
35
  override,
37
36
  )
38
37
 
39
- from utilities.errors import ImpossibleCaseError, is_instance_error
40
38
  from utilities.functions import ensure_int, ensure_not_none
41
- from utilities.logging import to_logger
39
+ from utilities.os import is_pytest
42
40
  from utilities.random import SYSTEM_RANDOM
43
41
  from utilities.sentinel import Sentinel, sentinel
44
42
  from utilities.shelve import yield_shelf
@@ -70,8 +68,6 @@ if TYPE_CHECKING:
70
68
  from utilities.types import (
71
69
  Coro,
72
70
  Delta,
73
- ExceptionTypeLike,
74
- LoggerLike,
75
71
  MaybeCallableBoolLike,
76
72
  MaybeType,
77
73
  PathLike,
@@ -369,8 +365,6 @@ async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> li
369
365
  try:
370
366
  items = [await queue.get()]
371
367
  except RuntimeError as error: # pragma: no cover
372
- from utilities.pytest import is_pytest
373
-
374
368
  if (not is_pytest()) or (error.args[0] != "Event loop is closed"):
375
369
  raise
376
370
  return []
@@ -400,43 +394,6 @@ def get_items_nowait[T](queue: Queue[T], /, *, max_size: int | None = None) -> l
400
394
  ##
401
395
 
402
396
 
403
- async def loop_until_succeed(
404
- func: Callable[[], Coro[None]],
405
- /,
406
- *,
407
- logger: LoggerLike | None = None,
408
- errors: ExceptionTypeLike[Exception] | None = None,
409
- sleep: Delta | None = None,
410
- ) -> bool:
411
- """Repeatedly call a coroutine until it succeeds."""
412
- name = get_coroutine_name(func)
413
- while True:
414
- try:
415
- await func()
416
- except Exception as error: # noqa: BLE001
417
- if logger is not None:
418
- to_logger(logger).error("Error running %r", name, exc_info=True)
419
- exc_type, exc_value, traceback = sys.exc_info()
420
- if (exc_type is None) or (exc_value is None): # pragma: no cover
421
- raise ImpossibleCaseError(
422
- case=[f"{exc_type=}", f"{exc_value=}"]
423
- ) from None
424
- sys.excepthook(exc_type, exc_value, traceback)
425
- if (errors is not None) and is_instance_error(error, errors):
426
- return False
427
- if sleep is not None:
428
- if logger is not None:
429
- to_logger(logger).info("Sleeping for %s...", sleep)
430
- await sleep_td(sleep)
431
- if logger is not None:
432
- to_logger(logger).info("Retrying %r...", name)
433
- else:
434
- return True
435
-
436
-
437
- ##
438
-
439
-
440
397
  async def put_items[T](items: Iterable[T], queue: Queue[T], /) -> None:
441
398
  """Put items into a queue; if full then wait."""
442
399
  for item in items:
@@ -589,7 +546,6 @@ __all__ = [
589
546
  "get_coroutine_name",
590
547
  "get_items",
591
548
  "get_items_nowait",
592
- "loop_until_succeed",
593
549
  "put_items",
594
550
  "put_items_nowait",
595
551
  "sleep_max",
utilities/eventkit.py CHANGED
@@ -48,6 +48,7 @@ def add_listener[E: Event, F: Callable](
48
48
  error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
49
49
  ignore: TypeLike[BaseException] | None = None,
50
50
  logger: LoggerLike | None = None,
51
+ logger_allow_pytest: bool = False,
51
52
  decorators: MaybeIterable[Callable[[F], F]] | None = None,
52
53
  done: Callable[..., MaybeCoro[None]] | None = None,
53
54
  keep_ref: bool = False,
@@ -59,6 +60,7 @@ def add_listener[E: Event, F: Callable](
59
60
  error=error,
60
61
  ignore=ignore,
61
62
  logger=logger,
63
+ logger_allow_pytest=logger_allow_pytest,
62
64
  decorators=decorators,
63
65
  )
64
66
  return cast("E", event.connect(lifted, done=done, keep_ref=keep_ref))
@@ -284,6 +286,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
284
286
  error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
285
287
  ignore: TypeLike[BaseException] | None = None,
286
288
  logger: LoggerLike | None = None,
289
+ logger_allow_pytest: bool = False,
287
290
  decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
288
291
  ) -> F1:
289
292
  match error, bool(iscoroutinefunction(listener)):
@@ -297,7 +300,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
297
300
  except Exception as exc: # noqa: BLE001
298
301
  if (ignore is not None) and isinstance(exc, ignore):
299
302
  return
300
- to_logger(logger).exception("")
303
+ to_logger(logger, allow_pytest=logger_allow_pytest).exception("")
301
304
 
302
305
  lifted = listener_no_error_sync
303
306
 
@@ -311,7 +314,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
311
314
  except Exception as exc: # noqa: BLE001
312
315
  if (ignore is not None) and isinstance(exc, ignore):
313
316
  return
314
- to_logger(logger).exception("")
317
+ to_logger(logger, allow_pytest=logger_allow_pytest).exception("")
315
318
 
316
319
  lifted = listener_no_error_async
317
320
  case _, _:
utilities/logging.py CHANGED
@@ -37,6 +37,7 @@ from utilities.atomicwrites import move_many
37
37
  from utilities.dataclasses import replace_non_sentinel
38
38
  from utilities.errors import ImpossibleCaseError
39
39
  from utilities.iterables import OneEmptyError, always_iterable, one
40
+ from utilities.os import is_pytest
40
41
  from utilities.pathlib import ensure_suffix, to_path
41
42
  from utilities.re import (
42
43
  ExtractGroupError,
@@ -95,6 +96,7 @@ def basic_config(
95
96
  filters: MaybeIterable[_FilterType] | None = None,
96
97
  plain: bool = False,
97
98
  color_field_styles: Mapping[str, _FieldStyleKeys] | None = None,
99
+ allow_pytest: bool = False,
98
100
  ) -> None:
99
101
  """Do the basic config."""
100
102
  match obj:
@@ -120,7 +122,7 @@ def basic_config(
120
122
  )
121
123
  case str() as name:
122
124
  basic_config(
123
- obj=to_logger(name),
125
+ obj=to_logger(name, allow_pytest=allow_pytest),
124
126
  format_=format_,
125
127
  prefix=prefix,
126
128
  hostname=hostname,
@@ -260,6 +262,7 @@ def setup_logging(
260
262
  console_level: LogLevel = "INFO",
261
263
  console_prefix: str = "❯", # noqa: RUF001
262
264
  console_filters: MaybeIterable[_FilterType] | None = None,
265
+ allow_pytest: bool = False,
263
266
  files_dir: MaybeCallablePathLike = Path.cwd,
264
267
  files_max_bytes: int = _DEFAULT_MAX_BYTES,
265
268
  files_when: _When = _DEFAULT_WHEN,
@@ -276,7 +279,7 @@ def setup_logging(
276
279
  level=console_level,
277
280
  filters=console_filters,
278
281
  )
279
- logger_use = to_logger(logger)
282
+ logger_use = to_logger(logger, allow_pytest=allow_pytest)
280
283
  name = logger_use.name
281
284
  levels: list[LogLevel] = ["DEBUG", "INFO", "ERROR"]
282
285
  for level in levels:
@@ -575,13 +578,18 @@ class _Rotation:
575
578
  ##
576
579
 
577
580
 
578
- def to_logger(logger: LoggerLike | None = None, /) -> Logger:
581
+ def to_logger(
582
+ logger: LoggerLike | None = None, /, *, allow_pytest: bool = False
583
+ ) -> Logger:
579
584
  """Convert to a logger."""
580
585
  match logger:
581
586
  case Logger():
582
587
  return logger
583
588
  case str() | None:
584
- return getLogger(logger)
589
+ logger = getLogger(logger)
590
+ if not allow_pytest:
591
+ _ = logger.addFilter(lambda _: not is_pytest())
592
+ return logger
585
593
  case never:
586
594
  assert_never(never)
587
595
 
utilities/os.py CHANGED
@@ -126,7 +126,15 @@ class GetEnvVarError(Exception):
126
126
 
127
127
  def is_debug() -> bool:
128
128
  """Check if we are in `DEBUG` mode."""
129
- return get_env_var("DEBUG", nullable=True) is not None
129
+ return "DEBUG" in environ
130
+
131
+
132
+ ##
133
+
134
+
135
+ def is_pytest() -> bool:
136
+ """Check if `pytest` is running."""
137
+ return "PYTEST_VERSION" in environ
130
138
 
131
139
 
132
140
  ##
@@ -164,5 +172,6 @@ __all__ = [
164
172
  "get_cpu_use",
165
173
  "get_env_var",
166
174
  "is_debug",
175
+ "is_pytest",
167
176
  "temp_environ",
168
177
  ]
utilities/pottery.py CHANGED
@@ -1,32 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import nullcontext, suppress
3
+ from contextlib import suppress
4
4
  from dataclasses import dataclass
5
- from functools import wraps
6
5
  from sys import maxsize
7
6
  from typing import TYPE_CHECKING, override
8
7
 
9
- from pottery import AIORedlock, ExtendUnlockedLock
8
+ from pottery import AIORedlock
10
9
  from pottery.exceptions import ReleaseUnlockedLock
11
10
  from redis.asyncio import Redis
12
11
 
13
- from utilities.asyncio import loop_until_succeed, sleep_td, timeout_td
12
+ from utilities.asyncio import sleep_td, timeout_td
14
13
  from utilities.contextlib import enhanced_async_context_manager
15
- from utilities.contextvars import yield_set_context
16
14
  from utilities.iterables import always_iterable
17
- from utilities.logging import to_logger
18
15
  from utilities.whenever import MILLISECOND, SECOND, to_seconds
19
16
 
20
17
  if TYPE_CHECKING:
21
- from collections.abc import AsyncIterator, Callable, Iterable
22
- from contextvars import ContextVar
18
+ from collections.abc import AsyncIterator, Iterable
23
19
 
24
20
  from whenever import Delta
25
21
 
26
- from utilities.types import Coro, LoggerLike, MaybeIterable
22
+ from utilities.types import MaybeIterable
27
23
 
28
24
  _NUM: int = 1
29
- _TIMEOUT_TRY_ACQUIRE: Delta = SECOND
30
25
  _TIMEOUT_RELEASE: Delta = 10 * SECOND
31
26
  _SLEEP: Delta = MILLISECOND
32
27
 
@@ -45,79 +40,6 @@ async def extend_lock(
45
40
  ##
46
41
 
47
42
 
48
- @enhanced_async_context_manager
49
- async def try_yield_coroutine_looper(
50
- redis: MaybeIterable[Redis],
51
- key: str,
52
- /,
53
- *,
54
- num: int = _NUM,
55
- timeout_release: Delta = _TIMEOUT_RELEASE,
56
- num_extensions: int | None = None,
57
- timeout_acquire: Delta = _TIMEOUT_TRY_ACQUIRE,
58
- sleep_acquire: Delta = _SLEEP,
59
- throttle: Delta | None = None,
60
- logger: LoggerLike | None = None,
61
- sleep_error: Delta | None = None,
62
- context: ContextVar[bool] | None = None,
63
- ) -> AsyncIterator[CoroutineLooper | None]:
64
- """Try acquire access to a coroutine looper."""
65
- try: # skipif-ci-and-not-linux
66
- async with yield_access(
67
- redis,
68
- key,
69
- num=num,
70
- timeout_release=timeout_release,
71
- num_extensions=num_extensions,
72
- timeout_acquire=timeout_acquire,
73
- sleep=sleep_acquire,
74
- throttle=throttle,
75
- ) as lock:
76
- looper = CoroutineLooper(lock=lock, logger=logger, sleep=sleep_error)
77
- if context is None:
78
- yield looper
79
- else:
80
- with yield_set_context(context):
81
- yield looper
82
- except _YieldAccessUnableToAcquireLockError as error: # skipif-ci-and-not-linux
83
- if logger is not None:
84
- to_logger(logger).info("%s", error)
85
- async with nullcontext():
86
- yield
87
-
88
-
89
- @dataclass(order=True, unsafe_hash=True, kw_only=True)
90
- class CoroutineLooper:
91
- """Looper, guarded by a lock, to repeatedly call a coroutine until it succeeds."""
92
-
93
- lock: AIORedlock
94
- logger: LoggerLike | None = None
95
- sleep: Delta | None = None
96
-
97
- async def __call__[**P](
98
- self, func: Callable[P, Coro[None]], *args: P.args, **kwargs: P.kwargs
99
- ) -> bool:
100
- return await loop_until_succeed(
101
- lambda: self._make_coro(func, *args, **kwargs),
102
- logger=self.logger,
103
- errors=ExtendUnlockedLock,
104
- sleep=self.sleep,
105
- )
106
-
107
- def _make_coro[**P](
108
- self, func: Callable[P, Coro[None]], /, *args: P.args, **kwargs: P.kwargs
109
- ) -> Coro[None]:
110
- @wraps(func)
111
- async def wrapped() -> None:
112
- await extend_lock(lock=self.lock)
113
- return await func(*args, **kwargs)
114
-
115
- return wrapped()
116
-
117
-
118
- ##
119
-
120
-
121
43
  @enhanced_async_context_manager
122
44
  async def yield_access(
123
45
  redis: MaybeIterable[Redis],
@@ -212,10 +134,4 @@ class _YieldAccessUnableToAcquireLockError(YieldAccessError):
212
134
  return f"Unable to acquire any 1 of {self.num} locks for {self.key!r} after {self.timeout}" # skipif-ci-and-not-linux
213
135
 
214
136
 
215
- __all__ = [
216
- "CoroutineLooper",
217
- "YieldAccessError",
218
- "extend_lock",
219
- "try_yield_coroutine_looper",
220
- "yield_access",
221
- ]
137
+ __all__ = ["YieldAccessError", "extend_lock", "yield_access"]
utilities/pytest.py CHANGED
@@ -111,14 +111,6 @@ def add_pytest_configure(config: Config, options: Iterable[tuple[str, str]], /)
111
111
  ##
112
112
 
113
113
 
114
- def is_pytest() -> bool:
115
- """Check if `pytest` is running."""
116
- return "PYTEST_VERSION" in environ
117
-
118
-
119
- ##
120
-
121
-
122
114
  def node_id_path(
123
115
  node_id: str, /, *, root: PathLike | None = None, suffix: str | None = None
124
116
  ) -> Path:
@@ -257,7 +249,6 @@ __all__ = [
257
249
  "add_pytest_addoption",
258
250
  "add_pytest_collection_modifyitems",
259
251
  "add_pytest_configure",
260
- "is_pytest",
261
252
  "node_id_path",
262
253
  "skipif_linux",
263
254
  "skipif_mac",
utilities/redis.py CHANGED
@@ -25,6 +25,7 @@ from utilities.contextlib import enhanced_async_context_manager
25
25
  from utilities.errors import ImpossibleCaseError
26
26
  from utilities.functions import ensure_int, identity
27
27
  from utilities.iterables import always_iterable, one
28
+ from utilities.os import is_pytest
28
29
  from utilities.whenever import MILLISECOND, SECOND, to_milliseconds, to_seconds
29
30
 
30
31
  if TYPE_CHECKING:
@@ -730,8 +731,6 @@ async def subscribe[T](
730
731
  try:
731
732
  _ = task.cancel()
732
733
  except RuntimeError as error: # pragma: no cover
733
- from utilities.pytest import is_pytest
734
-
735
734
  if (not is_pytest()) or (error.args[0] != "Event loop is closed"):
736
735
  raise
737
736
  with suppress(CancelledError):
utilities/sqlalchemy.py CHANGED
@@ -95,6 +95,7 @@ from utilities.iterables import (
95
95
  merge_str_mappings,
96
96
  one,
97
97
  )
98
+ from utilities.os import is_pytest
98
99
  from utilities.reprlib import get_repr
99
100
  from utilities.text import secret_str, snake_case
100
101
  from utilities.types import (
@@ -976,8 +977,6 @@ async def yield_connection(
976
977
  async with timeout_td(timeout, error=error), engine.begin() as conn:
977
978
  yield conn
978
979
  except GeneratorExit: # pragma: no cover
979
- from utilities.pytest import is_pytest
980
-
981
980
  if not is_pytest():
982
981
  raise
983
982
  return