ruyi 0.42.0b20251014__tar.gz → 0.42.0b20251017__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 (102) hide show
  1. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/PKG-INFO +2 -2
  2. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/pyproject.toml +2 -2
  3. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/main.py +6 -1
  4. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/oobe.py +7 -1
  5. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/self_cli.py +7 -2
  6. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/config/__init__.py +1 -7
  7. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/config/errors.py +10 -6
  8. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/config/schema.py +67 -17
  9. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/api.py +1 -1
  10. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/state.py +10 -0
  11. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/store.py +1 -1
  12. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/global_mode.py +19 -1
  13. ruyi-0.42.0b20251017/ruyi/version.py +20 -0
  14. ruyi-0.42.0b20251014/ruyi/version.py +0 -67
  15. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/LICENSE-Apache.txt +0 -0
  16. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/README.md +0 -0
  17. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/__init__.py +0 -0
  18. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/__main__.py +0 -0
  19. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/__init__.py +0 -0
  20. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/builtin_commands.py +0 -0
  21. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/cmd.py +0 -0
  22. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/completer.py +0 -0
  23. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/completion.py +0 -0
  24. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/config_cli.py +0 -0
  25. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/user_input.py +0 -0
  26. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/cli/version_cli.py +0 -0
  27. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/config/editor.py +0 -0
  28. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/config/news.py +0 -0
  29. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/device/__init__.py +0 -0
  30. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/device/provision.py +0 -0
  31. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/device/provision_cli.py +0 -0
  32. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/log/__init__.py +0 -0
  33. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/__init__.py +0 -0
  34. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/runtime.py +0 -0
  35. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/venv/__init__.py +0 -0
  36. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/venv/emulator_cfg.py +0 -0
  37. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/venv/maker.py +0 -0
  38. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/venv/venv_cli.py +0 -0
  39. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/mux/venv_cfg.py +0 -0
  40. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/__init__.py +0 -0
  41. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/ctx.py +0 -0
  42. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/paths.py +0 -0
  43. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/plugin_cli.py +0 -0
  44. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/pluginhost/unsandboxed.py +0 -0
  45. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/py.typed +0 -0
  46. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/resource_bundle/__init__.py +0 -0
  47. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/resource_bundle/__main__.py +0 -0
  48. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/resource_bundle/data.py +0 -0
  49. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/__init__.py +0 -0
  50. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/admin_checksum.py +0 -0
  51. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/admin_cli.py +0 -0
  52. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/atom.py +0 -0
  53. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/augmented_pkg.py +0 -0
  54. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/canonical_dump.py +0 -0
  55. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/checksum.py +0 -0
  56. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/cli_completion.py +0 -0
  57. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/distfile.py +0 -0
  58. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/entity.py +0 -0
  59. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/entity_cli.py +0 -0
  60. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/entity_provider.py +0 -0
  61. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/fetcher.py +0 -0
  62. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/host.py +0 -0
  63. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/install.py +0 -0
  64. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/install_cli.py +0 -0
  65. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/list.py +0 -0
  66. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/list_cli.py +0 -0
  67. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/list_filter.py +0 -0
  68. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/msg.py +0 -0
  69. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/news.py +0 -0
  70. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/news_cli.py +0 -0
  71. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/news_store.py +0 -0
  72. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/pkg_manifest.py +0 -0
  73. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/profile.py +0 -0
  74. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/profile_cli.py +0 -0
  75. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/protocols.py +0 -0
  76. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/repo.py +0 -0
  77. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/unpack.py +0 -0
  78. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/unpack_method.py +0 -0
  79. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/ruyipkg/update_cli.py +0 -0
  80. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/__init__.py +0 -0
  81. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/aggregate.py +0 -0
  82. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/event.py +0 -0
  83. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/node_info.py +0 -0
  84. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/provider.py +0 -0
  85. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/scope.py +0 -0
  86. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/telemetry/telemetry_cli.py +0 -0
  87. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/__init__.py +0 -0
  88. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/ar.py +0 -0
  89. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/ci.py +0 -0
  90. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/frontmatter.py +0 -0
  91. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/git.py +0 -0
  92. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/l10n.py +0 -0
  93. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/markdown.py +0 -0
  94. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/mounts.py +0 -0
  95. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/nuitka.py +0 -0
  96. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/porcelain.py +0 -0
  97. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/prereqs.py +0 -0
  98. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/ssl_patch.py +0 -0
  99. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/templating.py +0 -0
  100. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/toml.py +0 -0
  101. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/url.py +0 -0
  102. {ruyi-0.42.0b20251014 → ruyi-0.42.0b20251017}/ruyi/utils/xdg_basedir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruyi
