ruyi 0.41.0b20250924__tar.gz → 0.42.0__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.
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/PKG-INFO +18 -11
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/README.md +16 -9
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/pyproject.toml +3 -2
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/config_cli.py +20 -4
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/main.py +30 -8
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/oobe.py +7 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/self_cli.py +7 -2
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/config/__init__.py +121 -38
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/config/editor.py +34 -3
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/config/errors.py +22 -6
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/config/schema.py +72 -17
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/device/provision.py +1 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/api.py +1 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/distfile.py +12 -4
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/install.py +99 -32
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/install_cli.py +29 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/profile.py +148 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/repo.py +6 -3
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/state.py +10 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/unpack.py +14 -16
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/provider.py +65 -22
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/store.py +1 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/git.py +4 -2
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/global_mode.py +19 -1
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/toml.py +11 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/xdg_basedir.py +20 -13
- ruyi-0.42.0/ruyi/version.py +20 -0
- ruyi-0.41.0b20250924/ruyi/mux/.gitignore +0 -1
- ruyi-0.41.0b20250924/ruyi/version.py +0 -67
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/LICENSE-Apache.txt +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/__main__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/builtin_commands.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/cmd.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/completer.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/completion.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/user_input.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/cli/version_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/config/news.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/device/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/device/provision_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/log/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/runtime.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/venv/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/venv/emulator_cfg.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/venv/maker.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/venv/venv_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/mux/venv_cfg.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/ctx.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/paths.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/plugin_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/pluginhost/unsandboxed.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/py.typed +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/resource_bundle/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/resource_bundle/__main__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/resource_bundle/data.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/admin_checksum.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/admin_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/atom.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/augmented_pkg.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/canonical_dump.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/checksum.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/cli_completion.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/entity.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/entity_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/entity_provider.py +0 -0
- /ruyi-0.41.0b20250924/ruyi/ruyipkg/fetch.py → /ruyi-0.42.0/ruyi/ruyipkg/fetcher.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/host.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/list.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/list_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/list_filter.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/msg.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/news.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/news_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/news_store.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/pkg_manifest.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/profile_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/protocols.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/unpack_method.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/ruyipkg/update_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/aggregate.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/event.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/node_info.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/scope.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/telemetry/telemetry_cli.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/__init__.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/ar.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/ci.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/frontmatter.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/l10n.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/markdown.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/mounts.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/nuitka.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/porcelain.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/prereqs.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/ssl_patch.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/templating.py +0 -0
- {ruyi-0.41.0b20250924 → ruyi-0.42.0}/ruyi/utils/url.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruyi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.42.0
|
|
4
4
|
Summary: Package manager for RuyiSDK
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -221,7 +221,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
221
221
|
Classifier: Topic :: Software Development :: Embedded Systems
|
|
222
222
|
Classifier: Topic :: System :: Software Distribution
|
|
223
223
|
Classifier: Typing :: Typed
|
|
224
|
-
Requires-Dist: argcomplete (>=2.0.0
|
|
224
|
+
Requires-Dist: argcomplete (>=2.0.0)
|
|
225
225
|
Requires-Dist: arpy
|
|
226
226
|
Requires-Dist: fastjsonschema (>=2.15.1)
|
|
227
227
|
Requires-Dist: jinja2 (>=3,<4)
|
|
@@ -332,6 +332,10 @@ Various aspects of `ruyi` can be configured with files or environment variables.
|
|
|
332
332
|
look up its config accordingly. If these are not explicitly set though, as in
|
|
333
333
|
typical use cases, the default config directory is most likely `~/.config/ruyi`.
|
|
334
334
|
|
|
335
|
+
GNU/Linux distribution packagers and system administrators will find that the
|
|
336
|
+
directories `/usr/share/ruyi` and `/usr/local/share/ruyi` are searched for the
|
|
337
|
+
config file on such systems.
|
|
338
|
+
|
|
335
339
|
### Config file
|
|
336
340
|
|
|
337
341
|
Currently `ruyi` will look for an optional `config.toml` in its XDG config
|
|
@@ -361,9 +365,8 @@ branch = "main"
|
|
|
361
365
|
# details.
|
|
362
366
|
#
|
|
363
367
|
# If unset or empty, this default value is used: data will be collected and
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
mode = "on"
|
|
368
|
+
# stored locally; nothing will be uploaded automatically.
|
|
369
|
+
mode = "local"
|
|
367
370
|
# The time the user's consent is given to telemetry data uploading. If the
|
|
368
371
|
# system time is later than the time given here, telemetry consent banner will
|
|
369
372
|
# not be displayed any more each time `ruyi` is executed. The exact consent
|
|
@@ -407,12 +410,16 @@ There are 3 telemetry modes available:
|
|
|
407
410
|
* `off`: data will not be collected nor uploaded.
|
|
408
411
|
* `on`: data will be collected and periodically uploaded.
|
|
409
412
|
|
|
410
|
-
By default the `
|
|
411
|
-
will record some non-sensitive information locally
|
|
412
|
-
states of `ruyi`,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
|
|
414
|
+
means every `ruyi` invocation will record some non-sensitive information locally
|
|
415
|
+
alongside various other states of `ruyi`, but collected data will not be
|
|
416
|
+
uploaded automatically unless you explicitly request so (for example by
|
|
417
|
+
switching to the `on` mode, or by executing `ruyi telemetry upload`).
|
|
418
|
+
|
|
419
|
+
In case the `on` mode is active, collected data will be periodically uploaded
|
|
420
|
+
to servers managed by the RuyiSDK team in the People's Republic of China, in a
|
|
421
|
+
weekly fashion. The upload will happen on a random weekday which is determined
|
|
422
|
+
by the installation's anonymous ID alone.
|
|
416
423
|
|
|
417
424
|
You can change the telemetry mode by editing `ruyi`'s config file, or simply
|
|
418
425
|
disable telemetry altogether by setting the `RUYI_TELEMETRY_OPTOUT` environment
|
|
@@ -88,6 +88,10 @@ Various aspects of `ruyi` can be configured with files or environment variables.
|
|
|
88
88
|
look up its config accordingly. If these are not explicitly set though, as in
|
|
89
89
|
typical use cases, the default config directory is most likely `~/.config/ruyi`.
|
|
90
90
|
|
|
91
|
+
GNU/Linux distribution packagers and system administrators will find that the
|
|
92
|
+
directories `/usr/share/ruyi` and `/usr/local/share/ruyi` are searched for the
|
|
93
|
+
config file on such systems.
|
|
94
|
+
|
|
91
95
|
### Config file
|
|
92
96
|
|
|
93
97
|
Currently `ruyi` will look for an optional `config.toml` in its XDG config
|
|
@@ -117,9 +121,8 @@ branch = "main"
|
|
|
117
121
|
# details.
|
|
118
122
|
#
|
|
119
123
|
# If unset or empty, this default value is used: data will be collected and
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
mode = "on"
|
|
124
|
+
# stored locally; nothing will be uploaded automatically.
|
|
125
|
+
mode = "local"
|
|
123
126
|
# The time the user's consent is given to telemetry data uploading. If the
|
|
124
127
|
# system time is later than the time given here, telemetry consent banner will
|
|
125
128
|
# not be displayed any more each time `ruyi` is executed. The exact consent
|
|
@@ -163,12 +166,16 @@ There are 3 telemetry modes available:
|
|
|
163
166
|
* `off`: data will not be collected nor uploaded.
|
|
164
167
|
* `on`: data will be collected and periodically uploaded.
|
|
165
168
|
|
|
166
|
-
By default the `
|
|
167
|
-
will record some non-sensitive information locally
|
|
168
|
-
states of `ruyi`,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
|
|
170
|
+
means every `ruyi` invocation will record some non-sensitive information locally
|
|
171
|
+
alongside various other states of `ruyi`, but collected data will not be
|
|
172
|
+
uploaded automatically unless you explicitly request so (for example by
|
|
173
|
+
switching to the `on` mode, or by executing `ruyi telemetry upload`).
|
|
174
|
+
|
|
175
|
+
In case the `on` mode is active, collected data will be periodically uploaded
|
|
176
|
+
to servers managed by the RuyiSDK team in the People's Republic of China, in a
|
|
177
|
+
weekly fashion. The upload will happen on a random weekday which is determined
|
|
178
|
+
by the installation's anonymous ID alone.
|
|
172
179
|
|
|
173
180
|
You can change the telemetry mode by editing `ruyi`'s config file, or simply
|
|
174
181
|
disable telemetry altogether by setting the `RUYI_TELEMETRY_OPTOUT` environment
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ruyi"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.42.0"
|
|
8
8
|
description = "Package manager for RuyiSDK"
|
|
9
9
|
keywords = ["ruyi", "ruyisdk"]
|
|
10
10
|
license = { file = "LICENSE-Apache.txt" }
|
|
@@ -29,6 +29,7 @@ classifiers = [
|
|
|
29
29
|
]
|
|
30
30
|
requires-python = ">=3.10"
|
|
31
31
|
dependencies = [
|
|
32
|
+
"argcomplete>=2.0.0",
|
|
32
33
|
"arpy",
|
|
33
34
|
"fastjsonschema>=2.15.1",
|
|
34
35
|
"jinja2 (>=3, <4)",
|
|
@@ -41,7 +42,6 @@ dependencies = [
|
|
|
41
42
|
"tomlkit>=0.9",
|
|
42
43
|
"tomli>=1.2; python_version<'3.11'",
|
|
43
44
|
"tzdata; sys_platform=='win32'",
|
|
44
|
-
"argcomplete (>=2.0.0,<4.0.0)",
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
[project.scripts]
|
|
@@ -57,6 +57,7 @@ repository = "https://github.com/ruyisdk/ruyi.git"
|
|
|
57
57
|
|
|
58
58
|
[tool.poetry]
|
|
59
59
|
include = ["ruyi/py.typed"]
|
|
60
|
+
exclude = ["**/.gitignore"]
|
|
60
61
|
|
|
61
62
|
[tool.poetry.group.dev.dependencies]
|
|
62
63
|
mypy = "^1.9.0"
|
|
@@ -43,15 +43,23 @@ class ConfigGetCommand(
|
|
|
43
43
|
|
|
44
44
|
@classmethod
|
|
45
45
|
def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
|
|
46
|
+
from ..config.errors import InvalidConfigKeyError
|
|
46
47
|
from ..config.schema import encode_value
|
|
48
|
+
from ..utils.toml import NoneValue
|
|
47
49
|
|
|
48
50
|
key: str = args.key
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
try:
|
|
53
|
+
val = cfg.get_by_key(key)
|
|
54
|
+
except InvalidConfigKeyError:
|
|
52
55
|
return 1
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
try:
|
|
58
|
+
encoded_val = encode_value(val)
|
|
59
|
+
except NoneValue:
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
cfg.logger.stdout(encoded_val)
|
|
55
63
|
return 0
|
|
56
64
|
|
|
57
65
|
|
|
@@ -80,6 +88,7 @@ class ConfigSetCommand(
|
|
|
80
88
|
@classmethod
|
|
81
89
|
def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
|
|
82
90
|
from ..config.editor import ConfigEditor
|
|
91
|
+
from ..config.errors import ProtectedGlobalConfigError
|
|
83
92
|
from ..config.schema import decode_value
|
|
84
93
|
|
|
85
94
|
key: str = args.key
|
|
@@ -87,7 +96,14 @@ class ConfigSetCommand(
|
|
|
87
96
|
|
|
88
97
|
pyval = decode_value(key, val)
|
|
89
98
|
with ConfigEditor.work_on_user_local_config(cfg) as ed:
|
|
90
|
-
|
|
99
|
+
try:
|
|
100
|
+
ed.set_value(key, pyval)
|
|
101
|
+
except ProtectedGlobalConfigError:
|
|
102
|
+
cfg.logger.F(
|
|
103
|
+
f"the config [yellow]{key}[/] is protected and not meant to be overridden by users",
|
|
104
|
+
)
|
|
105
|
+
return 2
|
|
106
|
+
|
|
91
107
|
ed.stage()
|
|
92
108
|
|
|
93
109
|
return 0
|
|
@@ -6,6 +6,7 @@ from typing import Final, TYPE_CHECKING
|
|
|
6
6
|
from ..config import GlobalConfig
|
|
7
7
|
from ..telemetry.scope import TelemetryScope
|
|
8
8
|
from ..utils.global_mode import GlobalModeProvider
|
|
9
|
+
from ..version import RUYI_SEMVER
|
|
9
10
|
from . import RUYI_ENTRYPOINT_NAME
|
|
10
11
|
from .oobe import OOBE
|
|
11
12
|
|
|
@@ -21,18 +22,40 @@ def is_called_as_ruyi(argv0: str) -> bool:
|
|
|
21
22
|
return os.path.basename(argv0).lower() in ALLOWED_RUYI_ENTRYPOINT_NAMES
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def should_prompt_for_renaming(argv0: str) -> bool:
|
|
26
|
+
# We need to allow things like "ruyi-qemu" through, to not break our mux.
|
|
27
|
+
# Only consider filenames starting with both our name *and* version to be
|
|
28
|
+
# un-renamed onefile artifacts that warrant a rename prompt.
|
|
29
|
+
likely_artifact_name_prefix = f"{RUYI_ENTRYPOINT_NAME}-{RUYI_SEMVER}."
|
|
30
|
+
return os.path.basename(argv0).lower().startswith(likely_artifact_name_prefix)
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
|
|
25
|
-
|
|
34
|
+
logger = gc.logger
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
oobe
|
|
36
|
+
# do not init telemetry or OOBE on CLI auto-completion invocations, because
|
|
37
|
+
# our output isn't meant for humans in that case, and a "real" invocation
|
|
38
|
+
# will likely follow shortly after
|
|
39
|
+
if not gm.is_cli_autocomplete:
|
|
40
|
+
oobe = OOBE(gc)
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
if tm := gc.telemetry:
|
|
43
|
+
tm.check_first_run_status()
|
|
44
|
+
tm.init_installation(False)
|
|
45
|
+
atexit.register(tm.flush)
|
|
46
|
+
oobe.handlers.append(tm.oobe_prompt)
|
|
47
|
+
|
|
48
|
+
oobe.maybe_prompt()
|
|
34
49
|
|
|
35
50
|
if not is_called_as_ruyi(gm.argv0):
|
|
51
|
+
if should_prompt_for_renaming(gm.argv0):
|
|
52
|
+
logger.F(
|
|
53
|
+
f"the {RUYI_ENTRYPOINT_NAME} executable must be named [green]'{RUYI_ENTRYPOINT_NAME}'[/] to work"
|
|
54
|
+
)
|
|
55
|
+
logger.I(f"it is now [yellow]'{gm.argv0}'[/]")
|
|
56
|
+
logger.I("please rename the command file and retry")
|
|
57
|
+
return 1
|
|
58
|
+
|
|
36
59
|
from ..mux.runtime import mux_main
|
|
37
60
|
|
|
38
61
|
# record an invocation and the command name being proxied to
|
|
@@ -54,7 +77,6 @@ def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
|
|
|
54
77
|
if TYPE_CHECKING:
|
|
55
78
|
from .cmd import CLIEntrypoint
|
|
56
79
|
|
|
57
|
-
logger = gc.logger
|
|
58
80
|
p = RootCommand.build_argparse(gc)
|
|
59
81
|
|
|
60
82
|
# We have to ensure argcomplete is only requested when it's supposed to,
|
|
@@ -41,10 +41,16 @@ class OOBE:
|
|
|
41
41
|
def should_prompt(self) -> bool:
|
|
42
42
|
from ..utils.global_mode import is_env_var_truthy
|
|
43
43
|
|
|
44
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
45
|
+
# This is of higher priority than even the debug override, because
|
|
46
|
+
# we don't want to mess up non-interactive sessions even in case of
|
|
47
|
+
# debugging.
|
|
48
|
+
return False
|
|
49
|
+
|
|
44
50
|
if is_env_var_truthy(os.environ, "RUYI_DEBUG_FORCE_FIRST_RUN"):
|
|
45
51
|
return True
|
|
46
52
|
|
|
47
|
-
return self.is_first_run()
|
|
53
|
+
return self.is_first_run()
|
|
48
54
|
|
|
49
55
|
def maybe_prompt(self) -> None:
|
|
50
56
|
if not self.should_prompt():
|
|
@@ -131,12 +131,16 @@ class SelfCleanCommand(
|
|
|
131
131
|
_do_reset(
|
|
132
132
|
cfg,
|
|
133
133
|
quiet=quiet,
|
|
134
|
+
# state-related
|
|
135
|
+
all_state=all,
|
|
136
|
+
news_read_status=news_read_status,
|
|
137
|
+
telemetry=telemetry,
|
|
138
|
+
# cache-related
|
|
139
|
+
all_cache=all,
|
|
134
140
|
distfiles=distfiles,
|
|
135
141
|
installed_pkgs=installed_pkgs,
|
|
136
|
-
news_read_status=news_read_status,
|
|
137
142
|
progcache=progcache,
|
|
138
143
|
repo=repo,
|
|
139
|
-
telemetry=telemetry,
|
|
140
144
|
)
|
|
141
145
|
|
|
142
146
|
return 0
|
|
@@ -233,6 +237,7 @@ def _do_reset(
|
|
|
233
237
|
if installed_pkgs:
|
|
234
238
|
status("removing installed packages")
|
|
235
239
|
shutil.rmtree(cfg.data_root, True)
|
|
240
|
+
cfg.ruyipkg_global_state.purge_installation_info()
|
|
236
241
|
|
|
237
242
|
# do not record any telemetry data if we're purging it
|
|
238
243
|
if all_state or telemetry:
|
|
@@ -15,8 +15,10 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from ..ruyipkg.state import RuyipkgGlobalStateStore
|
|
16
16
|
from ..telemetry.provider import TelemetryProvider
|
|
17
17
|
from ..utils.global_mode import ProvidesGlobalMode
|
|
18
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
18
19
|
from .news import NewsReadStatusStore
|
|
19
20
|
|
|
21
|
+
from . import errors
|
|
20
22
|
from . import schema
|
|
21
23
|
|
|
22
24
|
|
|
@@ -33,6 +35,7 @@ else:
|
|
|
33
35
|
DEFAULT_APP_NAME: Final = "ruyi"
|
|
34
36
|
DEFAULT_REPO_URL: Final = "https://github.com/ruyisdk/packages-index.git"
|
|
35
37
|
DEFAULT_REPO_BRANCH: Final = "main"
|
|
38
|
+
DEFAULT_TELEMETRY_MODE: Final = "local" # "off", "local", "on"
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def get_host_path_fragment_for_binary_install_dir(canonicalized_host: str) -> str:
|
|
@@ -101,12 +104,21 @@ class GlobalConfig:
|
|
|
101
104
|
self._telemetry_upload_consent: datetime.datetime | None = None
|
|
102
105
|
self._telemetry_pm_telemetry_url: str | None = None
|
|
103
106
|
|
|
104
|
-
def
|
|
107
|
+
def _apply_config(
|
|
108
|
+
self,
|
|
109
|
+
config_data: GlobalConfigRootType,
|
|
110
|
+
*,
|
|
111
|
+
is_global_scope: bool,
|
|
112
|
+
) -> None:
|
|
105
113
|
if ins_cfg := config_data.get(schema.SECTION_INSTALLATION):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
iem = ins_cfg.get(schema.KEY_INSTALLATION_EXTERNALLY_MANAGED, None)
|
|
115
|
+
if iem is not None and not is_global_scope:
|
|
116
|
+
iem_cfg_key = f"{schema.SECTION_INSTALLATION}.{schema.KEY_INSTALLATION_EXTERNALLY_MANAGED}"
|
|
117
|
+
self.logger.W(
|
|
118
|
+
f"the config key [yellow]{iem_cfg_key}[/] cannot be set from user config; ignoring",
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
self.is_installation_externally_managed = bool(iem)
|
|
110
122
|
|
|
111
123
|
if pkgs_cfg := config_data.get(schema.SECTION_PACKAGES):
|
|
112
124
|
self.include_prereleases = pkgs_cfg.get(
|
|
@@ -137,61 +149,84 @@ class GlobalConfig:
|
|
|
137
149
|
if isinstance(consent, datetime.datetime):
|
|
138
150
|
self._telemetry_upload_consent = consent
|
|
139
151
|
|
|
140
|
-
def get_by_key(self, key: str | Sequence[str]) -> object
|
|
152
|
+
def get_by_key(self, key: str | Sequence[str]) -> object:
|
|
153
|
+
parsed_key = schema.parse_config_key(key)
|
|
154
|
+
section, sel = parsed_key[0], parsed_key[1:]
|
|
155
|
+
attr_name = self._get_attr_name_by_key(section, sel)
|
|
156
|
+
if attr_name is None:
|
|
157
|
+
raise errors.InvalidConfigKeyError(key)
|
|
158
|
+
return getattr(self, attr_name)
|
|
159
|
+
|
|
160
|
+
def set_by_key(self, key: str | Sequence[str], value: object) -> None:
|
|
161
|
+
# We don't have to check for global-only keys here because this
|
|
162
|
+
# method is only used for programmatic changes to the in-memory
|
|
163
|
+
# config, not for loading from config files.
|
|
141
164
|
parsed_key = schema.parse_config_key(key)
|
|
142
165
|
section, sel = parsed_key[0], parsed_key[1:]
|
|
166
|
+
attr_name = self._get_attr_name_by_key(section, sel)
|
|
167
|
+
if attr_name is None:
|
|
168
|
+
raise errors.InvalidConfigKeyError(key)
|
|
169
|
+
schema.ensure_valid_config_kv(key, True, value)
|
|
170
|
+
setattr(self, attr_name, value)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _get_attr_name_by_key(cls, section: str, sel: list[str]) -> str | None:
|
|
143
174
|
if section == schema.SECTION_INSTALLATION:
|
|
144
|
-
return
|
|
175
|
+
return cls._get_section_installation(sel)
|
|
145
176
|
elif section == schema.SECTION_PACKAGES:
|
|
146
|
-
return
|
|
177
|
+
return cls._get_section_packages(sel)
|
|
147
178
|
elif section == schema.SECTION_REPO:
|
|
148
|
-
return
|
|
179
|
+
return cls._get_section_repo(sel)
|
|
149
180
|
elif section == schema.SECTION_TELEMETRY:
|
|
150
|
-
return
|
|
181
|
+
return cls._get_section_telemetry(sel)
|
|
151
182
|
else:
|
|
152
183
|
return None
|
|
153
184
|
|
|
154
|
-
|
|
185
|
+
@classmethod
|
|
186
|
+
def _get_section_installation(cls, selector: list[str]) -> str | None:
|
|
155
187
|
if len(selector) != 1:
|
|
156
188
|
return None
|
|
157
189
|
leaf = selector[0]
|
|
158
190
|
if leaf == schema.KEY_INSTALLATION_EXTERNALLY_MANAGED:
|
|
159
|
-
return
|
|
191
|
+
return "is_installation_externally_managed"
|
|
160
192
|
else:
|
|
161
193
|
return None
|
|
162
194
|
|
|
163
|
-
|
|
195
|
+
@classmethod
|
|
196
|
+
def _get_section_packages(cls, selector: list[str]) -> str | None:
|
|
164
197
|
if len(selector) != 1:
|
|
165
198
|
return None
|
|
166
199
|
leaf = selector[0]
|
|
167
200
|
if leaf == schema.KEY_PACKAGES_PRERELEASES:
|
|
168
|
-
return
|
|
201
|
+
return "include_prereleases"
|
|
169
202
|
else:
|
|
170
203
|
return None
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
@classmethod
|
|
206
|
+
def _get_section_repo(cls, selector: list[str]) -> str | None:
|
|
173
207
|
if len(selector) != 1:
|
|
174
208
|
return None
|
|
175
209
|
leaf = selector[0]
|
|
176
210
|
if leaf == schema.KEY_REPO_BRANCH:
|
|
177
|
-
return
|
|
211
|
+
return "override_repo_branch"
|
|
178
212
|
elif leaf == schema.KEY_REPO_LOCAL:
|
|
179
|
-
return
|
|
213
|
+
return "override_repo_dir"
|
|
180
214
|
elif leaf == schema.KEY_REPO_REMOTE:
|
|
181
|
-
return
|
|
215
|
+
return "override_repo_url"
|
|
182
216
|
else:
|
|
183
217
|
return None
|
|
184
218
|
|
|
185
|
-
|
|
219
|
+
@classmethod
|
|
220
|
+
def _get_section_telemetry(cls, selector: list[str]) -> str | None:
|
|
186
221
|
if len(selector) != 1:
|
|
187
222
|
return None
|
|
188
223
|
leaf = selector[0]
|
|
189
224
|
if leaf == schema.KEY_TELEMETRY_MODE:
|
|
190
|
-
return
|
|
225
|
+
return "telemetry_mode"
|
|
191
226
|
elif leaf == schema.KEY_TELEMETRY_PM_TELEMETRY_URL:
|
|
192
|
-
return
|
|
227
|
+
return "override_pm_telemetry_url"
|
|
193
228
|
elif leaf == schema.KEY_TELEMETRY_UPLOAD_CONSENT:
|
|
194
|
-
return
|
|
229
|
+
return "telemetry_upload_consent_time"
|
|
195
230
|
else:
|
|
196
231
|
return None
|
|
197
232
|
|
|
@@ -262,26 +297,63 @@ class GlobalConfig:
|
|
|
262
297
|
def telemetry_root(self) -> os.PathLike[Any]:
|
|
263
298
|
return pathlib.Path(self.ensure_state_dir()) / "telemetry"
|
|
264
299
|
|
|
265
|
-
@
|
|
300
|
+
@property
|
|
266
301
|
def telemetry(self) -> "TelemetryProvider | None":
|
|
302
|
+
return None if self.telemetry_mode == "off" else self._telemetry_provider
|
|
303
|
+
|
|
304
|
+
@cached_property
|
|
305
|
+
def _telemetry_provider(self) -> "TelemetryProvider | None":
|
|
306
|
+
"""Do not access directly; use the ``telemetry`` property instead."""
|
|
307
|
+
|
|
267
308
|
from ..telemetry.provider import TelemetryProvider
|
|
268
309
|
|
|
269
310
|
return None if self.telemetry_mode == "off" else TelemetryProvider(self)
|
|
270
311
|
|
|
271
312
|
@property
|
|
272
313
|
def telemetry_mode(self) -> str:
|
|
273
|
-
return self._telemetry_mode or
|
|
314
|
+
return self._telemetry_mode or DEFAULT_TELEMETRY_MODE
|
|
315
|
+
|
|
316
|
+
@telemetry_mode.setter
|
|
317
|
+
def telemetry_mode(self, mode: str) -> None:
|
|
318
|
+
if mode not in ("off", "local", "on"):
|
|
319
|
+
raise ValueError("telemetry mode must be one of: off, local, on")
|
|
320
|
+
if self._gm.is_telemetry_optout and mode != "off":
|
|
321
|
+
raise ValueError(
|
|
322
|
+
"cannot enable telemetry when the environment variable opt-out is set"
|
|
323
|
+
)
|
|
324
|
+
self._telemetry_mode = mode
|
|
274
325
|
|
|
275
326
|
@property
|
|
276
327
|
def telemetry_upload_consent_time(self) -> datetime.datetime | None:
|
|
277
328
|
return self._telemetry_upload_consent
|
|
278
329
|
|
|
330
|
+
@telemetry_upload_consent_time.setter
|
|
331
|
+
def telemetry_upload_consent_time(self, t: datetime.datetime | None) -> None:
|
|
332
|
+
self._telemetry_upload_consent = t
|
|
333
|
+
|
|
279
334
|
@property
|
|
280
335
|
def override_pm_telemetry_url(self) -> str | None:
|
|
281
336
|
return self._telemetry_pm_telemetry_url
|
|
282
337
|
|
|
338
|
+
@override_pm_telemetry_url.setter
|
|
339
|
+
def override_pm_telemetry_url(self, url: str | None) -> None:
|
|
340
|
+
self._telemetry_pm_telemetry_url = url
|
|
341
|
+
|
|
342
|
+
@cached_property
|
|
343
|
+
def default_repo_dir(self) -> str:
|
|
344
|
+
return os.path.join(self.cache_root, "packages-index")
|
|
345
|
+
|
|
283
346
|
def get_repo_dir(self) -> str:
|
|
284
|
-
return self.override_repo_dir or
|
|
347
|
+
return self.override_repo_dir or self.default_repo_dir
|
|
348
|
+
|
|
349
|
+
@cached_property
|
|
350
|
+
def have_overridden_repo_dir(self) -> bool:
|
|
351
|
+
if not self.override_repo_dir:
|
|
352
|
+
return False
|
|
353
|
+
override_path = pathlib.Path(self.override_repo_dir)
|
|
354
|
+
default_path = pathlib.Path(self.default_repo_dir)
|
|
355
|
+
# we don't use samefile() here because the path may not exist
|
|
356
|
+
return override_path.resolve() != default_path.resolve()
|
|
285
357
|
|
|
286
358
|
def get_repo_url(self) -> str:
|
|
287
359
|
return self.override_repo_url or DEFAULT_REPO_URL
|
|
@@ -312,7 +384,7 @@ class GlobalConfig:
|
|
|
312
384
|
def lookup_binary_install_dir(self, host: str, slug: str) -> PathLike[Any] | None:
|
|
313
385
|
host_path = get_host_path_fragment_for_binary_install_dir(host)
|
|
314
386
|
for data_dir in self._dirs.app_data_dirs:
|
|
315
|
-
p = data_dir / "binaries" / host_path / slug
|
|
387
|
+
p = data_dir.path / "binaries" / host_path / slug
|
|
316
388
|
if p.exists():
|
|
317
389
|
return p
|
|
318
390
|
return None
|
|
@@ -347,30 +419,40 @@ class GlobalConfig:
|
|
|
347
419
|
p.mkdir(parents=True, exist_ok=True)
|
|
348
420
|
return p
|
|
349
421
|
|
|
350
|
-
def iter_preset_configs(self) -> Iterable[
|
|
422
|
+
def iter_preset_configs(self) -> "Iterable[XDGPathEntry]":
|
|
351
423
|
"""
|
|
352
424
|
Yields possible Ruyi config files in all preset config path locations,
|
|
353
425
|
sorted by precedence from lowest to highest (so that each file may be
|
|
354
426
|
simply applied consecutively).
|
|
355
427
|
"""
|
|
356
428
|
|
|
429
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
430
|
+
|
|
357
431
|
for path in PRESET_GLOBAL_CONFIG_LOCATIONS:
|
|
358
|
-
yield pathlib.Path(path)
|
|
432
|
+
yield XDGPathEntry(pathlib.Path(path), True)
|
|
359
433
|
|
|
360
|
-
def iter_xdg_configs(self) -> Iterable[
|
|
434
|
+
def iter_xdg_configs(self) -> "Iterable[XDGPathEntry]":
|
|
361
435
|
"""
|
|
362
436
|
Yields possible Ruyi config files in all XDG config paths, sorted by precedence
|
|
363
437
|
from lowest to highest (so that each file may be simply applied consecutively).
|
|
364
438
|
"""
|
|
365
439
|
|
|
366
|
-
|
|
367
|
-
|
|
440
|
+
from ..utils.xdg_basedir import XDGPathEntry
|
|
441
|
+
|
|
442
|
+
entries = list(self._dirs.app_config_dirs)
|
|
443
|
+
for e in reversed(entries):
|
|
444
|
+
yield XDGPathEntry(e.path / "config.toml", e.is_global)
|
|
368
445
|
|
|
369
446
|
@property
|
|
370
447
|
def local_user_config_file(self) -> pathlib.Path:
|
|
371
448
|
return self._dirs.app_config / "config.toml"
|
|
372
449
|
|
|
373
|
-
def
|
|
450
|
+
def _try_apply_config_file(
|
|
451
|
+
self,
|
|
452
|
+
path: os.PathLike[Any],
|
|
453
|
+
*,
|
|
454
|
+
is_global_scope: bool,
|
|
455
|
+
) -> None:
|
|
374
456
|
import tomlkit
|
|
375
457
|
|
|
376
458
|
try:
|
|
@@ -379,23 +461,24 @@ class GlobalConfig:
|
|
|
379
461
|
except FileNotFoundError:
|
|
380
462
|
return
|
|
381
463
|
|
|
382
|
-
self.logger.D(f"applying config: {data}")
|
|
383
|
-
self.
|
|
464
|
+
self.logger.D(f"applying config: {data}, is_global_scope={is_global_scope}")
|
|
465
|
+
self._apply_config(data, is_global_scope=is_global_scope)
|
|
384
466
|
|
|
385
467
|
@classmethod
|
|
386
468
|
def load_from_config(cls, gm: "ProvidesGlobalMode", logger: "RuyiLogger") -> "Self":
|
|
387
469
|
obj = cls(gm, logger)
|
|
388
470
|
|
|
389
|
-
for config_path in obj.iter_preset_configs():
|
|
471
|
+
for config_path, is_global in obj.iter_preset_configs():
|
|
390
472
|
obj.logger.D(f"trying config file from preset location: {config_path}")
|
|
391
|
-
obj.
|
|
473
|
+
obj._try_apply_config_file(config_path, is_global_scope=is_global)
|
|
392
474
|
|
|
393
|
-
for config_path in obj.iter_xdg_configs():
|
|
475
|
+
for config_path, is_global in obj.iter_xdg_configs():
|
|
394
476
|
obj.logger.D(f"trying config file from XDG path: {config_path}")
|
|
395
|
-
obj.
|
|
477
|
+
obj._try_apply_config_file(config_path, is_global_scope=is_global)
|
|
396
478
|
|
|
397
479
|
# let environment variable take precedence
|
|
398
480
|
if gm.is_telemetry_optout:
|
|
399
481
|
obj._telemetry_mode = "off"
|
|
482
|
+
obj._telemetry_upload_consent = None
|
|
400
483
|
|
|
401
484
|
return obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from contextlib import AbstractContextManager
|
|
2
2
|
import pathlib
|
|
3
|
-
from typing import Sequence, TYPE_CHECKING, cast
|
|
3
|
+
from typing import Final, Sequence, TYPE_CHECKING, cast
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from types import TracebackType
|
|
@@ -9,13 +9,35 @@ if TYPE_CHECKING:
|
|
|
9
9
|
import tomlkit
|
|
10
10
|
from tomlkit.items import Table
|
|
11
11
|
|
|
12
|
-
from .errors import MalformedConfigFileError
|
|
13
|
-
from .schema import
|
|
12
|
+
from .errors import MalformedConfigFileError, ProtectedGlobalConfigError
|
|
13
|
+
from .schema import (
|
|
14
|
+
SECTION_INSTALLATION,
|
|
15
|
+
KEY_INSTALLATION_EXTERNALLY_MANAGED,
|
|
16
|
+
ensure_valid_config_kv,
|
|
17
|
+
parse_config_key,
|
|
18
|
+
validate_section,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
if TYPE_CHECKING:
|
|
16
22
|
from . import GlobalConfig
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
GLOBAL_ONLY_CONFIG_KEYS: Final[set[tuple[str, str]]] = {
|
|
26
|
+
(SECTION_INSTALLATION, KEY_INSTALLATION_EXTERNALLY_MANAGED),
|
|
27
|
+
}
|
|
28
|
+
"""Settings that can only be set in global-scope config files.
|
|
29
|
+
|
|
30
|
+
Changes should be reflected in ``GlobalConfig._apply_config`` too."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_config_key_global_only(key: str | Sequence[str]) -> bool:
|
|
34
|
+
parsed_key = parse_config_key(key)
|
|
35
|
+
if len(parsed_key) != 2:
|
|
36
|
+
return False
|
|
37
|
+
section, leaf = parsed_key
|
|
38
|
+
return (section, leaf) in GLOBAL_ONLY_CONFIG_KEYS
|
|
39
|
+
|
|
40
|
+
|
|
19
41
|
class ConfigEditor(AbstractContextManager["ConfigEditor"]):
|
|
20
42
|
def __init__(self, path: pathlib.Path) -> None:
|
|
21
43
|
self._path = path
|
|
@@ -63,6 +85,15 @@ class ConfigEditor(AbstractContextManager["ConfigEditor"]):
|
|
|
63
85
|
parsed_key = parse_config_key(key)
|
|
64
86
|
ensure_valid_config_kv(parsed_key, check_val=True, val=val)
|
|
65
87
|
|
|
88
|
+
# Gate protected settings: user-local config is not allowed to override
|
|
89
|
+
# global-only settings.
|
|
90
|
+
if _is_config_key_global_only(parsed_key):
|
|
91
|
+
# This mechanism is only meant for modifying user-local configs,
|
|
92
|
+
# because global configs are assumed to be maintained by packagers
|
|
93
|
+
# and/or sysadmins, who are expected to write the config file by
|
|
94
|
+
# hand.
|
|
95
|
+
raise ProtectedGlobalConfigError(parsed_key)
|
|
96
|
+
|
|
66
97
|
section, sel = parsed_key[0], parsed_key[1:]
|
|
67
98
|
if section in self._stage:
|
|
68
99
|
existing_section = self._stage[section]
|