rics 6.2.0__tar.gz → 6.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.
- {rics-6.2.0 → rics-6.2.2}/PKG-INFO +1 -1
- rics-6.2.2/pyproject.toml +197 -0
- rics-6.2.0/pyproject.toml → rics-6.2.2/pyproject.toml.orig +16 -14
- {rics-6.2.0 → rics-6.2.2}/src/rics/__init__.py +1 -1
- rics-6.2.2/src/rics/__main__.py +8 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/_local_or_remote.py +3 -0
- rics-6.2.2/src/rics/_internal_support/notebooks.py +34 -0
- rics-6.2.2/src/rics/_pytest_fixtures.py +70 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/click/_util.py +1 -1
- {rics-6.2.0 → rics-6.2.2}/src/rics/collections/dicts.py +4 -3
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/__init__.py +3 -4
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_kernel_helper.py +1 -1
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_venv_helper.py +4 -2
- {rics-6.2.0 → rics-6.2.2}/src/rics/logs.py +13 -12
- {rics-6.2.0 → rics-6.2.2}/src/rics/misc.py +78 -27
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/__init__.py +1 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_autonumber.py +23 -19
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_generated_data.py +9 -2
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_multi_case_timer.py +111 -79
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_params.py +11 -3
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_plot.py +1 -1
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_postprocessors.py +1 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_progress.py +1 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_skip_if.py +6 -2
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_strata.py +2 -2
- rics-6.2.2/src/rics/performance/_timer.py +114 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_util.py +2 -1
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_wrapper.py +9 -4
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/cli.py +20 -11
- {rics-6.2.0 → rics-6.2.2}/src/rics/plotting.py +1 -3
- {rics-6.2.0 → rics-6.2.2}/src/rics/strings.py +7 -5
- {rics-6.2.0 → rics-6.2.2}/src/rics/types.py +2 -2
- rics-6.2.0/src/rics/__main__.py +0 -9
- {rics-6.2.0 → rics-6.2.2}/README.md +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_cli_repos.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_patch_notes.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_split_changelog.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_write_changelog.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/make_toc_tree_titles_shorter.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/myst_parser_markdown_doc_refs.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/_just_the_way_i_like_it.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/action_level.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/cli.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/click/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/click/_alias_command_group.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/click/_logging.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/collections/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/collections/misc.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/_file_utils.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/_variable.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_base.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_bool.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_enum.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_numeric.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_str.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/envinterp/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_kernel_spec.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_print_paths.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/cli.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/paths.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_format_perf_counter.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/__init__.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/plot_types.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/templates/candidates.py.txt +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/templates/test_data.py.txt +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/performance/types.py +0 -0
- {rics-6.2.0 → rics-6.2.2}/src/rics/py.typed +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rics"
|
|
3
|
+
version = "6.2.2"
|
|
4
|
+
description = "My personal Python toolbox."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = ["rics"]
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Development Status :: 5 - Production/Stable",
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"Intended Audience :: Science/Research",
|
|
11
|
+
"Operating System :: POSIX :: Linux",
|
|
12
|
+
"Operating System :: MacOS",
|
|
13
|
+
"Operating System :: Microsoft :: Windows",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Natural Language :: English",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.11"
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[[project.authors]]
|
|
28
|
+
name = "Richard Sundqvist"
|
|
29
|
+
email = "richard.sundqvist@live.se"
|
|
30
|
+
|
|
31
|
+
[project.license]
|
|
32
|
+
text = "MIT"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
cli = [
|
|
36
|
+
"click >= 8.1.0",
|
|
37
|
+
"pandas >= 2.0.3",
|
|
38
|
+
"requests >= 2.30.0",
|
|
39
|
+
"ipykernel >= 6.29.3",
|
|
40
|
+
]
|
|
41
|
+
plotting = [
|
|
42
|
+
"matplotlib",
|
|
43
|
+
"seaborn",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
homepage = "https://github.com/rsundqvist/rics"
|
|
48
|
+
repository = "https://github.com/rsundqvist/rics"
|
|
49
|
+
documentation = "https://rics.readthedocs.io"
|
|
50
|
+
"Bug Tracker" = "https://github.com/rsundqvist/rics/issues"
|
|
51
|
+
Changelog = "https://github.com/rsundqvist/rics/blob/master/CHANGELOG.md"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
mtimeit = "rics.performance.cli:main"
|
|
55
|
+
rics = "rics.cli:main"
|
|
56
|
+
|
|
57
|
+
[project.entry-points."rics.cli"]
|
|
58
|
+
repos = "rics._cli_repos:main"
|
|
59
|
+
timeit = "rics.performance.cli:main"
|
|
60
|
+
kernel = "rics.jupyter.cli:main"
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
manual-extras = []
|
|
64
|
+
docs = [
|
|
65
|
+
"sphinx~=9.1.0 ; python_version >= '3.12'",
|
|
66
|
+
"pydata-sphinx-theme~=0.23.0 ; python_version >= '3.12'",
|
|
67
|
+
"myst-parser~=5.1.0 ; python_version >= '3.12'",
|
|
68
|
+
"nbsphinx~=0.9.8 ; python_version >= '3.12'",
|
|
69
|
+
"nbformat~=5.11.1 ; python_version >= '3.12'",
|
|
70
|
+
"ipython~=9.17.1 ; python_version >= '3.12'",
|
|
71
|
+
"sphinx-llm~=1.0.0 ; python_version >= '3.12'",
|
|
72
|
+
]
|
|
73
|
+
notebooks = [
|
|
74
|
+
"jupyterlab~=4.6.4",
|
|
75
|
+
"ipywidgets",
|
|
76
|
+
"jupyterlab-execute-time",
|
|
77
|
+
"tqdm",
|
|
78
|
+
]
|
|
79
|
+
test = [
|
|
80
|
+
"pytest~=9.1.1",
|
|
81
|
+
"xdoctest~=1.3.2",
|
|
82
|
+
"coverage~=7.16.2",
|
|
83
|
+
"pytest-cov~=7.1.0",
|
|
84
|
+
"responses~=0.26.3",
|
|
85
|
+
"poetry~=2.5.1",
|
|
86
|
+
]
|
|
87
|
+
devops = [
|
|
88
|
+
"nox~=2026.8.17",
|
|
89
|
+
"invoke~=3.0.3",
|
|
90
|
+
"mypy~=2.4.0",
|
|
91
|
+
"ruff~=0.17.1",
|
|
92
|
+
"codespell~=2.4.3",
|
|
93
|
+
"bump2version==1.0.1",
|
|
94
|
+
]
|
|
95
|
+
types = [
|
|
96
|
+
"types-colorama",
|
|
97
|
+
"types-decorator",
|
|
98
|
+
"types-docutils",
|
|
99
|
+
"types-jsonschema",
|
|
100
|
+
"types-pexpect",
|
|
101
|
+
"types-pycurl",
|
|
102
|
+
"types-pygments",
|
|
103
|
+
"types-pytz",
|
|
104
|
+
"types-pyyaml",
|
|
105
|
+
"types-requests",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[tool.uv]
|
|
109
|
+
default-groups = "all"
|
|
110
|
+
|
|
111
|
+
[tool.ruff]
|
|
112
|
+
line-length = 120
|
|
113
|
+
fix = true
|
|
114
|
+
unsafe-fixes = false
|
|
115
|
+
output-format = "grouped"
|
|
116
|
+
|
|
117
|
+
[tool.ruff.format]
|
|
118
|
+
docstring-code-format = true
|
|
119
|
+
docstring-code-line-length = 80
|
|
120
|
+
line-ending = "lf"
|
|
121
|
+
|
|
122
|
+
[tool.ruff.lint]
|
|
123
|
+
exclude = ["notebooks/*"]
|
|
124
|
+
select = [
|
|
125
|
+
"ANN",
|
|
126
|
+
"S",
|
|
127
|
+
"I",
|
|
128
|
+
"B",
|
|
129
|
+
"D",
|
|
130
|
+
"F",
|
|
131
|
+
"W",
|
|
132
|
+
"E",
|
|
133
|
+
"SLOT",
|
|
134
|
+
"SIM",
|
|
135
|
+
"TID",
|
|
136
|
+
"TD",
|
|
137
|
+
"TCH",
|
|
138
|
+
"ARG",
|
|
139
|
+
"PTH",
|
|
140
|
+
"NPY",
|
|
141
|
+
"PD",
|
|
142
|
+
"N",
|
|
143
|
+
"PERF",
|
|
144
|
+
"PLC",
|
|
145
|
+
"PLE",
|
|
146
|
+
"PLR",
|
|
147
|
+
"PLW",
|
|
148
|
+
"UP",
|
|
149
|
+
"FURB",
|
|
150
|
+
"RUF",
|
|
151
|
+
"TRY",
|
|
152
|
+
]
|
|
153
|
+
ignore = [
|
|
154
|
+
"D105",
|
|
155
|
+
"D107",
|
|
156
|
+
"ANN401",
|
|
157
|
+
"TID252",
|
|
158
|
+
"TRY003",
|
|
159
|
+
"PLC0415",
|
|
160
|
+
"PLR0913",
|
|
161
|
+
"PLR0917",
|
|
162
|
+
"SIM108",
|
|
163
|
+
"TD003",
|
|
164
|
+
"TC006",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
[tool.ruff.lint.per-file-ignores]
|
|
168
|
+
"tests/*" = [
|
|
169
|
+
"D",
|
|
170
|
+
"S101",
|
|
171
|
+
"ANN",
|
|
172
|
+
"PLR2004",
|
|
173
|
+
"S",
|
|
174
|
+
]
|
|
175
|
+
"examples/*" = [
|
|
176
|
+
"D205",
|
|
177
|
+
"B007",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
[tool.ruff.lint.pydocstyle]
|
|
181
|
+
convention = "google"
|
|
182
|
+
|
|
183
|
+
[tool.ruff.lint.pycodestyle]
|
|
184
|
+
max-line-length = 150
|
|
185
|
+
|
|
186
|
+
[tool.pytest.ini_options]
|
|
187
|
+
filterwarnings = [
|
|
188
|
+
"error",
|
|
189
|
+
"ignore:FigureCanvasAgg is non-interactive:UserWarning",
|
|
190
|
+
]
|
|
191
|
+
log_file_level = 10
|
|
192
|
+
log_file = "pytest.log"
|
|
193
|
+
norecursedirs = ["**/cli_modules/*"]
|
|
194
|
+
|
|
195
|
+
[build-system]
|
|
196
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
197
|
+
build-backend = "uv_build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rics"
|
|
3
|
-
version = "6.2.
|
|
3
|
+
version = "6.2.2"
|
|
4
4
|
description = "My personal Python toolbox."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Richard Sundqvist", email = "richard.sundqvist@live.se" },
|
|
@@ -65,32 +65,33 @@ manual-extras = [
|
|
|
65
65
|
]
|
|
66
66
|
docs = [
|
|
67
67
|
"sphinx~=9.1.0 ; python_version >= '3.12'",
|
|
68
|
-
"pydata-sphinx-theme~=0.
|
|
68
|
+
"pydata-sphinx-theme~=0.23.0 ; python_version >= '3.12'",
|
|
69
69
|
"myst-parser~=5.1.0 ; python_version >= '3.12'", # https://github.com/executablebooks/MyST-Parser/issues/749
|
|
70
70
|
"nbsphinx~=0.9.8 ; python_version >= '3.12'",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
71
|
+
"nbformat~=5.11.1 ; python_version >= '3.12'",
|
|
72
|
+
"ipython~=9.17.1 ; python_version >= '3.12'",
|
|
73
|
+
"sphinx-llm~=1.0.0 ; python_version >= '3.12'", # 1.1.0 warns "unknown node type" on nbsphinx nodes, failing -W
|
|
73
74
|
]
|
|
74
75
|
notebooks = [
|
|
75
|
-
"jupyterlab~=4.
|
|
76
|
+
"jupyterlab~=4.6.4",
|
|
76
77
|
"ipywidgets",
|
|
77
78
|
"jupyterlab-execute-time",
|
|
78
79
|
"tqdm",
|
|
79
80
|
]
|
|
80
81
|
test = [
|
|
81
|
-
"pytest~=9.1.
|
|
82
|
+
"pytest~=9.1.1",
|
|
82
83
|
"xdoctest~=1.3.2",
|
|
83
|
-
"coverage~=7.
|
|
84
|
+
"coverage~=7.16.2",
|
|
84
85
|
"pytest-cov~=7.1.0",
|
|
85
|
-
"responses~=0.26.
|
|
86
|
-
"poetry~=2.
|
|
86
|
+
"responses~=0.26.3",
|
|
87
|
+
"poetry~=2.5.1",
|
|
87
88
|
]
|
|
88
89
|
devops = [
|
|
89
|
-
"nox~=2026.
|
|
90
|
+
"nox~=2026.8.17",
|
|
90
91
|
"invoke~=3.0.3",
|
|
91
|
-
"mypy~=2.
|
|
92
|
-
"ruff~=0.
|
|
93
|
-
"codespell~=2.4.
|
|
92
|
+
"mypy~=2.4.0",
|
|
93
|
+
"ruff~=0.17.1",
|
|
94
|
+
"codespell~=2.4.3",
|
|
94
95
|
"bump2version==1.0.1",
|
|
95
96
|
]
|
|
96
97
|
types = [
|
|
@@ -159,6 +160,7 @@ ignore = [
|
|
|
159
160
|
"TRY003",
|
|
160
161
|
"PLC0415", # `import` should be at the top-level of a file
|
|
161
162
|
"PLR0913", #
|
|
163
|
+
"PLR0917", # Too many positional arguments
|
|
162
164
|
"SIM108", # Use ternary operator - coverage issue, see https://github.com/nedbat/coveragepy/issues/509
|
|
163
165
|
"TD003", # Missing issue link
|
|
164
166
|
"TC006", # Add quotes to type expression in `typing.cast()`
|
|
@@ -193,5 +195,5 @@ norecursedirs = [
|
|
|
193
195
|
]
|
|
194
196
|
|
|
195
197
|
[build-system]
|
|
196
|
-
requires = ["uv_build>=0.
|
|
198
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
197
199
|
build-backend = "uv_build"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Alias of :mod:`rics.cli`."""
|
|
2
|
+
|
|
3
|
+
from .cli import main
|
|
4
|
+
|
|
5
|
+
# The docs prefer no guard, but only so a zipapp root behaves the same. Without one, tools that import every submodule
|
|
6
|
+
# (e.g. Sphinx autosummary) run the CLI with their own argv.
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
main()
|
|
@@ -107,7 +107,10 @@ def _fetch(local_file_path: Path, remote_file_path: str, show_progress: bool) ->
|
|
|
107
107
|
|
|
108
108
|
_GLOR_LOGGER.info(f"Fetching data from '{remote_file_path}'..")
|
|
109
109
|
response = requests.get(remote_file_path, stream=True, timeout=300)
|
|
110
|
+
response.raise_for_status()
|
|
110
111
|
chunks = response.iter_content(_CHUNK_SIZE)
|
|
112
|
+
# TODO(6.3.0): A download that fails midway leaves a partial file, which later calls serve as cached. Write to a
|
|
113
|
+
# temporary file and rename it on success.
|
|
111
114
|
with local_file_path.open("wb") as output_file:
|
|
112
115
|
if show_progress:
|
|
113
116
|
total = response.headers.get("Content-Length")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def copy_notebooks(source: str | Path, destination: str | Path, *, clean: bool = True) -> None:
|
|
6
|
+
"""Copy the `source` directory to `destination`, optionally stripping notebook metadata in the copy.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
source: Directory to copy.
|
|
10
|
+
destination: Target directory; existing files are overwritten.
|
|
11
|
+
clean: If ``True``, call :func:`strip_metadata` on every ``.ipynb`` file in `destination` after copying.
|
|
12
|
+
"""
|
|
13
|
+
shutil.copytree(source, destination, dirs_exist_ok=True)
|
|
14
|
+
if clean:
|
|
15
|
+
for path in Path(destination).rglob("*.ipynb"):
|
|
16
|
+
strip_metadata(path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def strip_metadata(path: str | Path) -> None:
|
|
20
|
+
"""Strip volatile cell metadata from the notebook at `path`, in place.
|
|
21
|
+
|
|
22
|
+
Removes the ``execution`` timing metadata.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
path: Path to an ``.ipynb`` file.
|
|
26
|
+
"""
|
|
27
|
+
import nbformat
|
|
28
|
+
|
|
29
|
+
notebook = nbformat.read(path, as_version=nbformat.NO_CONVERT)
|
|
30
|
+
|
|
31
|
+
for cell in notebook.cells:
|
|
32
|
+
cell.metadata.pop("execution", None)
|
|
33
|
+
|
|
34
|
+
nbformat.write(notebook, path)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Pytest fixtures, loaded through ``pytest_plugins``.
|
|
2
|
+
|
|
3
|
+
Imports ``pytest`` at module scope since fixtures need the decorator; never import this module from ``rics`` itself.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def tmp_path_cwd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
20
|
+
"""Run the test with `tmp_path` as the working directory."""
|
|
21
|
+
monkeypatch.chdir(tmp_path)
|
|
22
|
+
return tmp_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def restore_logging() -> Iterator[None]:
|
|
27
|
+
"""Restore the level, handlers, ``propagate`` and ``disabled`` of every logger after the test.
|
|
28
|
+
|
|
29
|
+
Loggers created during the test are reset to the state of a new logger. Handlers are re-attached as-is; one that
|
|
30
|
+
the test closed (e.g. through ``logging.basicConfig(force=True)``) stays closed.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def loggers() -> Iterator[logging.Logger]:
|
|
34
|
+
yield logging.root
|
|
35
|
+
yield from (lg for lg in logging.Logger.manager.loggerDict.values() if isinstance(lg, logging.Logger))
|
|
36
|
+
|
|
37
|
+
snapshot = {lg: (lg.level, lg.propagate, lg.disabled, list(lg.handlers)) for lg in loggers()}
|
|
38
|
+
disable = logging.root.manager.disable
|
|
39
|
+
|
|
40
|
+
yield
|
|
41
|
+
|
|
42
|
+
logging.disable(disable)
|
|
43
|
+
for lg in loggers():
|
|
44
|
+
level, lg.propagate, lg.disabled, lg.handlers[:] = snapshot.get(lg, (logging.NOTSET, True, False, []))
|
|
45
|
+
lg.setLevel(level) # Clears the isEnabledFor-cache, unlike assigning `level` directly.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def unload_modules() -> Iterator[Callable[..., None]]:
|
|
50
|
+
"""Unload modules so that the test imports them anew; restore the original ``sys.modules`` entries afterwards.
|
|
51
|
+
|
|
52
|
+
Call the returned function with module names; each name covers its submodules as well. Modules first imported
|
|
53
|
+
during the test are unloaded again at teardown.
|
|
54
|
+
"""
|
|
55
|
+
names: list[str] = []
|
|
56
|
+
snapshot: dict[str, ModuleType] = {}
|
|
57
|
+
|
|
58
|
+
def matching() -> list[str]:
|
|
59
|
+
return [m for m in sys.modules if any(m == name or m.startswith(f"{name}.") for name in names)]
|
|
60
|
+
|
|
61
|
+
def unload(*modules: str) -> None:
|
|
62
|
+
names.extend(modules)
|
|
63
|
+
for m in matching():
|
|
64
|
+
snapshot.setdefault(m, sys.modules.pop(m))
|
|
65
|
+
|
|
66
|
+
yield unload
|
|
67
|
+
|
|
68
|
+
for m in matching():
|
|
69
|
+
del sys.modules[m]
|
|
70
|
+
sys.modules.update(snapshot)
|
|
@@ -9,7 +9,7 @@ def get_user_command(cxt: click.Context | None = None, *, meta: bool = True) ->
|
|
|
9
9
|
"""Recreate the user CLI command.
|
|
10
10
|
|
|
11
11
|
Args:
|
|
12
|
-
cxt: Click context. Derive
|
|
12
|
+
cxt: Click context. Derive if ``None``.
|
|
13
13
|
meta: If ``True``, use :attr:`click.Context.meta` caching.
|
|
14
14
|
|
|
15
15
|
Returns:
|
|
@@ -289,7 +289,7 @@ class InheritedKeysDict(_t.Mapping[OKT, dict[KT, VT]]): # noqa: PLW1641
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
No other top-level keys are accepted, but neither `default` nor `
|
|
292
|
+
No other top-level keys are accepted, but neither `default` nor `specific` are required.
|
|
293
293
|
|
|
294
294
|
Args:
|
|
295
295
|
arg: Input to make an instance from.
|
|
@@ -298,12 +298,13 @@ class InheritedKeysDict(_t.Mapping[OKT, dict[KT, VT]]): # noqa: PLW1641
|
|
|
298
298
|
A new instance.
|
|
299
299
|
|
|
300
300
|
Raises:
|
|
301
|
-
ValueError: If there are any keys other than 'default' and '
|
|
301
|
+
ValueError: If there are any keys other than 'default' and 'specific' present in `arg`.
|
|
302
302
|
|
|
303
303
|
"""
|
|
304
304
|
if isinstance(arg, InheritedKeysDict): # pragma: no cover
|
|
305
305
|
return arg
|
|
306
306
|
|
|
307
|
+
arg = arg.copy()
|
|
307
308
|
default: dict[KT, VT] | None = arg.pop("default", None)
|
|
308
309
|
specific: dict[OKT, dict[KT, VT]] | None = arg.pop("specific", None)
|
|
309
310
|
|
|
@@ -373,7 +374,7 @@ def format_changed_keys(
|
|
|
373
374
|
'added(1): {-1: None}, removed(1): {2}'
|
|
374
375
|
|
|
375
376
|
Notes:
|
|
376
|
-
This is a convenience function that
|
|
377
|
+
This is a convenience function that wraps :func:`DictComparer.to_string`.
|
|
377
378
|
"""
|
|
378
379
|
return DictComparer(mapping, baseline=baseline).to_string(added=added, updated=updated, removed=removed)
|
|
379
380
|
|
|
@@ -12,13 +12,12 @@ Default values:
|
|
|
12
12
|
Default values are specified by stating the default value after a colon; ``${<var>:default-value}``. The default
|
|
13
13
|
value may be blank, in which case an empty string is used as the default.
|
|
14
14
|
|
|
15
|
-
There are
|
|
15
|
+
There are two main ways of using this module:
|
|
16
16
|
|
|
17
17
|
* The :meth:`.Variable.parse_string`-function, combined with :meth:`.Variable.get_value`. This gives you the most
|
|
18
18
|
amount of control.
|
|
19
|
-
* The :func:`replace_in_string`-function, for
|
|
20
|
-
|
|
21
|
-
the :class:`Variable`-methods provide.
|
|
19
|
+
* The :func:`replace_in_string`-function, for interpolation in an entire string. Unlike
|
|
20
|
+
:meth:`.Variable.get_value`, it resolves nested defaults by default; ``allow_nested=False`` raises on them.
|
|
22
21
|
|
|
23
22
|
Examples:
|
|
24
23
|
We'll set ``ENV_VAR0=VALUE0`` and ``ENV_VAR1=VALUE1``. Vars 2 and 4 are not set.
|
|
@@ -84,7 +84,7 @@ class KernelHelper:
|
|
|
84
84
|
try:
|
|
85
85
|
from ipykernel.kernelspec import install
|
|
86
86
|
except ModuleNotFoundError as e:
|
|
87
|
-
msg = "Package `ipykernel` not installed. Run of
|
|
87
|
+
msg = "Package `ipykernel` not installed. Run one of:\n pip install ipykernel\n pip install rics[cli]\nto use this feature."
|
|
88
88
|
raise RuntimeError(msg) from e
|
|
89
89
|
|
|
90
90
|
logger = self.logger
|
|
@@ -109,13 +109,15 @@ class Resolve:
|
|
|
109
109
|
if not _check_files(cwd, "uv", logger):
|
|
110
110
|
return None
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
# --project pins resolution to `cwd`; without it, `uv run <script>` resolves the project from the
|
|
113
|
+
# script's own location once the script lives outside `cwd` (as `_print_paths.py` always does).
|
|
114
|
+
paths = _print_paths(logger, "uv", "run", "--project", str(cwd))
|
|
113
115
|
|
|
114
116
|
if paths is None:
|
|
115
117
|
return None
|
|
116
118
|
|
|
117
119
|
logger.debug("Ensure pip in uv venv.") # Not installed unless --seed/UV_VENV_SEED=pip is used.
|
|
118
|
-
_get_output(logger, "uv", "pip", "install", "--no-upgrade", "--quiet", "pip")
|
|
120
|
+
_get_output(logger, "uv", "pip", "install", "--project", str(cwd), "--no-upgrade", "--quiet", "pip")
|
|
119
121
|
|
|
120
122
|
return paths
|
|
121
123
|
|
|
@@ -201,7 +201,7 @@ class LoggingSetupHelper:
|
|
|
201
201
|
|
|
202
202
|
>>> [{"id_translation": "DEBUG"}, {"id_translation": "INFO"}]
|
|
203
203
|
|
|
204
|
-
are **not** permitted, since going from ``verbosity=1`` to ``verbosity=2`` would reduce the amount of
|
|
204
|
+
are **not** permitted, since going from ``verbosity=1`` to ``verbosity=2`` would reduce the amount of logs
|
|
205
205
|
emitted. Calling :meth:`LoggingSetupHelper.configure_logging` with ``verbosity=0`` disables logging entirely.
|
|
206
206
|
|
|
207
207
|
Args:
|
|
@@ -239,13 +239,13 @@ class LoggingSetupHelper:
|
|
|
239
239
|
"""Get log levels per logger at the given `verbosity` level.
|
|
240
240
|
|
|
241
241
|
Args:
|
|
242
|
-
verbosity: Verbosity level. Pass -1 for max level.
|
|
242
|
+
verbosity: Verbosity level. Pass -1 for max level; levels above the max are treated as the max.
|
|
243
243
|
|
|
244
244
|
Returns:
|
|
245
245
|
A dict ``{logger: log_level}``
|
|
246
246
|
|
|
247
247
|
Raises:
|
|
248
|
-
ValueError: If `
|
|
248
|
+
ValueError: If `verbosity` is zero or less than -1.
|
|
249
249
|
"""
|
|
250
250
|
max_level = -1
|
|
251
251
|
if verbosity in {max_level, self.max}:
|
|
@@ -253,13 +253,10 @@ class LoggingSetupHelper:
|
|
|
253
253
|
elif verbosity == 0:
|
|
254
254
|
raise ValueError(f"{verbosity=} = 0")
|
|
255
255
|
elif verbosity < max_level:
|
|
256
|
-
raise ValueError(f"{verbosity=} < -
|
|
256
|
+
raise ValueError(f"{verbosity=} < -1")
|
|
257
257
|
else:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
except IndexError:
|
|
261
|
-
msg = f"Got {verbosity=}, but only {self.max} levels are defined."
|
|
262
|
-
raise ValueError(msg) from None
|
|
258
|
+
# TODO(6.3.0): Emit a DeprecationWarning for verbosity > self.max; raise in 7.0.0.
|
|
259
|
+
levels = self._levels[:verbosity]
|
|
263
260
|
|
|
264
261
|
rv: dict[str, int] = {}
|
|
265
262
|
for level_dict in reversed(levels):
|
|
@@ -444,17 +441,21 @@ def get_logger(logger: AnyLoggerAdapter) -> AnyLoggerAdapter: ...
|
|
|
444
441
|
def get_logger(logger: LoggerArg | AnyLoggerAdapter) -> _l.Logger | AnyLoggerAdapter:
|
|
445
442
|
"""Get a logger.
|
|
446
443
|
|
|
447
|
-
Returns a :attr:`~logging.Logger.disabled` logger if `logger` is ``None
|
|
444
|
+
Returns a :attr:`~logging.Logger.disabled` logger if `logger` is ``None`` or ``False``; its child loggers are
|
|
445
|
+
silenced too. Loggers and adapters are returned as-is.
|
|
448
446
|
|
|
449
447
|
Args:
|
|
450
|
-
logger: An adapter, logger, logger name, or ``
|
|
448
|
+
logger: An adapter, logger, logger name, ``None``, or ``False``.
|
|
451
449
|
|
|
452
450
|
Returns:
|
|
453
451
|
A logger instance.
|
|
454
452
|
"""
|
|
455
|
-
if logger is None:
|
|
453
|
+
if logger is None or logger is False:
|
|
456
454
|
logger = _l.getLogger("null")
|
|
457
455
|
logger.disabled = True
|
|
456
|
+
logger.propagate = False
|
|
457
|
+
if not logger.handlers:
|
|
458
|
+
logger.addHandler(_l.NullHandler())
|
|
458
459
|
return logger
|
|
459
460
|
|
|
460
461
|
if isinstance(logger, str):
|