ruyi 0.42.0a20251013__tar.gz → 0.42.0b20251015__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 (101) hide show
  1. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/PKG-INFO +2 -2
  2. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/pyproject.toml +2 -2
  3. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/oobe.py +7 -1
  4. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/self_cli.py +7 -2
  5. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/config/__init__.py +1 -7
  6. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/config/errors.py +10 -6
  7. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/config/schema.py +67 -17
  8. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/profile.py +148 -1
  9. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/repo.py +2 -1
  10. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/state.py +10 -0
  11. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/global_mode.py +19 -1
  12. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/LICENSE-Apache.txt +0 -0
  13. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/README.md +0 -0
  14. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/__init__.py +0 -0
  15. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/__main__.py +0 -0
  16. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/__init__.py +0 -0
  17. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/builtin_commands.py +0 -0
  18. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/cmd.py +0 -0
  19. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/completer.py +0 -0
  20. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/completion.py +0 -0
  21. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/config_cli.py +0 -0
  22. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/main.py +0 -0
  23. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/user_input.py +0 -0
  24. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/cli/version_cli.py +0 -0
  25. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/config/editor.py +0 -0
  26. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/config/news.py +0 -0
  27. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/device/__init__.py +0 -0
  28. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/device/provision.py +0 -0
  29. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/device/provision_cli.py +0 -0
  30. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/log/__init__.py +0 -0
  31. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/__init__.py +0 -0
  32. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/runtime.py +0 -0
  33. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/venv/__init__.py +0 -0
  34. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/venv/emulator_cfg.py +0 -0
  35. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/venv/maker.py +0 -0
  36. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/venv/venv_cli.py +0 -0
  37. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/mux/venv_cfg.py +0 -0
  38. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/__init__.py +0 -0
  39. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/api.py +0 -0
  40. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/ctx.py +0 -0
  41. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/paths.py +0 -0
  42. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/plugin_cli.py +0 -0
  43. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/pluginhost/unsandboxed.py +0 -0
  44. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/py.typed +0 -0
  45. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/resource_bundle/__init__.py +0 -0
  46. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/resource_bundle/__main__.py +0 -0
  47. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/resource_bundle/data.py +0 -0
  48. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/__init__.py +0 -0
  49. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/admin_checksum.py +0 -0
  50. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/admin_cli.py +0 -0
  51. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/atom.py +0 -0
  52. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/augmented_pkg.py +0 -0
  53. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/canonical_dump.py +0 -0
  54. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/checksum.py +0 -0
  55. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/cli_completion.py +0 -0
  56. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/distfile.py +0 -0
  57. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/entity.py +0 -0
  58. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/entity_cli.py +0 -0
  59. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/entity_provider.py +0 -0
  60. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/fetcher.py +0 -0
  61. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/host.py +0 -0
  62. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/install.py +0 -0
  63. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/install_cli.py +0 -0
  64. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/list.py +0 -0
  65. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/list_cli.py +0 -0
  66. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/list_filter.py +0 -0
  67. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/msg.py +0 -0
  68. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/news.py +0 -0
  69. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/news_cli.py +0 -0
  70. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/news_store.py +0 -0
  71. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/pkg_manifest.py +0 -0
  72. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/profile_cli.py +0 -0
  73. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/protocols.py +0 -0
  74. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/unpack.py +0 -0
  75. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/unpack_method.py +0 -0
  76. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/ruyipkg/update_cli.py +0 -0
  77. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/__init__.py +0 -0
  78. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/aggregate.py +0 -0
  79. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/event.py +0 -0
  80. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/node_info.py +0 -0
  81. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/provider.py +0 -0
  82. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/scope.py +0 -0
  83. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/store.py +0 -0
  84. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/telemetry/telemetry_cli.py +0 -0
  85. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/__init__.py +0 -0
  86. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/ar.py +0 -0
  87. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/ci.py +0 -0
  88. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/frontmatter.py +0 -0
  89. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/git.py +0 -0
  90. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/l10n.py +0 -0
  91. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/markdown.py +0 -0
  92. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/mounts.py +0 -0
  93. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/nuitka.py +0 -0
  94. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/porcelain.py +0 -0
  95. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/prereqs.py +0 -0
  96. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/ssl_patch.py +0 -0
  97. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/templating.py +0 -0
  98. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/toml.py +0 -0
  99. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/url.py +0 -0
  100. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/utils/xdg_basedir.py +0 -0
  101. {ruyi-0.42.0a20251013 → ruyi-0.42.0b20251015}/ruyi/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruyi
3
- Version: 0.42.0a20251013
3
+ Version: 0.42.0b20251015
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-alpha.20251013"
7
+ version = "0.42.0-beta.20251015"
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]
@@ -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}")
@@ -1,7 +1,21 @@
1
1
  from os import PathLike
2
- from typing import Any, Iterable, TypeGuard, cast
2
+ from typing import (
3
+ Any,
4
+ Iterable,
5
+ Mapping,
6
+ Protocol,
7
+ Sequence,
8
+ TypedDict,
9
+ TypeGuard,
10
+ TYPE_CHECKING,
11
+ cast,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from typing_extensions import NotRequired
3
16
 
4
17
  from ..pluginhost.ctx import PluginHostContext, SupportsEvalFunction
18
+ from .entity_provider import BaseEntityProvider
5
19
  from .pkg_manifest import EmulatorFlavor
6
20
 
7
21
 
@@ -206,3 +220,136 @@ class ProfileProxy:
206
220
  sysroot: PathLike[Any] | None,
207
221
  ) -> dict[str, str] | None:
208
222
  return self._provider.get_env_config_for_emu_flavor(self._id, flavor, sysroot)
223
+
224
+
225
+ #
226
+ # Protocols
227
+ #
228
+
229
+
230
+ # MetadataRepo is defined in repo.py, but we don't want to import repo.py here
231
+ # to avoid circular import. Instead, we just describe the methods and properties
232
+ # that we need from MetadataRepo with a Protocol.
233
+ class ProvidesProfiles(Protocol):
234
+ def get_supported_arches(self) -> list[str]: ...
235
+ def get_profile_for_arch(self, arch: str, name: str) -> ProfileProxy | None: ...
236
+ def iter_profiles_for_arch(self, arch: str) -> Iterable[ProfileProxy]: ...
237
+
238
+
239
+ #
240
+ # Entity type and schema for profile entities
241
+ #
242
+
243
+ PROFILE_V1_ENTITY_TYPE = "profile-v1"
244
+ PROFILE_V1_ENTITY_TYPE_SCHEMA = {
245
+ "$schema": "http://json-schema.org/draft-07/schema#",
246
+ "required": ["profile-v1"],
247
+ "properties": {
248
+ "profile-v1": {
249
+ "type": "object",
250
+ "properties": {
251
+ "id": {"type": "string"},
252
+ "display_name": {"type": "string"},
253
+ "name": {"type": "string"},
254
+ "arch": {"type": "string"},
255
+ "needed_toolchain_quirks": {
256
+ "type": "array",
257
+ "items": {"type": "string"},
258
+ },
259
+ "toolchain_common_flags_str": {"type": "string"},
260
+ },
261
+ "required": [
262
+ "id",
263
+ "display_name",
264
+ "name",
265
+ "arch",
266
+ "needed_toolchain_quirks",
267
+ "toolchain_common_flags_str",
268
+ ],
269
+ },
270
+ "related": {
271
+ "type": "array",
272
+ "description": "List of related entity references",
273
+ "items": {"type": "string", "pattern": "^.+:.+"},
274
+ },
275
+ "unique_among_type_during_traversal": {
276
+ "type": "boolean",
277
+ "description": "Whether this entity should be unique among all entities of the same type during traversal",
278
+ },
279
+ },
280
+ }
281
+
282
+
283
+ class ProfileV1EntityData(TypedDict):
284
+ id: str
285
+ display_name: str
286
+ name: str
287
+ arch: str
288
+ needed_toolchain_quirks: list[str]
289
+ toolchain_common_flags_str: str
290
+
291
+
292
+ ProfileV1Entity = TypedDict(
293
+ "ProfileV1Entity",
294
+ {
295
+ "profile-v1": ProfileV1EntityData,
296
+ "related": "NotRequired[list[str]]",
297
+ "unique_among_type_during_traversal": "NotRequired[bool]",
298
+ },
299
+ total=False,
300
+ )
301
+
302
+
303
+ class ProfileEntityProvider(BaseEntityProvider):
304
+ def __init__(self, provider: ProvidesProfiles) -> None:
305
+ super().__init__()
306
+ self._provider = provider
307
+
308
+ def discover_schemas(self) -> dict[str, object]:
309
+ return {
310
+ PROFILE_V1_ENTITY_TYPE: PROFILE_V1_ENTITY_TYPE_SCHEMA,
311
+ }
312
+
313
+ def load_entities(
314
+ self,
315
+ entity_types: Sequence[str],
316
+ ) -> Mapping[str, Mapping[str, Mapping[str, Any]]]:
317
+ result: dict[str, Mapping[str, Mapping[str, Any]]] = {}
318
+ for ty in entity_types:
319
+ if ty == PROFILE_V1_ENTITY_TYPE:
320
+ result[ty] = _load_profile_v1_entities(self._provider)
321
+ return result
322
+
323
+
324
+ def _load_profile_v1_entities(provider: ProvidesProfiles) -> dict[str, ProfileV1Entity]:
325
+ result: dict[str, ProfileV1Entity] = {}
326
+ for arch in provider.get_supported_arches():
327
+ result.update(_load_profile_v1_entities_for_arch(provider, arch))
328
+ return result
329
+
330
+
331
+ def _load_profile_v1_entities_for_arch(
332
+ provider: ProvidesProfiles,
333
+ arch: str,
334
+ ) -> dict[str, ProfileV1Entity]:
335
+ result: dict[str, ProfileV1Entity] = {}
336
+ for profile in provider.iter_profiles_for_arch(arch):
337
+ full_name = profile.id
338
+ relations = [f"arch:{arch}"]
339
+
340
+ needed_toolchain_quirks = sorted(profile.need_quirks)
341
+
342
+ result[profile.id] = {
343
+ "profile-v1": {
344
+ "id": profile.id,
345
+ "display_name": full_name,
346
+ "name": profile.id,
347
+ "arch": profile.arch,
348
+ "needed_toolchain_quirks": needed_toolchain_quirks,
349
+ "toolchain_common_flags_str": profile.get_common_flags(
350
+ needed_toolchain_quirks,
351
+ ),
352
+ },
353
+ "related": relations,
354
+ }
355
+ return result
@@ -34,7 +34,7 @@ from .pkg_manifest import (
34
34
  InputPackageManifestType,
35
35
  is_prerelease,
36
36
  )
37
- from .profile import PluginProfileProvider, ProfileProxy
37
+ from .profile import PluginProfileProvider, ProfileEntityProvider, ProfileProxy
38
38
  from .protocols import ProvidesPackageManifests
39
39
 
40
40
  if sys.version_info >= (3, 11):
@@ -234,6 +234,7 @@ class MetadataRepo(ProvidesPackageManifests):
234
234
  gc.logger,
235
235
  FSEntityProvider(gc.logger, pathlib.Path(self.root) / "entities"),
236
236
  MetadataRepoEntityProvider(self),
237
+ ProfileEntityProvider(self),
237
238
  )
238
239
  self._plugin_host_ctx = PluginHostContext.new(gc.logger, self.plugin_root)
239
240
  self._plugin_fn_evaluator = self._plugin_host_ctx.make_evaluator()
@@ -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:
@@ -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
 
File without changes