ruyi 0.39.0__py3-none-any.whl

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/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
ruyi/cli/self_cli.py ADDED
@@ -0,0 +1,295 @@
1
+ import argparse
2
+ import os
3
+ import pathlib
4
+ import shutil
5
+ from typing import Final, TYPE_CHECKING
6
+
7
+ from .cmd import RootCommand
8
+
9
+ if TYPE_CHECKING:
10
+ from .completion import ArgumentParser
11
+ from .. import config
12
+
13
+ UNINSTALL_NOTICE: Final = """
14
+ [bold]Thanks for hacking with [yellow]Ruyi[/]![/]
15
+
16
+ This will uninstall [yellow]Ruyi[/] from your system, and optionally remove
17
+ all installed packages and [yellow]Ruyi[/]-managed repository data if the
18
+ [green]--purge[/] switch is given on the command line.
19
+
20
+ Note that your [yellow]Ruyi[/] virtual environments [bold]will become unusable[/] after
21
+ [yellow]Ruyi[/] is uninstalled. You should take care of migrating or cleaning
22
+ them yourselves afterwards.
23
+ """
24
+
25
+
26
+ # Self-management commands
27
+ class SelfCommand(
28
+ RootCommand,
29
+ cmd="self",
30
+ has_subcommands=True,
31
+ help="Manage this Ruyi installation",
32
+ ):
33
+ @classmethod
34
+ def configure_args(
35
+ cls,
36
+ gc: "config.GlobalConfig",
37
+ p: "ArgumentParser",
38
+ ) -> None:
39
+ pass
40
+
41
+
42
+ class SelfCleanCommand(
43
+ SelfCommand,
44
+ cmd="clean",
45
+ help="Remove various Ruyi-managed data to reclaim storage",
46
+ ):
47
+ @classmethod
48
+ def configure_args(
49
+ cls,
50
+ gc: "config.GlobalConfig",
51
+ p: "ArgumentParser",
52
+ ) -> None:
53
+ p.add_argument(
54
+ "--quiet",
55
+ "-q",
56
+ action="store_true",
57
+ help="Do not print out the actions being performed",
58
+ )
59
+ p.add_argument(
60
+ "--all",
61
+ action="store_true",
62
+ help="Remove all covered data",
63
+ )
64
+ p.add_argument(
65
+ "--distfiles",
66
+ action="store_true",
67
+ help="Remove all downloaded distfiles if any",
68
+ )
69
+ p.add_argument(
70
+ "--installed-pkgs",
71
+ action="store_true",
72
+ help="Remove all installed packages if any",
73
+ )
74
+ p.add_argument(
75
+ "--news-read-status",
76
+ action="store_true",
77
+ help="Mark all news items as unread",
78
+ )
79
+ p.add_argument(
80
+ "--progcache",
81
+ action="store_true",
82
+ help="Clear the Ruyi program cache",
83
+ )
84
+ p.add_argument(
85
+ "--repo",
86
+ action="store_true",
87
+ help="Remove the Ruyi repo if located in Ruyi-managed cache directory",
88
+ )
89
+ p.add_argument(
90
+ "--telemetry",
91
+ action="store_true",
92
+ help="Remove all telemetry data recorded if any",
93
+ )
94
+
95
+ @classmethod
96
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
97
+ logger = cfg.logger
98
+ quiet: bool = args.quiet
99
+ all: bool = args.all
100
+ distfiles: bool = args.distfiles
101
+ installed_pkgs: bool = args.installed_pkgs
102
+ news_read_status: bool = args.news_read_status
103
+ progcache: bool = args.progcache
104
+ repo: bool = args.repo
105
+ telemetry: bool = args.telemetry
106
+
107
+ if all:
108
+ distfiles = True
109
+ installed_pkgs = True
110
+ news_read_status = True
111
+ progcache = True
112
+ repo = True
113
+ telemetry = True
114
+
115
+ if not any(
116
+ [
117
+ distfiles,
118
+ installed_pkgs,
119
+ news_read_status,
120
+ progcache,
121
+ repo,
122
+ telemetry,
123
+ ]
124
+ ):
125
+ logger.F("no data specified for cleaning")
126
+ logger.I(
127
+ "please check [yellow]ruyi self clean --help[/] for a list of cleanable data"
128
+ )
129
+ return 1
130
+
131
+ _do_reset(
132
+ cfg,
133
+ quiet=quiet,
134
+ distfiles=distfiles,
135
+ installed_pkgs=installed_pkgs,
136
+ news_read_status=news_read_status,
137
+ progcache=progcache,
138
+ repo=repo,
139
+ telemetry=telemetry,
140
+ )
141
+
142
+ return 0
143
+
144
+
145
+ class SelfUninstallCommand(
146
+ SelfCommand,
147
+ cmd="uninstall",
148
+ help="Uninstall Ruyi",
149
+ ):
150
+ @classmethod
151
+ def configure_args(
152
+ cls,
153
+ gc: "config.GlobalConfig",
154
+ p: "ArgumentParser",
155
+ ) -> None:
156
+ p.add_argument(
157
+ "--purge",
158
+ action="store_true",
159
+ help="Remove all installed packages and Ruyi-managed remote repo data",
160
+ )
161
+ p.add_argument(
162
+ "-y",
163
+ action="store_true",
164
+ dest="consent",
165
+ help="Give consent for uninstallation on CLI; do not ask for confirmation",
166
+ )
167
+
168
+ @classmethod
169
+ def main(cls, cfg: "config.GlobalConfig", args: argparse.Namespace) -> int:
170
+ from . import user_input
171
+
172
+ logger = cfg.logger
173
+ purge: bool = args.purge
174
+ consent: bool = args.consent
175
+ logger.D(f"ruyi self uninstall: purge={purge}, consent={consent}")
176
+
177
+ if cfg.is_installation_externally_managed:
178
+ logger.F(
179
+ "this [yellow]ruyi[/] is externally managed, for example, by the system package manager, and cannot be uninstalled this way"
180
+ )
181
+ logger.I("please uninstall via the external manager instead")
182
+ return 1
183
+
184
+ if not cfg.is_packaged:
185
+ logger.F(
186
+ "this [yellow]ruyi[/] is not in standalone form, and cannot be uninstalled this way"
187
+ )
188
+ return 1
189
+
190
+ if not consent:
191
+ logger.stdout(UNINSTALL_NOTICE)
192
+ if not user_input.ask_for_yesno_confirmation(logger, "Continue?"):
193
+ logger.I("aborting uninstallation")
194
+ return 0
195
+ else:
196
+ logger.I("uninstallation consent given over CLI, proceeding")
197
+
198
+ _do_reset(
199
+ cfg,
200
+ quiet=False,
201
+ installed_pkgs=purge,
202
+ all_state=purge,
203
+ all_cache=purge,
204
+ self_binary=True,
205
+ )
206
+
207
+ logger.I("[yellow]ruyi[/] is uninstalled")
208
+
209
+ return 0
210
+
211
+
212
+ def _do_reset(
213
+ cfg: "config.GlobalConfig",
214
+ quiet: bool = False,
215
+ *,
216
+ installed_pkgs: bool = False,
217
+ all_state: bool = False,
218
+ news_read_status: bool = False, # ignored if all_state=True
219
+ telemetry: bool = False, # ignored if all_state=True
220
+ all_cache: bool = False,
221
+ distfiles: bool = False, # ignored if all_cache=True
222
+ progcache: bool = False, # ignored if all_cache=True
223
+ repo: bool = False, # ignored if all_cache=True
224
+ self_binary: bool = False,
225
+ ) -> None:
226
+ logger = cfg.logger
227
+
228
+ def status(s: str) -> None:
229
+ if quiet:
230
+ return
231
+ logger.I(s)
232
+
233
+ if installed_pkgs:
234
+ status("removing installed packages")
235
+ shutil.rmtree(cfg.data_root, True)
236
+
237
+ # do not record any telemetry data if we're purging it
238
+ if all_state or telemetry:
239
+ if tm := cfg.telemetry:
240
+ tm.discard_events(True)
241
+
242
+ if all_state:
243
+ status("removing state data")
244
+ shutil.rmtree(cfg.state_root, True)
245
+ else:
246
+ if news_read_status:
247
+ status("removing read status of news items")
248
+ cfg.news_read_status.remove()
249
+
250
+ if telemetry:
251
+ status("removing all telemetry data")
252
+ shutil.rmtree(cfg.telemetry_root, True)
253
+
254
+ if all_cache:
255
+ status("removing cached data")
256
+ shutil.rmtree(cfg.cache_root, True)
257
+ else:
258
+ if distfiles:
259
+ status("removing downloaded distfiles")
260
+ # TODO: deduplicate the path derivation
261
+ shutil.rmtree(os.path.join(cfg.cache_root, "distfiles"), True)
262
+
263
+ if progcache:
264
+ status("clearing the Ruyi program cache")
265
+ # TODO: deduplicate the path derivation
266
+ shutil.rmtree(os.path.join(cfg.cache_root, "progcache"), True)
267
+
268
+ if repo:
269
+ # for safety, don't remove the repo if it's outside of Ruyi's XDG
270
+ # cache root
271
+ repo_dir = pathlib.Path(cfg.get_repo_dir()).resolve()
272
+ cache_root = pathlib.Path(cfg.cache_root).resolve()
273
+
274
+ repo_is_below_cache_root = False
275
+ for p in repo_dir.parents:
276
+ if p == cache_root:
277
+ repo_is_below_cache_root = True
278
+ break
279
+
280
+ if not repo_is_below_cache_root:
281
+ logger.W(
282
+ "not removing the Ruyi repo: it is outside of the Ruyi cache directory"
283
+ )
284
+ else:
285
+ status("removing the Ruyi repo")
286
+ shutil.rmtree(repo_dir, True)
287
+
288
+ if self_binary:
289
+ status("removing the ruyi binary")
290
+ try:
291
+ os.unlink(cfg.self_exe)
292
+ except FileNotFoundError:
293
+ # we might have already removed ourselves during the purge; nothing to
294
+ # do now.
295
+ pass
ruyi/cli/user_input.py ADDED
@@ -0,0 +1,127 @@
1
+ import os.path
2
+
3
+ from ..log import RuyiLogger
4
+
5
+
6
+ def pause_before_continuing(
7
+ logger: RuyiLogger,
8
+ ) -> None:
9
+ """Pause and wait for the user to press Enter before continuing.
10
+
11
+ EOFError should be handled by the caller."""
12
+
13
+ logger.stdout("Press [green]<ENTER>[/] to continue: ", end="")
14
+ input()
15
+
16
+
17
+ def ask_for_yesno_confirmation(
18
+ logger: RuyiLogger,
19
+ prompt: str,
20
+ default: bool = False,
21
+ ) -> bool:
22
+ choices_help = "(Y/n)" if default else "(y/N)"
23
+
24
+ while True:
25
+ try:
26
+ logger.stdout(f"{prompt} {choices_help} ", end="")
27
+ user_input = input()
28
+ except EOFError:
29
+ yesno = "YES" if default else "NO"
30
+ logger.W(
31
+ f"EOF while reading user input, assuming the default choice {yesno}"
32
+ )
33
+ return default
34
+
35
+ if not user_input:
36
+ return default
37
+ if user_input in {"Y", "y", "yes"}:
38
+ return True
39
+ if user_input in {"N", "n", "no"}:
40
+ return False
41
+ else:
42
+ logger.stdout(f"Unrecognized input [yellow]'{user_input}'[/].")
43
+ logger.stdout("Accepted choices: Y/y/yes for YES, N/n/no for NO.")
44
+
45
+
46
+ def ask_for_kv_choice(
47
+ logger: RuyiLogger,
48
+ prompt: str,
49
+ choices_kv: dict[str, str],
50
+ default_key: str | None = None,
51
+ ) -> str:
52
+ choices_kv_list = list(choices_kv.items())
53
+ choices_prompts = [i[1] for i in choices_kv_list]
54
+
55
+ default_idx: int | None = None
56
+ if default_key is not None:
57
+ for i, k in enumerate(choices_kv_list):
58
+ if k[0] == default_key:
59
+ default_idx = i
60
+ break
61
+ if default_idx is None:
62
+ raise ValueError(f"Default choice key '{default_key}' not in choices")
63
+
64
+ choice = ask_for_choice(logger, prompt, choices_prompts, default_idx)
65
+ return choices_kv_list[choice][0]
66
+
67
+
68
+ def ask_for_choice(
69
+ logger: RuyiLogger,
70
+ prompt: str,
71
+ choices_texts: list[str],
72
+ default_idx: int | None = None,
73
+ ) -> int:
74
+ logger.stdout(prompt, end="\n\n")
75
+ for i, choice_text in enumerate(choices_texts):
76
+ logger.stdout(f" {i + 1}. {choice_text}")
77
+
78
+ logger.stdout("")
79
+
80
+ nr_choices = len(choices_texts)
81
+ if default_idx is not None:
82
+ if not (0 <= default_idx < nr_choices):
83
+ raise ValueError(f"Default choice index {default_idx} out of range")
84
+ choices_help = f"(1-{nr_choices}, default {default_idx + 1})"
85
+ else:
86
+ choices_help = f"(1-{nr_choices})"
87
+ while True:
88
+ try:
89
+ user_input = input(f"Choice? {choices_help} ")
90
+ except EOFError:
91
+ raise ValueError("EOF while reading user choice")
92
+
93
+ if default_idx is not None and not user_input:
94
+ return default_idx
95
+
96
+ try:
97
+ choice_int = int(user_input)
98
+ except ValueError:
99
+ logger.stdout(f"Unrecognized input [yellow]'{user_input}'[/].")
100
+ logger.stdout(
101
+ f"Accepted choices: an integer number from 1 to {nr_choices} inclusive."
102
+ )
103
+ continue
104
+
105
+ if 1 <= choice_int <= nr_choices:
106
+ return choice_int - 1
107
+
108
+ logger.stdout(f"Out-of-range input [yellow]'{user_input}'[/].")
109
+ logger.stdout(
110
+ f"Accepted choices: an integer number from 1 to {nr_choices} inclusive."
111
+ )
112
+
113
+
114
+ def ask_for_file(
115
+ logger: RuyiLogger,
116
+ prompt: str,
117
+ ) -> str:
118
+ while True:
119
+ try:
120
+ user_input = input(f"{prompt} ")
121
+ except EOFError:
122
+ raise ValueError("EOF while reading user input")
123
+
124
+ if os.path.exists(user_input):
125
+ return user_input
126
+
127
+ logger.stdout(f"[yellow]'{user_input}'[/] is not a path to an existing file.")
@@ -0,0 +1,45 @@
1
+ import argparse
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .cmd import RootCommand
5
+
6
+ if TYPE_CHECKING:
7
+ from .completion import ArgumentParser
8
+ from ..config import GlobalConfig
9
+
10
+
11
+ class VersionCommand(
12
+ RootCommand,
13
+ cmd="version",
14
+ help="Print version information",
15
+ ):
16
+ @classmethod
17
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
18
+ pass
19
+
20
+ @classmethod
21
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
22
+ return cli_version(cfg, args)
23
+
24
+
25
+ def cli_version(cfg: "GlobalConfig", args: argparse.Namespace) -> int:
26
+ import ruyi
27
+ from ..ruyipkg.host import get_native_host
28
+ from ..version import COPYRIGHT_NOTICE, MPL_REDIST_NOTICE, RUYI_SEMVER
29
+
30
+ print(f"Ruyi {RUYI_SEMVER}\n\nRunning on {get_native_host()}.")
31
+
32
+ if cfg.is_installation_externally_managed:
33
+ print("This Ruyi installation is externally managed.")
34
+
35
+ print()
36
+
37
+ cfg.logger.stdout(COPYRIGHT_NOTICE)
38
+
39
+ # Output the MPL notice only when we actually bundle and depend on the
40
+ # MPL component(s), which right now is only certifi. Keep the condition
41
+ # synced with __main__.py.
42
+ if hasattr(ruyi, "__compiled__") and ruyi.__compiled__.standalone:
43
+ cfg.logger.stdout(MPL_REDIST_NOTICE)
44
+
45
+ return 0