3
- Version: 0.42.0b20251014
3
+ Version: 0.42.0b20251017
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,<4.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)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "ruyi"
7
- version = "0.42.0-beta.20251014"
7
+ version = "0.42.0-beta.20251017"
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]
@@ -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
 
@@ -22,7 +23,11 @@ def is_called_as_ruyi(argv0: str) -> bool:
22
23
 
23
24
 
24
25
  def should_prompt_for_renaming(argv0: str) -> bool:
25
- return os.path.basename(argv0).lower().startswith(f"{RUYI_ENTRYPOINT_NAME}-")
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)
26
31
 
27
32
 
28
33
  def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
@@ -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() and sys.stdin.isatty()
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:
@@ -166,13 +166,7 @@ class GlobalConfig:
166
166
  attr_name = self._get_attr_name_by_key(section, sel)
167
167
  if attr_name is None:
168
168
  raise errors.InvalidConfigKeyError(key)
169
-
170
- expected_type = schema.get_expected_type_for_config_key(key)
171
- if not isinstance(value, expected_type):
172
- raise TypeError(
173
- f"expected type {expected_type.__name__} for config key '{key}', got {type(value).__name__}"
174
- )
175
-
169
+ schema.ensure_valid_config_kv(key, True, value)
176
170
  setattr(self, attr_name, value)
177
171
 
178
172
  @classmethod
@@ -31,7 +31,7 @@ class InvalidConfigValueTypeError(TypeError):
31
31
  self,
32
32
  key: str | Sequence[str],
33
33
  val: object | None,
34
- expected: type,
34
+ expected: type | Sequence[type],
35
35
  ) -> None:
36
36
  super().__init__()
37
37
  self._key = key
@@ -48,20 +48,24 @@ class InvalidConfigValueTypeError(TypeError):
48
48
  class InvalidConfigValueError(ValueError):
49
49
  def __init__(
50
50
  self,
51
- key: str | Sequence[str] | type,
51
+ key: str | Sequence[str] | None,
52
52
  val: object | None,
53
+ typ: type | Sequence[type],
53
54
  ) -> None:
54
55
  super().__init__()
55
56
  self._key = key
56
57
  self._val = val
58
+ self._typ = typ
57
59
 
58
60
  def __str__(self) -> str:
59
- if isinstance(self._key, type):
60
- return f"invalid config value for type {self._key}: {self._val}"
61
- return f"invalid config value for key {self._key}: {self._val}"
61
+ return (
62
+ f"invalid config value for key {self._key} (type {self._typ}): {self._val}"
63
+ )
62
64
 
63
65
  def __repr__(self) -> str:
64
- return f"InvalidConfigValueError({self._key:!r}, {self._val:!r})"
66
+ return (
67
+ f"InvalidConfigValueError({self._key:!r}, {self._val:!r}, {self._typ:!r})"
68
+ )
65
69
 
66
70
 
67
71
  class MalformedConfigFileError(Exception):
@@ -1,6 +1,6 @@
1
1
  import datetime
2
2
  import sys
3
- from typing import Final, Sequence
3
+ from typing import Final, Sequence, TypeGuard
4
4
 
