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.
Files changed (72) hide show
  1. {rics-6.2.0 → rics-6.2.2}/PKG-INFO +1 -1
  2. rics-6.2.2/pyproject.toml +197 -0
  3. rics-6.2.0/pyproject.toml → rics-6.2.2/pyproject.toml.orig +16 -14
  4. {rics-6.2.0 → rics-6.2.2}/src/rics/__init__.py +1 -1
  5. rics-6.2.2/src/rics/__main__.py +8 -0
  6. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/_local_or_remote.py +3 -0
  7. rics-6.2.2/src/rics/_internal_support/notebooks.py +34 -0
  8. rics-6.2.2/src/rics/_pytest_fixtures.py +70 -0
  9. {rics-6.2.0 → rics-6.2.2}/src/rics/click/_util.py +1 -1
  10. {rics-6.2.0 → rics-6.2.2}/src/rics/collections/dicts.py +4 -3
  11. {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/__init__.py +3 -4
  12. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_kernel_helper.py +1 -1
  13. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_venv_helper.py +4 -2
  14. {rics-6.2.0 → rics-6.2.2}/src/rics/logs.py +13 -12
  15. {rics-6.2.0 → rics-6.2.2}/src/rics/misc.py +78 -27
  16. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/__init__.py +1 -0
  17. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_autonumber.py +23 -19
  18. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_generated_data.py +9 -2
  19. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_multi_case_timer.py +111 -79
  20. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_params.py +11 -3
  21. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_plot.py +1 -1
  22. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/_postprocessors.py +1 -0
  23. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_progress.py +1 -0
  24. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_skip_if.py +6 -2
  25. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_strata.py +2 -2
  26. rics-6.2.2/src/rics/performance/_timer.py +114 -0
  27. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_util.py +2 -1
  28. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_wrapper.py +9 -4
  29. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/cli.py +20 -11
  30. {rics-6.2.0 → rics-6.2.2}/src/rics/plotting.py +1 -3
  31. {rics-6.2.0 → rics-6.2.2}/src/rics/strings.py +7 -5
  32. {rics-6.2.0 → rics-6.2.2}/src/rics/types.py +2 -2
  33. rics-6.2.0/src/rics/__main__.py +0 -9
  34. {rics-6.2.0 → rics-6.2.2}/README.md +0 -0
  35. {rics-6.2.0 → rics-6.2.2}/src/rics/_cli_repos.py +0 -0
  36. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/__init__.py +0 -0
  37. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/__init__.py +0 -0
  38. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_patch_notes.py +0 -0
  39. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_split_changelog.py +0 -0
  40. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/changelog/_write_changelog.py +0 -0
  41. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/make_toc_tree_titles_shorter.py +0 -0
  42. {rics-6.2.0 → rics-6.2.2}/src/rics/_internal_support/myst_parser_markdown_doc_refs.py +0 -0
  43. {rics-6.2.0 → rics-6.2.2}/src/rics/_just_the_way_i_like_it.py +0 -0
  44. {rics-6.2.0 → rics-6.2.2}/src/rics/action_level.py +0 -0
  45. {rics-6.2.0 → rics-6.2.2}/src/rics/cli.py +0 -0
  46. {rics-6.2.0 → rics-6.2.2}/src/rics/click/__init__.py +0 -0
  47. {rics-6.2.0 → rics-6.2.2}/src/rics/click/_alias_command_group.py +0 -0
  48. {rics-6.2.0 → rics-6.2.2}/src/rics/click/_logging.py +0 -0
  49. {rics-6.2.0 → rics-6.2.2}/src/rics/collections/__init__.py +0 -0
  50. {rics-6.2.0 → rics-6.2.2}/src/rics/collections/misc.py +0 -0
  51. {rics-6.2.0 → rics-6.2.2}/src/rics/env/__init__.py +0 -0
  52. {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/_file_utils.py +0 -0
  53. {rics-6.2.0 → rics-6.2.2}/src/rics/env/interpolation/_variable.py +0 -0
  54. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/__init__.py +0 -0
  55. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_base.py +0 -0
  56. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_bool.py +0 -0
  57. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_enum.py +0 -0
  58. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_numeric.py +0 -0
  59. {rics-6.2.0 → rics-6.2.2}/src/rics/env/read/_str.py +0 -0
  60. {rics-6.2.0 → rics-6.2.2}/src/rics/envinterp/__init__.py +0 -0
  61. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/__init__.py +0 -0
  62. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_kernel_spec.py +0 -0
  63. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/_print_paths.py +0 -0
  64. {rics-6.2.0 → rics-6.2.2}/src/rics/jupyter/cli.py +0 -0
  65. {rics-6.2.0 → rics-6.2.2}/src/rics/paths.py +0 -0
  66. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_format_perf_counter.py +0 -0
  67. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/_plot/__init__.py +0 -0
  68. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/plot_types.py +0 -0
  69. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/templates/candidates.py.txt +0 -0
  70. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/templates/test_data.py.txt +0 -0
  71. {rics-6.2.0 → rics-6.2.2}/src/rics/performance/types.py +0 -0
  72. {rics-6.2.0 → rics-6.2.2}/src/rics/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rics
3
- Version: 6.2.0
3
+ Version: 6.2.2
4
4
  Summary: My personal Python toolbox.
5
5
  Keywords: rics
6
6
  Author: Richard Sundqvist
@@ -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.0"
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.18.0 ; python_version >= '3.12'",
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
- "ipython~=9.14.1 ; python_version >= '3.12'",
72
- "sphinx-llm~=0.4.1 ; python_version >= '3.12'",
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.5.8",
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.0",
82
+ "pytest~=9.1.1",
82
83
  "xdoctest~=1.3.2",
83
- "coverage~=7.14.1",
84
+ "coverage~=7.16.2",
84
85
  "pytest-cov~=7.1.0",
85
- "responses~=0.26.1",
86
- "poetry~=2.4.1",
86
+ "responses~=0.26.3",
87
+ "poetry~=2.5.1",
87
88
  ]
88
89
  devops = [
89
- "nox~=2026.4.10",
90
+ "nox~=2026.8.17",
90
91
  "invoke~=3.0.3",
91
- "mypy~=2.1.0",
92
- "ruff~=0.15.17",
93
- "codespell~=2.4.2",
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.11.0,<0.12.0"]
198
+ requires = ["uv_build>=0.13.0,<0.14.0"]
197
199
  build-backend = "uv_build"
@@ -19,6 +19,6 @@ __all__ = [
19
19
  "configure_stuff",
20
20
  ]
21
21
 
22
- __version__ = "6.2.0"
22
+ __version__ = "6.2.2"
23
23
 
24
24
  _logging.getLogger(__name__).addHandler(_logging.NullHandler())
@@ -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 is ``None``.
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 `context-specific` are required.
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 'context-specific' present in `mapping`.
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 warps :func:`DictComparer.to_string`.
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 three main ways of using this module:
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 basic interpolation without recursion in an entire string.
20
- * The :func:`rics.misc.interpolate_environment_variables`-function, which adds some additional logic on top of what
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 one:\n pip install ipykernel\n pip install rics[cli]\nto use this feature."
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
- paths = _print_paths(logger, "uv", "run")
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 legts
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 `verbose` is zero, less than -1, or too high.
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=} < -2")
256
+ raise ValueError(f"{verbosity=} < -1")
257
257
  else:
258
- try:
259
- levels = self._levels[:verbosity]
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``. Loggers and adapters are returned as-is.
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 ``None``.
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):