corio 2.2.1__tar.gz → 2.2.2__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.
Files changed (160) hide show
  1. {corio-2.2.1 → corio-2.2.2}/PKG-INFO +7 -1
  2. {corio-2.2.1 → corio-2.2.2}/corio/entrypoint.py +2 -2
  3. {corio-2.2.1 → corio-2.2.2}/corio/infra/incrementor_pyproject.py +112 -35
  4. {corio-2.2.1 → corio-2.2.2}/corio/infra/releaser.py +175 -1
  5. {corio-2.2.1 → corio-2.2.2}/corio/infra/stack.py +4 -4
  6. {corio-2.2.1 → corio-2.2.2}/corio/logs.py +10 -0
  7. {corio-2.2.1 → corio-2.2.2}/corio/path/path.py +10 -0
  8. {corio-2.2.1 → corio-2.2.2}/corio/pyproject.package.toml +10 -9
  9. corio-2.2.2/corio/tests/test_caching.py +39 -0
  10. corio-2.2.2/corio/tests/test_datatype.py +24 -0
  11. corio-2.2.2/corio/tests/test_encrypt.py +43 -0
  12. corio-2.2.2/corio/tests/test_infra.py +163 -0
  13. {corio-2.2.1 → corio-2.2.2}/corio/tests/test_path.py +13 -8
  14. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/PKG-INFO +7 -1
  15. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/SOURCES.txt +6 -3
  16. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/requires.txt +6 -0
  17. {corio-2.2.1 → corio-2.2.2}/pyproject.toml +10 -9
  18. corio-2.2.1/corio/tests/test_datatype.py +0 -33
  19. {corio-2.2.1 → corio-2.2.2}/LICENSE +0 -0
  20. {corio-2.2.1 → corio-2.2.2}/README.md +0 -0
  21. {corio-2.2.1 → corio-2.2.2}/corio/__init__.py +0 -0
  22. {corio-2.2.1 → corio-2.2.2}/corio/ai/__init__.py +0 -0
  23. {corio-2.2.1 → corio-2.2.2}/corio/ai/agentic.py +0 -0
  24. {corio-2.2.1 → corio-2.2.2}/corio/ai/infer.py +0 -0
  25. {corio-2.2.1 → corio-2.2.2}/corio/aio.py +0 -0
  26. {corio-2.2.1 → corio-2.2.2}/corio/api.py +0 -0
  27. {corio-2.2.1 → corio-2.2.2}/corio/augmentation.py +0 -0
  28. {corio-2.2.1 → corio-2.2.2}/corio/av.py +0 -0
  29. {corio-2.2.1 → corio-2.2.2}/corio/caching.py +0 -0
  30. {corio-2.2.1 → corio-2.2.2}/corio/constants.py +0 -0
  31. {corio-2.2.1 → corio-2.2.2}/corio/context.py +0 -0
  32. {corio-2.2.1 → corio-2.2.2}/corio/dataclass.py +0 -0
  33. {corio-2.2.1 → corio-2.2.2}/corio/datatype.py +0 -0
  34. {corio-2.2.1 → corio-2.2.2}/corio/db/__init__.py +0 -0
  35. {corio-2.2.1 → corio-2.2.2}/corio/db/document.py +0 -0
  36. {corio-2.2.1 → corio-2.2.2}/corio/debug.py +0 -0
  37. {corio-2.2.1 → corio-2.2.2}/corio/dm.py +0 -0
  38. {corio-2.2.1 → corio-2.2.2}/corio/dns/__init__.py +0 -0
  39. {corio-2.2.1 → corio-2.2.2}/corio/dns/client.py +0 -0
  40. {corio-2.2.1 → corio-2.2.2}/corio/dns/dm.py +0 -0
  41. {corio-2.2.1 → corio-2.2.2}/corio/dns/proxy.py +0 -0
  42. {corio-2.2.1 → corio-2.2.2}/corio/dns/server.py +0 -0
  43. {corio-2.2.1 → corio-2.2.2}/corio/docker/__init__.py +0 -0
  44. {corio-2.2.1 → corio-2.2.2}/corio/dt.py +0 -0
  45. {corio-2.2.1 → corio-2.2.2}/corio/encrypt.py +0 -0
  46. {corio-2.2.1 → corio-2.2.2}/corio/env.py +0 -0
  47. {corio-2.2.1 → corio-2.2.2}/corio/function.py +0 -0
  48. {corio-2.2.1 → corio-2.2.2}/corio/google_api.py +0 -0
  49. {corio-2.2.1 → corio-2.2.2}/corio/ha/__init__.py +0 -0
  50. {corio-2.2.1 → corio-2.2.2}/corio/ha/constants.py +0 -0
  51. {corio-2.2.1 → corio-2.2.2}/corio/ha/core.py +0 -0
  52. {corio-2.2.1 → corio-2.2.2}/corio/ha/supervisor.py +0 -0
  53. {corio-2.2.1 → corio-2.2.2}/corio/ha/utils.py +0 -0
  54. {corio-2.2.1 → corio-2.2.2}/corio/hash.py +0 -0
  55. {corio-2.2.1 → corio-2.2.2}/corio/hfh.py +0 -0
  56. {corio-2.2.1 → corio-2.2.2}/corio/hook.py +0 -0
  57. {corio-2.2.1 → corio-2.2.2}/corio/https.py +0 -0
  58. {corio-2.2.1 → corio-2.2.2}/corio/infra/__init__.py +0 -0
  59. {corio-2.2.1 → corio-2.2.2}/corio/infra/api.py +0 -0
  60. {corio-2.2.1 → corio-2.2.2}/corio/infra/project.py +0 -0
  61. {corio-2.2.1 → corio-2.2.2}/corio/infra/repository.py +0 -0
  62. {corio-2.2.1 → corio-2.2.2}/corio/inherit.py +0 -0
  63. {corio-2.2.1 → corio-2.2.2}/corio/inspection.py +0 -0
  64. {corio-2.2.1 → corio-2.2.2}/corio/interface/__init__.py +0 -0
  65. {corio-2.2.1 → corio-2.2.2}/corio/interface/context.py +0 -0
  66. {corio-2.2.1 → corio-2.2.2}/corio/interface/controls.py +0 -0
  67. {corio-2.2.1 → corio-2.2.2}/corio/interface/interface.py +0 -0
  68. {corio-2.2.1 → corio-2.2.2}/corio/iterator.py +0 -0
  69. {corio-2.2.1 → corio-2.2.2}/corio/jsn.py +0 -0
  70. {corio-2.2.1 → corio-2.2.2}/corio/json_fix.py +0 -0
  71. {corio-2.2.1 → corio-2.2.2}/corio/markup.py +0 -0
  72. {corio-2.2.1 → corio-2.2.2}/corio/merging.py +0 -0
  73. {corio-2.2.1 → corio-2.2.2}/corio/metric.py +0 -0
  74. {corio-2.2.1 → corio-2.2.2}/corio/mqtt.py +0 -0
  75. {corio-2.2.1 → corio-2.2.2}/corio/name.py +0 -0
  76. {corio-2.2.1 → corio-2.2.2}/corio/net.py +0 -0
  77. {corio-2.2.1 → corio-2.2.2}/corio/netrc.py +0 -0
  78. {corio-2.2.1 → corio-2.2.2}/corio/openai.py +0 -0
  79. {corio-2.2.1 → corio-2.2.2}/corio/parallel.py +0 -0
  80. {corio-2.2.1 → corio-2.2.2}/corio/path/__init__.py +0 -0
  81. {corio-2.2.1 → corio-2.2.2}/corio/path/app.py +0 -0
  82. {corio-2.2.1 → corio-2.2.2}/corio/path/type.py +0 -0
  83. {corio-2.2.1 → corio-2.2.2}/corio/paths.py +0 -0
  84. {corio-2.2.1 → corio-2.2.2}/corio/patterns.py +0 -0
  85. {corio-2.2.1 → corio-2.2.2}/corio/pdf.py +0 -0
  86. {corio-2.2.1 → corio-2.2.2}/corio/plat.py +0 -0
  87. {corio-2.2.1 → corio-2.2.2}/corio/process.py +0 -0
  88. {corio-2.2.1 → corio-2.2.2}/corio/profiling.py +0 -0
  89. {corio-2.2.1 → corio-2.2.2}/corio/rand.py +0 -0
  90. /corio-2.2.1/corio/secrets.py → /corio-2.2.2/corio/sec.py +0 -0
  91. {corio-2.2.1 → corio-2.2.2}/corio/semantic.py +0 -0
  92. {corio-2.2.1 → corio-2.2.2}/corio/sets.py +0 -0
  93. {corio-2.2.1 → corio-2.2.2}/corio/setup/__init__.py +0 -0
  94. {corio-2.2.1 → corio-2.2.2}/corio/spaces.py +0 -0
  95. {corio-2.2.1 → corio-2.2.2}/corio/strings.py +0 -0
  96. {corio-2.2.1 → corio-2.2.2}/corio/tabular.py +0 -0
  97. {corio-2.2.1 → corio-2.2.2}/corio/tests/__init__.py +0 -0
  98. {corio-2.2.1 → corio-2.2.2}/corio/tests/conftest.py +0 -0
  99. {corio-2.2.1 → corio-2.2.2}/corio/tests/helpers.py +0 -0
  100. /corio-2.2.1/corio/tests/test_environment.py → /corio-2.2.2/corio/tests/test_env.py +0 -0
  101. /corio-2.2.1/corio/tests/test_json.py → /corio-2.2.2/corio/tests/test_jsn.py +0 -0
  102. {corio-2.2.1 → corio-2.2.2}/corio/tests/test_yaml.py +0 -0
  103. {corio-2.2.1 → corio-2.2.2}/corio/tokenization.py +0 -0
  104. {corio-2.2.1 → corio-2.2.2}/corio/toml.py +0 -0
  105. {corio-2.2.1 → corio-2.2.2}/corio/tools.py +0 -0
  106. {corio-2.2.1 → corio-2.2.2}/corio/unicode.py +0 -0
  107. {corio-2.2.1 → corio-2.2.2}/corio/version/__init__.py +0 -0
  108. {corio-2.2.1 → corio-2.2.2}/corio/version/version.py +0 -0
  109. {corio-2.2.1 → corio-2.2.2}/corio/webhook.py +0 -0
  110. {corio-2.2.1 → corio-2.2.2}/corio/yml.py +0 -0
  111. {corio-2.2.1 → corio-2.2.2}/corio/youtube.py +0 -0
  112. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/dependency_links.txt +0 -0
  113. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/entry_points.txt +0 -0
  114. {corio-2.2.1 → corio-2.2.2}/corio.egg-info/top_level.txt +0 -0
  115. {corio-2.2.1 → corio-2.2.2}/scripts/add-service +0 -0
  116. {corio-2.2.1 → corio-2.2.2}/scripts/add-user-path +0 -0
  117. {corio-2.2.1 → corio-2.2.2}/scripts/add-ve-shell +0 -0
  118. {corio-2.2.1 → corio-2.2.2}/scripts/apt-essentials +0 -0
  119. {corio-2.2.1 → corio-2.2.2}/scripts/apt-headless +0 -0
  120. {corio-2.2.1 → corio-2.2.2}/scripts/auth-token +0 -0
  121. {corio-2.2.1 → corio-2.2.2}/scripts/compose-update +0 -0
  122. {corio-2.2.1 → corio-2.2.2}/scripts/compress-dir +0 -0
  123. {corio-2.2.1 → corio-2.2.2}/scripts/cru +0 -0
  124. {corio-2.2.1 → corio-2.2.2}/scripts/docker-build-bases +0 -0
  125. {corio-2.2.1 → corio-2.2.2}/scripts/docker-build-bases-dev +0 -0
  126. {corio-2.2.1 → corio-2.2.2}/scripts/docker-install-deps +0 -0
  127. {corio-2.2.1 → corio-2.2.2}/scripts/docker-install-prod +0 -0
  128. {corio-2.2.1 → corio-2.2.2}/scripts/docker-prune +0 -0
  129. {corio-2.2.1 → corio-2.2.2}/scripts/docker-sandbox +0 -0
  130. {corio-2.2.1 → corio-2.2.2}/scripts/docker-sandbox-init +0 -0
  131. {corio-2.2.1 → corio-2.2.2}/scripts/docs-deploy +0 -0
  132. {corio-2.2.1 → corio-2.2.2}/scripts/download +0 -0
  133. {corio-2.2.1 → corio-2.2.2}/scripts/encrypt-secrets +0 -0
  134. {corio-2.2.1 → corio-2.2.2}/scripts/fmtr-test-script +0 -0
  135. {corio-2.2.1 → corio-2.2.2}/scripts/git-clone +0 -0
  136. {corio-2.2.1 → corio-2.2.2}/scripts/ha-addon-launch +0 -0
  137. {corio-2.2.1 → corio-2.2.2}/scripts/infra +0 -0
  138. {corio-2.2.1 → corio-2.2.2}/scripts/infra-sync +0 -0
  139. {corio-2.2.1 → corio-2.2.2}/scripts/install-browser +0 -0
  140. {corio-2.2.1 → corio-2.2.2}/scripts/install-docker +0 -0
  141. {corio-2.2.1 → corio-2.2.2}/scripts/install-ts +0 -0
  142. {corio-2.2.1 → corio-2.2.2}/scripts/install-ys +0 -0
  143. {corio-2.2.1 → corio-2.2.2}/scripts/mirror-dir +0 -0
  144. {corio-2.2.1 → corio-2.2.2}/scripts/opt-dev-init +0 -0
  145. {corio-2.2.1 → corio-2.2.2}/scripts/parse-args +0 -0
  146. {corio-2.2.1 → corio-2.2.2}/scripts/pypi-check +0 -0
  147. {corio-2.2.1 → corio-2.2.2}/scripts/pypi-reserve +0 -0
  148. {corio-2.2.1 → corio-2.2.2}/scripts/run-script +0 -0
  149. {corio-2.2.1 → corio-2.2.2}/scripts/set-password +0 -0
  150. {corio-2.2.1 → corio-2.2.2}/scripts/set-secure-path +0 -0
  151. {corio-2.2.1 → corio-2.2.2}/scripts/set-user-sudo +0 -0
  152. {corio-2.2.1 → corio-2.2.2}/scripts/snips-install +0 -0
  153. {corio-2.2.1 → corio-2.2.2}/scripts/ssh-auth +0 -0
  154. {corio-2.2.1 → corio-2.2.2}/scripts/ssh-serve +0 -0
  155. {corio-2.2.1 → corio-2.2.2}/scripts/tasmota-config +0 -0
  156. {corio-2.2.1 → corio-2.2.2}/scripts/tasmota-flash +0 -0
  157. {corio-2.2.1 → corio-2.2.2}/scripts/tasmota-terminal +0 -0
  158. {corio-2.2.1 → corio-2.2.2}/scripts/vlc-tn +0 -0
  159. {corio-2.2.1 → corio-2.2.2}/scripts/vm-launch +0 -0
  160. {corio-2.2.1 → corio-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corio
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
5
  Author-email: Frontmatter AI <innovative.fowler@mask.pro.fmtr.dev>
6
6
  License-Expression: Apache-2.0
@@ -39,7 +39,10 @@ Requires-Dist: logfire[fastapi]; extra == "dev"
39
39
  Requires-Dist: tomlkit; extra == "dev"
40
40
  Requires-Dist: pyrage; extra == "dev"
41
41
  Requires-Dist: dotenv; extra == "dev"
42
+ Requires-Dist: pytest; extra == "dev"
43
+ Requires-Dist: pytest-cov; extra == "dev"
42
44
  Provides-Extra: test
45
+ Requires-Dist: pytest; extra == "test"
43
46
  Requires-Dist: pytest-cov; extra == "test"
44
47
  Provides-Extra: yaml
45
48
  Requires-Dist: yamlscript; extra == "yaml"
@@ -255,6 +258,8 @@ Requires-Dist: logfire[fastapi]; extra == "infra"
255
258
  Requires-Dist: tomlkit; extra == "infra"
256
259
  Requires-Dist: pyrage; extra == "infra"
257
260
  Requires-Dist: dotenv; extra == "infra"
261
+ Requires-Dist: pytest; extra == "infra"
262
+ Requires-Dist: pytest-cov; extra == "infra"
258
263
  Provides-Extra: vcs
259
264
  Requires-Dist: pygit2; extra == "vcs"
260
265
  Provides-Extra: tasmota
@@ -313,6 +318,7 @@ Requires-Dist: logfire[fastapi]; extra == "all"
313
318
  Requires-Dist: tomlkit; extra == "all"
314
319
  Requires-Dist: pyrage; extra == "all"
315
320
  Requires-Dist: dotenv; extra == "all"
321
+ Requires-Dist: pytest; extra == "all"
316
322
  Requires-Dist: pytest-cov; extra == "all"
317
323
  Requires-Dist: dask[bag]; extra == "all"
318
324
  Requires-Dist: distributed; extra == "all"
@@ -1,7 +1,7 @@
1
1
  from pydantic_settings import CliSubCommand
2
2
 
3
3
  from corio import dm
4
- from corio import secrets
4
+ from corio import sec
5
5
  from corio import sets
6
6
 
7
7
 
@@ -78,7 +78,7 @@ class RemoteDebugTest(dm.Base):
78
78
 
79
79
 
80
80
  class Cli(sets.Base, cli_parse_args=True):
81
- secrets: CliSubCommand[secrets.Cli]
81
+ secrets: CliSubCommand[sec.Cli]
82
82
  docs: CliSubCommand[Docs]
83
83
  pyproject: CliSubCommand[Pyproject]
84
84
  ep_test: CliSubCommand[EpTest]
@@ -1,12 +1,15 @@
1
1
  from copy import deepcopy
2
2
  from functools import cached_property
3
3
  from itertools import chain
4
+ from packaging.requirements import Requirement, InvalidRequirement
5
+ from packaging.utils import canonicalize_name
4
6
 
5
7
  from corio.constants import Constants
6
8
  from corio.infra.releaser import Incrementor
7
9
  from corio.iterator import dedupe
8
10
  from corio.logs import logger
9
- from corio.path import Path
11
+ from corio.path import Path, PackagePaths
12
+ from corio.path.path import Metadata
10
13
  from corio.toml import ensure_table
11
14
 
12
15
 
@@ -25,6 +28,86 @@ class IncrementorPyproject(Incrementor):
25
28
  def name_command(self) -> str:
26
29
  return self.paths.name_ns.replace(".", self.ENTRYPOINT_COMMAND_SEP)
27
30
 
31
+ @cached_property
32
+ def editables(self) -> dict[str, Metadata]:
33
+ data = self.path.read_toml()
34
+ sources = data.get("tool", {}).get("uv", {}).get("sources", {})
35
+
36
+ editables = {}
37
+ for key, source in sources.items():
38
+ if not isinstance(source, dict):
39
+ continue
40
+ if not source.get("editable"):
41
+ continue
42
+
43
+ source_path = source.get("path")
44
+ if not source_path:
45
+ logger.warning(f'Editable source "{key}" is missing "path". Skipping.')
46
+ continue
47
+ path_repo = (self.paths.repo / str(source_path)).resolve()
48
+ if not path_repo.exists():
49
+ continue
50
+
51
+ try:
52
+ paths = PackagePaths(path_repo)
53
+ except Exception as exception:
54
+ logger.warning(f'Failed to resolve editable source at "{path_repo}". Skipping. {exception!r}')
55
+ continue
56
+
57
+ metadata = paths.metadata
58
+ if metadata.version_obj.prerelease and not self.versions.is_pre:
59
+ raise ValueError(
60
+ f'Editable dependency "{paths.name_ns}" is pre-release '
61
+ f'({metadata.version_obj.prerelease}) while "{self.paths.name_ns}" is release. Refusing to pin.'
62
+ )
63
+
64
+ editables[paths.name_ns] = metadata
65
+ editables[canonicalize_name(paths.name_ns)] = metadata
66
+
67
+ return editables
68
+
69
+ def _pin_editable(self, dep: str) -> str:
70
+ try:
71
+ requirement = Requirement(dep)
72
+ except InvalidRequirement:
73
+ return dep
74
+
75
+ if requirement.specifier:
76
+ return dep
77
+ if requirement.url:
78
+ return dep
79
+
80
+ metadata = self.editables.get(requirement.name)
81
+ if metadata is None:
82
+ metadata = self.editables.get(canonicalize_name(requirement.name))
83
+ if metadata is None:
84
+ return dep
85
+
86
+ extras = ""
87
+ if requirement.extras:
88
+ extras = f"[{','.join(sorted(requirement.extras))}]"
89
+
90
+ marker = ""
91
+ if requirement.marker:
92
+ marker = f"; {requirement.marker}"
93
+
94
+ pinned = f"{requirement.name}{extras}=={metadata.version}{marker}"
95
+ logger.info(f'Pinning editable dependency "{dep}" -> "{pinned}".')
96
+ return pinned
97
+
98
+ def _process_deps(self, deps: str|list[str]) -> str|list[str]:
99
+
100
+ if isinstance(deps, list):
101
+ deps=[self._process_deps(dep) for dep in deps]
102
+ deps=dedupe(deps)
103
+ return deps
104
+
105
+ dep=deps
106
+ dep=self._pin_editable(dep)
107
+
108
+ return dep
109
+
110
+
28
111
  def apply(self) -> Path | list[Path] | None:
29
112
  if not self.path.exists():
30
113
  logger.info(f'pyproject.toml not found: "{self.path}". Skipping.')
@@ -43,6 +126,7 @@ class IncrementorPyproject(Incrementor):
43
126
  self.path.write_toml(data)
44
127
  return self.path
45
128
 
129
+
46
130
  @property
47
131
  def _author(self) -> str:
48
132
  if self.paths.metadata.is_client:
@@ -106,36 +190,23 @@ class IncrementorPyproject(Incrementor):
106
190
  values_resolved += resolve_values(value)
107
191
  return values_resolved
108
192
 
109
- install = dedupe(resolve_values("install")) if "install" in dependencies else []
193
+
110
194
  extras = {key: dedupe(resolve_values(key)) for key in dependencies}
111
- extras.pop("install", None)
195
+ install = extras.pop("install", [])
112
196
  extras["all"] = dedupe(list(chain.from_iterable(extras.values())))
113
197
  return install, extras
114
198
 
115
- def _get_dependencies(self, data) -> dict[str, list[str]]:
116
- table = data
117
- for key in self.DEPENDENCIES_SECTION_PATH:
118
- if key not in table:
119
- return {}
120
- table = table[key]
121
-
122
- if table is None:
123
- return {}
124
- return {str(key): [str(value) for value in values] for key, values in table.items()}
125
199
 
126
200
  def _enrich_toml(self, data):
127
- old = self.versions.old
128
- new = self._bump(old)
129
- if old != new:
130
- logger.info(f'Incrementing version "{self.path}" {old} {Constants.ARROW_RIGHT} {new}...')
131
- self.paths.metadata.version = str(new)
201
+ version = str(self.version)
202
+ logger.info(f'Applying release version "{version}" to "{self.path}"...')
132
203
 
133
204
  metadata = ensure_table(data, ("tool", "corio", "metadata"))
134
- metadata["version"] = self.paths.metadata.version
205
+ metadata["version"] = version
135
206
 
136
207
  project = ensure_table(data, ("project",))
137
208
  project["name"] = self.paths.name_ns
138
- project["version"] = self.paths.metadata.version
209
+ project["version"] = version
139
210
  project["description"] = self.paths.metadata.description
140
211
  project["keywords"] = self.paths.metadata.keywords
141
212
  project["readme"] = self.paths.readme.name
@@ -146,18 +217,32 @@ class IncrementorPyproject(Incrementor):
146
217
  elif "license-files" in project:
147
218
  del project["license-files"]
148
219
 
149
- dependencies = self._get_dependencies(data)
150
- if dependencies:
151
- install, extras = self._flatten_dependencies(dependencies)
220
+
221
+ deps_corio = data.get("tool", {}).get("corio", {}).get("dependencies", None)
222
+ if deps_corio is not None:
223
+ install, extras = self._flatten_dependencies(deps_corio)
152
224
  project["dependencies"] = install
153
225
  project["optional-dependencies"] = extras
154
-
155
- if "dev" in extras:
156
- dependency_groups = ensure_table(data, ("dependency-groups",))
157
- dependency_groups["dev"] = list(extras["dev"])
158
226
  else:
159
227
  logger.info(f'No dependencies section found in "{self.path}". Skipping dependency enrichment.')
160
228
 
229
+
230
+ deps = project.get("dependencies")
231
+ if deps is not None:
232
+ project["dependencies"] = self._process_deps(deps)
233
+
234
+ optionals = project.get("optional-dependencies")
235
+ if optionals is not None:
236
+ optionals = {
237
+ key: self._process_deps(values)
238
+ for key, values in optionals.items()
239
+ }
240
+ project["optional-dependencies"] = optionals
241
+
242
+ if "dev" in optionals:
243
+ dependency_groups = ensure_table(data, ("dependency-groups",))
244
+ dependency_groups["dev"] = list(optionals["dev"])
245
+
161
246
  urls = ensure_table(project, ("urls",))
162
247
  urls["Homepage"] = self.repo_url
163
248
 
@@ -187,11 +272,3 @@ class IncrementorPyproject(Incrementor):
187
272
  del setuptools["script-files"]
188
273
 
189
274
  return data
190
-
191
- def _bump(self, version):
192
- if self.versions.pinned:
193
- return self.versions.pinned
194
-
195
- if version.prerelease:
196
- return version.bump_prerelease()
197
- return version.bump_patch()
@@ -1,4 +1,5 @@
1
1
  import shutil
2
+ import subprocess
2
3
  from functools import cached_property
3
4
 
4
5
  import build
@@ -17,7 +18,7 @@ from corio import https as https
17
18
  from corio.constants import Constants
18
19
  from corio.infra.project import Project
19
20
  from corio.inherit import Inherit
20
- from corio.logs import logger
21
+ from corio.logs import logger, sanitize
21
22
  from corio.path import Path
22
23
 
23
24
 
@@ -47,6 +48,15 @@ class Releaser(Inherit[Project]):
47
48
  if increment:
48
49
  self.repo.fetch()
49
50
  self.increment()
51
+
52
+ is_passed = self.tester.run()
53
+ if not is_passed:
54
+ if self.versions.is_pre:
55
+ logger.warning(f"Tests failed, but release is pre-release ({self.version.prerelease}). Continuing.")
56
+ else:
57
+ raise RuntimeError("Tests failed. Aborting release.")
58
+
59
+ if increment:
50
60
  self.repo.push()
51
61
  self.repo.fetch()
52
62
 
@@ -149,6 +159,7 @@ class Releaser(Inherit[Project]):
149
159
 
150
160
  return IndexList[Incrementor](
151
161
  [
162
+ IncrementorVersion(self),
152
163
  IncrementorPyproject(self),
153
164
  IncrementorHomeAssistantAddon(self),
154
165
  IncrementorChangelog(self),
@@ -174,6 +185,10 @@ class Releaser(Inherit[Project]):
174
185
 
175
186
  return releases
176
187
 
188
+ @cached_property
189
+ def tester(self):
190
+ return Tester(self)
191
+
177
192
  def release(self):
178
193
  for release in self.releases:
179
194
  release.release()
@@ -200,6 +215,27 @@ class Incrementor(Inherit[Releaser]):
200
215
  raise NotImplementedError
201
216
 
202
217
 
218
+ class IncrementorVersion(Incrementor):
219
+ @logger.instrument('Incrementing release version in-memory for "{self.paths.name_ns}"...')
220
+ def apply(self) -> Path | list[Path] | None:
221
+ old = self.versions.old
222
+ if self.versions.pinned:
223
+ new = self.versions.pinned
224
+ elif old.prerelease:
225
+ new = old.bump_prerelease()
226
+ else:
227
+ new = old.bump_patch()
228
+
229
+ if old != new:
230
+ logger.info(f'Incrementing runtime version {old} {Constants.ARROW_RIGHT} {new}...')
231
+
232
+ self.paths.metadata.version = str(new)
233
+ return None
234
+
235
+
236
+
237
+
238
+
203
239
  class IncrementorHomeAssistantAddon(Incrementor):
204
240
  DESC = 'Home Assistant Add-On config file'
205
241
 
@@ -529,3 +565,141 @@ class ReleaseDocumentation(Release):
529
565
 
530
566
  with self.paths.repo.chdir:
531
567
  self.deploy()
568
+
569
+
570
+ class Tester(Inherit[Releaser]):
571
+ TEST_FILENAME_PREFIX = "test_"
572
+ TEST_FILENAME_SUFFIX = ".py"
573
+ TOX_REQUIRES = ["tox>=4.22", "tox-uv>=1"]
574
+
575
+ @cached_property
576
+ def path_config_dir(self) -> Path:
577
+ return Path.temp() / f"{self.name}-tox"
578
+
579
+ @cached_property
580
+ def path_config(self) -> Path:
581
+ return self.path_config_dir / "tox.toml"
582
+
583
+ @cached_property
584
+ def dependencies(self) -> dict[str, list[str]]:
585
+ data = self.paths.pyproject_repo.read_toml()
586
+ table = data.get("tool", {}).get("corio", {}).get("dependencies", {})
587
+ dependencies = {
588
+ str(key): [str(value) for value in values]
589
+ for key, values in table.items()
590
+ if isinstance(values, list)
591
+ }
592
+ return dependencies
593
+
594
+ @cached_property
595
+ def modules(self) -> list[str]:
596
+ if not self.paths.tests.exists():
597
+ return []
598
+
599
+ modules = []
600
+ for path in sorted(self.paths.tests.glob(f"{self.TEST_FILENAME_PREFIX}*{self.TEST_FILENAME_SUFFIX}")):
601
+ module = path.stem.removeprefix(self.TEST_FILENAME_PREFIX)
602
+ if module:
603
+ modules.append(module)
604
+ return modules
605
+
606
+
607
+ def get_env(self, name: str, path_tests: Path, extras: list[str]) -> dict:
608
+ if path_tests.is_relative_to(self.paths.repo):
609
+ path_tests = path_tests.relative_to(self.paths.repo)
610
+ env = {
611
+ "description": f"Run {name} tests.",
612
+ "extras": extras,
613
+ "commands": [["python", "-m", "pytest", "-q", str(path_tests)]],
614
+ }
615
+ return env
616
+
617
+ @cached_property
618
+ def env(self) -> dict:
619
+ return self.get_env(name=self.paths.name_ns, path_tests=self.paths.tests, extras=["test"])
620
+
621
+ @cached_property
622
+ def envs(self) -> dict[str, dict]:
623
+ if not self.paths.metadata.test_envs:
624
+ if not self.modules:
625
+ return {}
626
+ return {self.paths.name_ns: self.env}
627
+
628
+ envs = {}
629
+ extras_available = set(self.dependencies.keys())
630
+
631
+ for module in self.modules:
632
+ extras = ["test"]
633
+ if module in extras_available:
634
+ extras.insert(0, module)
635
+
636
+ path_test = self.paths.tests / f"{self.TEST_FILENAME_PREFIX}{module}{self.TEST_FILENAME_SUFFIX}"
637
+ name = f"{self.paths.name_ns}.{module}"
638
+ envs[name] = self.get_env(name=name, path_tests=path_test, extras=extras)
639
+
640
+ return envs
641
+
642
+ @cached_property
643
+ def data(self) -> dict:
644
+ data = {
645
+ "requires": self.TOX_REQUIRES,
646
+ "env_list": list(self.envs.keys()),
647
+ "env": self.envs,
648
+ }
649
+ return data
650
+
651
+ def write_config(self):
652
+ self.path_config_dir.mkdir(parents=True, exist_ok=True)
653
+ self.path_config.write_toml(self.data)
654
+
655
+ def run_subprocess(self) -> int:
656
+ command = [
657
+ "uvx",
658
+ "--with",
659
+ "tox-uv",
660
+ "tox",
661
+ "-c",
662
+ str(self.path_config),
663
+ "--root",
664
+ str(self.paths.repo),
665
+ "--workdir",
666
+ str(self.paths.repo / ".tox"),
667
+ "run",
668
+ ]
669
+
670
+ process = subprocess.Popen(
671
+ command,
672
+ cwd=self.paths.repo,
673
+ stdout=subprocess.PIPE,
674
+ stderr=subprocess.STDOUT,
675
+ text=True,
676
+ bufsize=1,
677
+ )
678
+
679
+ assert process.stdout is not None
680
+ for line in process.stdout:
681
+ logger.info(sanitize(line))
682
+
683
+ code = process.wait()
684
+ return code
685
+
686
+ @logger.instrument('Running test suite for "{self.paths.name_ns}"...')
687
+ def run(self) -> bool:
688
+ if not self.envs:
689
+ logger.warning(f'No tests found under "{self.paths.tests}". Skipping.')
690
+ return True
691
+
692
+ logger.info(f'Generating temporary tox config: "{self.path_config}"')
693
+ self.write_config()
694
+ try:
695
+ code = self.run_subprocess()
696
+ finally:
697
+ self.path_config.unlink(missing_ok=True)
698
+ shutil.rmtree(self.path_config_dir, ignore_errors=True)
699
+
700
+ if code == 0:
701
+ logger.info("All test environments passed.")
702
+ return True
703
+
704
+ logger.error(f"Test suite failed with exit code {code}.")
705
+ return False
@@ -6,7 +6,7 @@ from corio.docker import DockerClient
6
6
  from corio.infra.project import Project
7
7
  from corio.inherit import Inherit
8
8
  from corio.iterator import IndexList
9
- from corio.logs import logger
9
+ from corio.logs import logger, sanitize
10
10
  from corio.merging import merge
11
11
  from corio.path import Path, PackagePaths
12
12
 
@@ -114,7 +114,7 @@ class Stack(Inherit[Project]):
114
114
  progress="plain",
115
115
  stream_logs=True,
116
116
  ):
117
- logger.info(line.rstrip())
117
+ logger.info(sanitize(line))
118
118
 
119
119
 
120
120
  class Development(Stack):
@@ -177,8 +177,8 @@ class ProductionPublic(ProductionPrivate):
177
177
  for tag in self.tags_public:
178
178
  with logger.span(f'Pushing image "{tag}"'):
179
179
  for tag, line_bytes in self.client.push(tag, stream_logs=True):
180
- line = line_bytes.decode().rstrip()
181
- logger.info(line.rstrip())
180
+ line = line_bytes.decode()
181
+ logger.info(sanitize(line))
182
182
 
183
183
  self
184
184
 
@@ -12,6 +12,16 @@ else:
12
12
 
13
13
  LEVEL_DEFAULT = logging.DEBUG if env.IS_DEV else logging.INFO
14
14
 
15
+
16
+ def sanitize(message: str) -> str:
17
+ """
18
+
19
+ Sanitize a log line for brace-style formatters.
20
+
21
+ """
22
+ return message.rstrip().replace("{", "{{").replace("}", "}}")
23
+
24
+
15
25
  def get_logger(name, version=None, host=Constants.FMTR_OBS_HOST, key=None, org=Constants.ORG_NAME,
16
26
  stream=STREAM_DEFAULT, environment=ENVIRONMENT_DEFAULT, level=LEVEL_DEFAULT):
17
27
  """
@@ -484,6 +484,7 @@ class Metadata:
484
484
 
485
485
  is_pypi: bool = False
486
486
  is_dockerhub: bool = False
487
+ test_envs: bool = False
487
488
 
488
489
  @classmethod
489
490
  def read(cls, path: Path) -> Self:
@@ -762,6 +763,15 @@ class PackagePaths(FromCallerMixin):
762
763
 
763
764
  return self.repo / Constants.SCRIPTS_DIR
764
765
 
766
+ @property
767
+ def tests(self) -> Path:
768
+ """
769
+
770
+ Path of package tests.
771
+
772
+ """
773
+ return self.path / "tests"
774
+
765
775
  def __repr__(self) -> str:
766
776
  """
767
777
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [tool.corio.dependencies]
6
6
  dev = ["logging", "version.dev", "debug", "sets", "yaml", "db.document", "infra"]
7
7
  install = []
8
- test = ["pytest-cov"]
8
+ test = ["pytest", "pytest-cov"]
9
9
  yaml = ["yamlscript", "pyyaml"]
10
10
  logging = ["logfire", "version"]
11
11
  parallel = ["dask[bag]", "distributed", "bokeh"]
@@ -56,7 +56,7 @@ ha = ["env.io"]
56
56
  "ha.api" = ["ha", "homeassistant_api", "aiohasupervisor"]
57
57
  doc = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav"]
58
58
  youtube = ["pytubefix"]
59
- infra = ["version.dev", "logging", "setup", "doc", "sets", "build", "twine", "packaging", "vcs", "docker.client", "merging", "http", "api", "tomlkit", "secrets", "cli"]
59
+ infra = ["version.dev", "logging", "setup", "doc", "sets", "build", "twine", "packaging", "vcs", "docker.client", "merging", "http", "api", "tomlkit", "secrets", "cli", "test"]
60
60
  vcs = ["pygit2"]
61
61
  tasmota = ["decode-config", "esptool"]
62
62
  encrypt = ["pyrage"]
@@ -64,7 +64,7 @@ secrets = ["encrypt", "env.io", "yaml", "logging", "sets", "vcs"]
64
64
  cli = ["sets", "logging"]
65
65
 
66
66
  [tool.corio.metadata]
67
- version = "2.2.1"
67
+ version = "2.2.2"
68
68
  port = 0
69
69
  base = "python"
70
70
  description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
@@ -77,6 +77,7 @@ services = []
77
77
  keywords = []
78
78
  is_pypi = true
79
79
  is_dockerhub = false
80
+ test_envs = true
80
81
 
81
82
  [tool.corio.metadata.setup]
82
83
 
@@ -93,7 +94,7 @@ corio = ["pyproject.package.toml"]
93
94
 
94
95
  [project]
95
96
  name = "corio"
96
- version = "2.2.1"
97
+ version = "2.2.2"
97
98
  description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
98
99
  readme = "README.md"
99
100
  dependencies = []
@@ -102,8 +103,8 @@ license-files = ["LICENSE"]
102
103
  keywords = []
103
104
 
104
105
  [project.optional-dependencies]
105
- dev = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv"]
106
- test = ["pytest-cov"]
106
+ dev = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov"]
107
+ test = ["pytest", "pytest-cov"]
107
108
  yaml = ["yamlscript", "pyyaml"]
108
109
  logging = ["logfire"]
109
110
  parallel = ["dask[bag]", "distributed", "bokeh"]
@@ -154,13 +155,13 @@ ha = ["dotenv"]
154
155
  "ha.api" = ["dotenv", "homeassistant_api", "aiohasupervisor"]
155
156
  doc = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav"]
156
157
  youtube = ["pytubefix"]
157
- infra = ["semver", "logfire", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv"]
158
+ infra = ["semver", "logfire", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov"]
158
159
  vcs = ["pygit2"]
159
160
  tasmota = ["decode-config", "esptool"]
160
161
  encrypt = ["pyrage"]
161
162
  secrets = ["pyrage", "dotenv", "yamlscript", "pyyaml", "logfire", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "pygit2"]
162
163
  cli = ["pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "logfire"]
163
- all = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest-cov", "dask[bag]", "distributed", "bokeh", "tokenizers", "faker", "sre_yield", "contexttimer", "Unidecode", "tinynetrc", "huggingface_hub", "peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0", "openai", "pydantic-ai-slim[logfire,openai]", "ollama", "json_repair", "sentence_transformers", "pandas", "tabulate", "openpyxl", "odfpy", "deepdiff", "html2text", "flet[all] <0.80.0", "flet-video", "flet-webview", "google-auth", "google-auth-oauthlib", "google-auth-httplib2", "google-api-python-client", "diskcache", "cachetools", "pymupdf", "pymupdf4llm", "appdirs", "filetype", "dnspython[doh]", "regex", "playwright", "aiomqtt", "av", "homeassistant_api", "aiohasupervisor", "pytubefix", "decode-config", "esptool"]
164
+ all = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov", "dask[bag]", "distributed", "bokeh", "tokenizers", "faker", "sre_yield", "contexttimer", "Unidecode", "tinynetrc", "huggingface_hub", "peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0", "openai", "pydantic-ai-slim[logfire,openai]", "ollama", "json_repair", "sentence_transformers", "pandas", "tabulate", "openpyxl", "odfpy", "deepdiff", "html2text", "flet[all] <0.80.0", "flet-video", "flet-webview", "google-auth", "google-auth-oauthlib", "google-auth-httplib2", "google-api-python-client", "diskcache", "cachetools", "pymupdf", "pymupdf4llm", "appdirs", "filetype", "dnspython[doh]", "regex", "playwright", "aiomqtt", "av", "homeassistant_api", "aiohasupervisor", "pytubefix", "decode-config", "esptool"]
164
165
 
165
166
  [[project.authors]]
166
167
  name = "Frontmatter AI"
@@ -173,4 +174,4 @@ Homepage = "https://github.com/fmtr/corio"
173
174
  corio = "corio.entrypoint:main"
174
175
 
175
176
  [dependency-groups]
176
- dev = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv"]
177
+ dev = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov"]
@@ -0,0 +1,39 @@
1
+ from contextlib import nullcontext
2
+
3
+ import corio.caching as caching_module
4
+ from corio.caching import Disk, TLRU
5
+
6
+
7
+ def test_disk_nested_dump(tmp_path):
8
+ cache = Disk(tmp_path / "cache")
9
+
10
+ cache.setdefault("svc", Disk)["enabled"] = True
11
+ cache["count"] = 2
12
+
13
+ dumped = cache.dump()
14
+ assert dumped["count"] == 2
15
+ assert dumped["svc"]["enabled"] is True
16
+
17
+
18
+ def test_tlru_expire_with_custom_timer(monkeypatch):
19
+ class DummyLogger:
20
+ @staticmethod
21
+ def span(_):
22
+ return nullcontext()
23
+
24
+ @staticmethod
25
+ def debug(_):
26
+ return None
27
+
28
+ monkeypatch.setattr(caching_module, "logger", DummyLogger())
29
+
30
+ now = [0]
31
+ cache = TLRU(maxsize=4, timer=lambda: now[0], ttu_static=5, desc="test")
32
+ cache["k1"] = "v1"
33
+
34
+ assert "k1" in cache
35
+ now[0] = 6
36
+ expired = cache.expire()
37
+
38
+ assert expired == [("k1", "v1")]
39
+ assert "k1" not in cache