5
5
  from .errors import (
6
6
  InvalidConfigKeyError,
@@ -43,7 +43,7 @@ def validate_section(section: str) -> None:
43
43
  raise InvalidConfigSectionError(section)
44
44
 
45
45
 
46
- def get_expected_type_for_config_key(key: str | Sequence[str]) -> type:
46
+ def get_expected_type_for_config_key(key: str | Sequence[str]) -> type | Sequence[type]:
47
47
  parsed_key = parse_config_key(key)
48
48
  if len(parsed_key) != 2:
49
49
  # for now there's no nested config option
@@ -87,17 +87,29 @@ def _get_expected_type_for_section_repo(sel: str) -> type:
87
87
  raise InvalidConfigKeyError(sel)
88
88
 
89
89
 
90
- def _get_expected_type_for_section_telemetry(sel: str) -> type:
90
+ def _get_expected_type_for_section_telemetry(sel: str) -> type | tuple[type, ...]:
91
91
  if sel == KEY_TELEMETRY_MODE:
92
92
  return str
93
93
  elif sel == KEY_TELEMETRY_PM_TELEMETRY_URL:
94
94
  return str
95
95
  elif sel == KEY_TELEMETRY_UPLOAD_CONSENT:
96
- return datetime.datetime
96
+ return (type(None), datetime.datetime)
97
97
  else:
98
98
  raise InvalidConfigKeyError(sel)
99
99
 
100
100
 
101
+ def _is_all_str(obj: object) -> TypeGuard[Sequence[str]]:
102
+ if not isinstance(obj, Sequence):
103
+ return False
104
+ return all(isinstance(i, str) for i in obj)
105
+
106
+
107
+ def _is_all_type(obj: object) -> TypeGuard[Sequence[type]]:
108
+ if not isinstance(obj, Sequence):
109
+ return False
110
+ return all(isinstance(i, type) for i in obj)
111
+
112
+
101
113
  def ensure_valid_config_kv(
102
114
  key: str | Sequence[str],
103
115
  check_val: bool = False,
@@ -108,9 +120,9 @@ def ensure_valid_config_kv(
108
120
  # for now there's no nested config option
109
121
  raise InvalidConfigKeyError(key)
110
122
 
111
- expected_type = get_expected_type_for_config_key(parsed_key)
123
+ expected_types = get_expected_type_for_config_key(parsed_key)
112
124
  # validity of config key is already checked by get_expected_type_for_config_key
113
- _ensure_value_type(key, check_val, val, expected_type)
125
+ ensure_value_type(key, check_val, val, expected_types)
114
126
 
115
127
  if not check_val:
116
128
  return
@@ -120,14 +132,26 @@ def ensure_valid_config_kv(
120
132
  return _extra_validate_section_telemetry_kv(key, sel, val)
121
133
 
122
134
 
123
- def _ensure_value_type(
135
+ def ensure_value_type(
124
136
  key: str | Sequence[str],
125
137
  check_val: bool,
126
138
  val: object | None,
127
- expected: type,
139
+ expected: type | Sequence[type],
128
140
  ) -> None:
129
- if check_val and not isinstance(val, expected):
130
- raise InvalidConfigValueTypeError(key, val, expected)
141
+ if not check_val:
142
+ return
143
+
144
+ expected_types: tuple[type, ...]
145
+ if isinstance(expected, type):
146
+ expected_types = (expected,)
147
+ else:
148
+ expected_types = tuple(expected)
149
+
150
+ for ty in expected_types:
151
+ if isinstance(val, ty):
152
+ return
153
+
154
+ raise InvalidConfigValueTypeError(key, val, expected)
131
155
 
132
156
 
133
157
  def _extra_validate_section_telemetry_kv(
@@ -138,7 +162,7 @@ def _extra_validate_section_telemetry_kv(
138
162
  if sel == KEY_TELEMETRY_MODE:
139
163
  # value type is already ensured earlier
140
164
  if val not in ("local", "off", "on"):
141
- raise InvalidConfigValueError(key, val)
165
+ raise InvalidConfigValueError(key, val, str)
142
166
 
143
167
 
144
168
  def encode_value(v: object) -> str:
@@ -169,24 +193,50 @@ def encode_value(v: object) -> str:
169
193
 
170
194
 
171
195
  def decode_value(
172
- key: str | Sequence[str] | type,
196
+ key: str | Sequence[str],
173
197
  val: str,
174
198
  ) -> object:
175
199
  """Decodes the given string representation of a config value into a Python
176
200
  value, directed by type information implied by the config key."""
177
201
 
178
202
  if isinstance(key, type):
179
- expected_type = key
180
- else:
181
- expected_type = get_expected_type_for_config_key(key)
203
+ return _decode_single_type_value(None, val, key)
204
+ elif _is_all_type(key):
205
+ return _decode_typed_value(None, val, key)
206
+
207
+ assert isinstance(key, str) or _is_all_str(key)
208
+ expected_types = get_expected_type_for_config_key(key)
209
+
210
+ if isinstance(expected_types, type):
211
+ return _decode_single_type_value(key, val, expected_types)
212
+ return _decode_typed_value(key, val, expected_types)
213
+
214
+
215
+ def _decode_typed_value(
216
+ key: str | Sequence[str] | None,
217
+ val: str,
218
+ expected_types: Sequence[type],
219
+ ) -> object:
220
+ for ty in expected_types:
221
+ try:
222
+ return _decode_single_type_value(key, val, ty)
223
+ except (RuntimeError, TypeError, ValueError):
224
+ continue
225
+ raise InvalidConfigValueError(key, val, expected_types)
226
+
182
227
 
228
+ def _decode_single_type_value(
229
+ key: str | Sequence[str] | None,
230
+ val: str,
231
+ expected_type: type,
232
+ ) -> object:
183
233
  if expected_type is bool:
184
234
  if val in ("true", "yes", "1"):
185
235
  return True
186
236
  elif val in ("false", "no", "0"):
187
237
  return False
188
238
  else:
189
- raise InvalidConfigValueError(key, val)
239
+ raise InvalidConfigValueError(key, val, expected_type)
190
240
  elif expected_type is int:
191
241
  return int(val, 10)
192
242
  elif expected_type is str:
@@ -199,4 +249,4 @@ def decode_value(
199
249
  v = datetime.datetime.fromisoformat(val)
200
250
  return v.astimezone() if v.tzinfo is None else v
201
251
  else:
202
- raise NotImplementedError(f"invalid type for config value: {expected_type}")
252
+ raise NotImplementedError(f"unhandled type for config value: {expected_type}")
@@ -44,7 +44,7 @@ class RuyiHostAPI:
44
44
 
45
45
  @property
46
46
  def ruyi_version(self) -> str:
47
- return str(RUYI_SEMVER)
47
+ return RUYI_SEMVER
48
48
 
49
49
  @property
50
50
  def ruyi_plugin_api_rev(self) -> int:
@@ -76,6 +76,16 @@ class RuyipkgGlobalStateStore:
76
76
  """Ensure the state directory exists."""
77
77
  self.root.mkdir(parents=True, exist_ok=True)
78
78
 
79
+ def purge_installation_info(self) -> None:
80
+ """Purge installation records."""
81
+ self._installs_file.unlink(missing_ok=True)
82
+ self._installs_cache = None
83
+ # if the state dir is empty, remove it
84
+ try:
85
+ self.root.rmdir()
86
+ except OSError:
87
+ pass
88
+
79
89
  def _load_installs(self) -> dict[str, PackageInstallationInfo]:
80
90
  """Load installation records from disk."""
81
91
  if self._installs_cache is not None:
@@ -165,7 +165,7 @@ class TelemetryStore:
165
165
  payload: UploadPayload = {
166
166
  "fmt": 1,
167
167
  "nonce": payload_nonce,
168
- "ruyi_version": str(RUYI_SEMVER),
168
+ "ruyi_version": RUYI_SEMVER,
169
169
  "events": aggregate_data,
170
170
  }
171
171
  if installation_data is not None:
@@ -128,6 +128,24 @@ def _guess_porcelain_from_argv(argv: list[str]) -> bool:
128
128
  return len(argv) > 1 and argv[1] == "--porcelain"
129
129
 
130
130
 
131
+ def _probe_cli_autocomplete(env: Mapping[str, str], argv: list[str]) -> bool:
132
+ """
133
+ Probe if the current invocation is related to shell completion based on
134
+ the arguments passed, without requiring the ``argparse`` machinery to be
135
+ completely initialized, and the environment.
136
+ """
137
+
138
+ # If `--output-completion-script` is present anywhere in the arguments,
139
+ # then this is related to shell completion even if _ARGCOMPLETE is not yet
140
+ # set (which is only set for invocations after the shell finished sourcing
141
+ # the completion script).
142
+ for arg in argv:
143
+ if arg.startswith("--output-completion-script"):
144
+ return True
145
+
146
+ return "_ARGCOMPLETE" in os.environ
147
+
148
+
131
149
  class EnvGlobalModeProvider(GlobalModeProvider):
132
150
  def __init__(
133
151
  self,
@@ -150,7 +168,7 @@ class EnvGlobalModeProvider(GlobalModeProvider):
150
168
 
151
169
  # We have to lift this piece of implementation detail out of argcomplete,
152
170
  # as the argcomplete import is very costly in terms of startup time.
153
- self._is_cli_autocomplete = "_ARGCOMPLETE" in os.environ
171
+ self._is_cli_autocomplete = _probe_cli_autocomplete(env, argv)
154
172
 
155
173
  self._venv_root = env.get(ENV_VENV_ROOT_KEY)
156
174
 
@@ -0,0 +1,20 @@
1
+ from typing import Final
2
+
3
+ RUYI_SEMVER: Final = "0.42.0-beta.20251017"
4
+ RUYI_USER_AGENT: Final = f"ruyi/{RUYI_SEMVER}"
5
+
6
+ COPYRIGHT_NOTICE: Final = """\
7
+ Copyright (C) Institute of Software, Chinese Academy of Sciences (ISCAS).
8
+ All rights reserved.
9
+ License: Apache-2.0 <https://www.apache.org/licenses/LICENSE-2.0>
10
+ \
11
+ """
12
+
13
+ MPL_REDIST_NOTICE: Final = """\
14
+ This distribution of ruyi contains code licensed under the Mozilla Public
15
+ License 2.0 (https://mozilla.org/MPL/2.0/). You can get the respective
16
+ project's sources from the project's official website:
17
+
18
+ * certifi: https://github.com/certifi/python-certifi
19
+ \
20
+ """
@@ -1,67 +0,0 @@
1
- import importlib.metadata
2
- from typing import Final, TYPE_CHECKING
3
-
4
- import packaging.version
5
-
6
- if TYPE_CHECKING:
7
- # pyright only works with semver 3.x
8
- from semver.version import Version
9
- else:
10
- try:
11
- from semver.version import Version # type: ignore[import-untyped,unused-ignore]
12
- except ModuleNotFoundError:
13
- # semver 2.x
14
- from semver import VersionInfo as Version # type: ignore[import-untyped,unused-ignore]
15
-
16
- # NOTE: one cannot print logs in the version helpers, because the version info
17
- # is initialized so early (before argparse can look at argv because --version
18
- # requires version info to be ready) that the porcelain status is not yet
19
- # available.
20
-
21
- _PYPI_PRERELEASE_KINDS_MAP: Final = {
22
- "a": "alpha",
23
- "b": "beta",
24
- "rc": "rc",
25
- }
26
-
27
-
28
- # based on https://python-semver.readthedocs.io/en/3.0.2/advanced/convert-pypi-to-semver.html
29
- def _convert2semver(ver: packaging.version.Version) -> Version:
30
- if ver.epoch:
31
- raise ValueError("Can't convert an epoch to semver")
32
- if ver.post:
33
- raise ValueError("Can't convert a post part to semver")
34
-
35
- pre: str | None = None
36
- if ver.pre:
37
- kind, val = ver.pre
38
- pre = f"{_PYPI_PRERELEASE_KINDS_MAP.get(kind, kind)}.{val}"
39
-
40
- maj, min, pat = ver.release[:3]
41
- return Version(maj, min, pat, prerelease=pre, build=ver.dev)
42
-
43
-
44
- def _init_pkg_semver() -> Version:
45
- pkg_pypi_ver = packaging.version.Version(importlib.metadata.version("ruyi"))
46
- # log.D(f"PyPI-style version of ruyi: {pkg_pypi_ver}")
47
- return _convert2semver(pkg_pypi_ver)
48
-
49
-
50
- RUYI_SEMVER: Final = _init_pkg_semver()
51
- RUYI_USER_AGENT: Final = f"ruyi/{RUYI_SEMVER}"
52
-
53
- COPYRIGHT_NOTICE: Final = """\
54
- Copyright (C) Institute of Software, Chinese Academy of Sciences (ISCAS).
55
- All rights reserved.
56
- License: Apache-2.0 <https://www.apache.org/licenses/LICENSE-2.0>
57
- \
58
- """
59
-
60
- MPL_REDIST_NOTICE: Final = """\
61
- This distribution of ruyi contains code licensed under the Mozilla Public
62
- License 2.0 (https://mozilla.org/MPL/2.0/). You can get the respective
63
- project's sources from the project's official website:
64
-
65
- * certifi: https://github.com/certifi/python-certifi
66
- \
67
- """
File without changes