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/pluginhost/ctx.py ADDED
@@ -0,0 +1,222 @@
1
+ import abc
2
+ import os
3
+ import pathlib
4
+ from typing import (
5
+ Callable,
6
+ Final,
7
+ Generic,
8
+ MutableMapping,
9
+ Protocol,
10
+ TypeVar,
11
+ TYPE_CHECKING,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from typing_extensions import Self
16
+
17
+ from ..log import RuyiLogger
18
+ from . import api
19
+ from . import paths
20
+
21
+
22
+ ENV_PLUGIN_BACKEND_KEY: Final = "RUYI_PLUGIN_BACKEND"
23
+
24
+
25
+ class SupportsGetOption(Protocol):
26
+ def get_option(self, key: str) -> object: ...
27
+
28
+
29
+ class SupportsEvalFunction(Protocol):
30
+ def eval_function(
31
+ self,
32
+ function: object,
33
+ *args: object,
34
+ **kwargs: object,
35
+ ) -> object: ...
36
+
37
+
38
+ ModuleTy = TypeVar("ModuleTy", bound=SupportsGetOption, covariant=True)
39
+ EvalTy = TypeVar("EvalTy", bound=SupportsEvalFunction, covariant=True)
40
+
41
+
42
+ class PluginHostContext(Generic[ModuleTy, EvalTy], metaclass=abc.ABCMeta):
43
+ @staticmethod
44
+ def new(
45
+ host_logger: RuyiLogger,
46
+ plugin_root: pathlib.Path,
47
+ ) -> "PluginHostContext[SupportsGetOption, SupportsEvalFunction]":
48
+ plugin_backend = os.environ.get("RUYI_PLUGIN_BACKEND", "")
49
+ if not plugin_backend:
50
+ plugin_backend = "unsandboxed"
51
+
52
+ match plugin_backend:
53
+ case "unsandboxed":
54
+ return UnsandboxedPluginHostContext(host_logger, plugin_root)
55
+ case _:
56
+ raise RuntimeError(f"unsupported plugin backend: {plugin_backend}")
57
+
58
+ def __init__(
59
+ self,
60
+ host_logger: RuyiLogger,
61
+ plugin_root: pathlib.Path,
62
+ ) -> None:
63
+ self._host_logger = host_logger
64
+ self._plugin_root = plugin_root
65
+ # resolved path: finalized module
66
+ self._module_cache: MutableMapping[str, ModuleTy] = {}
67
+ # plugin id: finalized plugin module
68
+ self._loaded_plugins: dict[str, SupportsGetOption] = {}
69
+ # plugin id: {key: value}
70
+ self._value_cache: dict[str, dict[str, object]] = {}
71
+
72
+ @abc.abstractmethod
73
+ def make_loader(
74
+ self,
75
+ originating_file: pathlib.Path,
76
+ module_cache: MutableMapping[str, ModuleTy],
77
+ is_cmd: bool,
78
+ ) -> "BasePluginLoader[ModuleTy]":
79
+ raise NotImplementedError
80
+
81
+ @abc.abstractmethod
82
+ def make_evaluator(self) -> EvalTy:
83
+ raise NotImplementedError
84
+
85
+ @property
86
+ def host_logger(self) -> RuyiLogger:
87
+ return self._host_logger
88
+
89
+ @property
90
+ def plugin_root(self) -> pathlib.Path:
91
+ return self._plugin_root
92
+
93
+ def load_plugin(self, plugin_id: str, is_cmd: bool) -> None:
94
+ plugin_dir = paths.get_plugin_dir(plugin_id, self._plugin_root)
95
+
96
+ loader = self.make_loader(
97
+ plugin_dir / paths.PLUGIN_ENTRYPOINT_FILENAME,
98
+ self._module_cache,
99
+ is_cmd,
100
+ )
101
+ loaded_plugin = loader.load_this_plugin()
102
+ self._loaded_plugins[plugin_id] = loaded_plugin
103
+
104
+ def is_plugin_loaded(self, plugin_id: str) -> bool:
105
+ return plugin_id in self._loaded_plugins
106
+
107
+ def get_from_plugin(
108
+ self,
109
+ plugin_id: str,
110
+ key: str,
111
+ is_cmd_plugin: bool = False,
112
+ ) -> object | None:
113
+ if not self.is_plugin_loaded(plugin_id):
114
+ self.load_plugin(plugin_id, is_cmd_plugin)
115
+
116
+ if plugin_id not in self._value_cache:
117
+ self._value_cache[plugin_id] = {}
118
+
119
+ try:
120
+ return self._value_cache[plugin_id][key]
121
+ except KeyError:
122
+ v = self._loaded_plugins[plugin_id].get_option(key)
123
+ self._value_cache[plugin_id][key] = v
124
+ return v
125
+
126
+
127
+ class BasePluginLoader(Generic[ModuleTy], metaclass=abc.ABCMeta):
128
+ """Base class for plugin loaders loading from Ruyi repo.
129
+
130
+ Load paths take one of the following shapes:
131
+
132
+ * relative path: loads the path relative from the originating file's location,
133
+ but crossing plugin boundary is not allowed
134
+ * absolute path: similar to above, but relative to the plugin's FS root
135
+ * `ruyi-plugin://${plugin-id}`: loads from the plugin `plugin-id` residing
136
+ in the same repo as the originating plugin, the "entrypoint" being hard-coded
137
+ to whatever the concrete implementation dictates
138
+ """
139
+
140
+ def __init__(
141
+ self,
142
+ phctx: PluginHostContext[ModuleTy, SupportsEvalFunction],
143
+ originating_file: pathlib.Path,
144
+ module_cache: MutableMapping[str, ModuleTy],
145
+ is_cmd: bool,
146
+ ) -> None:
147
+ self._phctx = phctx
148
+ self.originating_file = originating_file
149
+ self.module_cache = module_cache
150
+ self.is_cmd = is_cmd
151
+
152
+ @property
153
+ def host_logger(self) -> RuyiLogger:
154
+ return self._phctx.host_logger
155
+
156
+ @property
157
+ def root(self) -> pathlib.Path:
158
+ return self._phctx.plugin_root
159
+
160
+ def make_sub_loader(self, originating_file: pathlib.Path) -> "Self":
161
+ return self.__class__(
162
+ self._phctx,
163
+ originating_file,
164
+ self.module_cache,
165
+ self.is_cmd,
166
+ )
167
+
168
+ def load_this_plugin(self) -> ModuleTy:
169
+ return self._load(str(self.originating_file), True)
170
+
171
+ def load(self, path: str) -> ModuleTy:
172
+ return self._load(path, False)
173
+
174
+ def _load(self, path: str, is_root: bool) -> ModuleTy:
175
+ resolved_path: pathlib.Path
176
+ if is_root:
177
+ resolved_path = pathlib.Path(path)
178
+ else:
179
+ resolved_path = paths.resolve_ruyi_load_path(
180
+ path,
181
+ self.root,
182
+ False,
183
+ self.originating_file,
184
+ self.is_cmd,
185
+ )
186
+ resolved_path_str = str(resolved_path)
187
+ if resolved_path_str in self.module_cache:
188
+ return self.module_cache[resolved_path_str]
189
+
190
+ plugin_id = resolved_path.relative_to(self.root).parts[0]
191
+ plugin_dir = self.root / plugin_id
192
+
193
+ host_bridge = api.make_ruyi_plugin_api_for_module(
194
+ self._phctx,
195
+ resolved_path,
196
+ plugin_dir,
197
+ self.is_cmd,
198
+ )
199
+
200
+ mod = self.do_load_module(
201
+ resolved_path,
202
+ resolved_path.read_text("utf-8"),
203
+ host_bridge,
204
+ )
205
+ self.module_cache[resolved_path_str] = mod
206
+ return mod
207
+
208
+ @abc.abstractmethod
209
+ def do_load_module(
210
+ self,
211
+ resolved_path: pathlib.Path,
212
+ program: str,
213
+ ruyi_host_bridge: Callable[[object], api.RuyiHostAPI],
214
+ ) -> ModuleTy:
215
+ raise NotImplementedError
216
+
217
+
218
+ # import the built-in supported PluginHostContext implementation(s)
219
+ # this must come after the baseclass declarations
220
+
221
+ # pylint: disable-next=wrong-import-position
222
+ from .unsandboxed import UnsandboxedPluginHostContext # noqa: E402
@@ -0,0 +1,135 @@
1
+ import pathlib
2
+ import re
3
+ from typing import Final
4
+ from urllib.parse import unquote, urlparse
5
+
6
+ PLUGIN_ENTRYPOINT_FILENAME: Final = "mod.star"
7
+ PLUGIN_DATA_DIR: Final = "data"
8
+ PLUGIN_ID_RE: Final = re.compile("^[A-Za-z_][A-Za-z0-9_-]*$")
9
+
10
+
11
+ def validate_plugin_id(name: str) -> None:
12
+ if PLUGIN_ID_RE.match(name) is None:
13
+ raise RuntimeError(f"invalid plugin ID '{name}'")
14
+
15
+
16
+ def get_plugin_dir(plugin_id: str, plugin_root: pathlib.Path) -> pathlib.Path:
17
+ validate_plugin_id(plugin_id)
18
+ return plugin_root / plugin_id
19
+
20
+
21
+ def resolve_ruyi_load_path(
22
+ path: str,
23
+ plugin_root: pathlib.Path,
24
+ is_for_data: bool,
25
+ originating_file: pathlib.Path,
26
+ allow_host_fs_access: bool,
27
+ ) -> pathlib.Path:
28
+ parsed = urlparse(path)
29
+ if parsed.params or parsed.query or parsed.fragment:
30
+ raise RuntimeError("fancy URI features are not supported for load paths")
31
+
32
+ match parsed.scheme:
33
+ case "":
34
+ if parsed.netloc:
35
+ raise RuntimeError("'//' is not allowed as load path prefix")
36
+ return resolve_plain_load_path(
37
+ parsed.path,
38
+ plugin_root,
39
+ is_for_data,
40
+ originating_file=originating_file,
41
+ )
42
+
43
+ case "ruyi-plugin":
44
+ if is_for_data:
45
+ raise RuntimeError(
46
+ "the ruyi-plugin protocol is not allowed in this context"
47
+ )
48
+
49
+ if parsed.path:
50
+ raise RuntimeError(
51
+ "non-empty path segment is not allowed for ruyi-plugin:// load paths"
52
+ )
53
+
54
+ if not parsed.netloc:
55
+ raise RuntimeError(
56
+ "empty location is not allowed for ruyi-plugin:// load paths"
57
+ )
58
+
59
+ plugin_id = unquote(parsed.netloc)
60
+ return get_plugin_dir(plugin_id, plugin_root) / PLUGIN_ENTRYPOINT_FILENAME
61
+
62
+ case "ruyi-plugin-data":
63
+ if not is_for_data:
64
+ raise RuntimeError(
65
+ "the ruyi-plugin-data protocol is not allowed in this context"
66
+ )
67
+
68
+ if not parsed.path:
69
+ raise RuntimeError(
70
+ "empty path segment is not allowed for ruyi-plugin-data:// load paths"
71
+ )
72
+
73
+ if not parsed.netloc:
74
+ raise RuntimeError(
75
+ "empty location is not allowed for ruyi-plugin-data:// load paths"
76
+ )
77
+
78
+ return resolve_plain_load_path(
79
+ parsed.path,
80
+ plugin_root,
81
+ True,
82
+ plugin_id=parsed.netloc,
83
+ )
84
+
85
+ case "host":
86
+ if not allow_host_fs_access:
87
+ raise RuntimeError("the host protocol is not allowed in this context")
88
+
89
+ if not parsed.path:
90
+ raise RuntimeError(
91
+ "empty path segment is not allowed for host:// load paths"
92
+ )
93
+
94
+ if parsed.netloc:
95
+ raise RuntimeError(
96
+ "non-empty location is not allowed for host:// load paths"
97
+ )
98
+
99
+ return pathlib.Path(parsed.path)
100
+
101
+ case _:
102
+ raise RuntimeError(
103
+ f"unsupported Ruyi Starlark load path scheme {parsed.scheme}"
104
+ )
105
+
106
+
107
+ def resolve_plain_load_path(
108
+ path: str,
109
+ plugin_root: pathlib.Path,
110
+ is_for_data: bool,
111
+ *,
112
+ originating_file: pathlib.Path | None = None,
113
+ plugin_id: str | None = None,
114
+ ) -> pathlib.Path:
115
+ if originating_file is None and plugin_id is None:
116
+ raise ValueError("one of originating_file or plugin_id must be specified")
117
+
118
+ if plugin_id is None:
119
+ assert originating_file is not None
120
+ rel = originating_file.relative_to(plugin_root)
121
+ plugin_id = rel.parts[0]
122
+
123
+ plugin_dir = plugin_root / plugin_id
124
+ if is_for_data:
125
+ plugin_dir = plugin_dir / PLUGIN_DATA_DIR
126
+
127
+ p = pathlib.PurePosixPath(path)
128
+ if p.is_absolute():
129
+ return plugin_dir / p.relative_to("/")
130
+
131
+ resolved = (plugin_dir / p).resolve()
132
+ if not resolved.is_relative_to(plugin_dir):
133
+ raise ValueError("plain load paths are not allowed to cross plugin boundary")
134
+
135
+ return resolved
@@ -0,0 +1,37 @@
1
+ import argparse
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ..cli.cmd import AdminCommand
5
+
6
+ if TYPE_CHECKING:
7
+ from ..cli.completion import ArgumentParser
8
+ from ..config import GlobalConfig
9
+
10
+
11
+ class AdminRunPluginCommand(
12
+ AdminCommand,
13
+ cmd="run-plugin-cmd",
14
+ help="Run a plugin-defined command",
15
+ ):
16
+ @classmethod
17
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
18
+ p.add_argument(
19
+ "cmd_name",
20
+ type=str,
21
+ metavar="COMMAND-NAME",
22
+ help="Command name",
23
+ )
24
+ p.add_argument(
25
+ "cmd_args",
26
+ type=str,
27
+ nargs="*",
28
+ metavar="COMMAND-ARG",
29
+ help="Arguments to pass to the plugin command",
30
+ )
31
+
32
+ @classmethod
33
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
34
+ cmd_name = args.cmd_name
35
+ cmd_args = args.cmd_args
36
+
37
+ return cfg.repo.run_plugin_cmd(cmd_name, cmd_args)
@@ -0,0 +1,246 @@
1
+ import ast
2
+ import builtins
3
+ import inspect
4
+ import os
5
+ import pathlib
6
+ import sys
7
+ from types import CodeType
8
+ from typing import Callable, Final, MutableMapping, NoReturn, TYPE_CHECKING, cast
9
+
10
+ if TYPE_CHECKING:
11
+ from typing_extensions import Buffer
12
+
13
+ from .api import RuyiHostAPI
14
+ from .ctx import PluginHostContext, BasePluginLoader
15
+
16
+
17
+ class UnsandboxedModuleDict(dict[str, object]):
18
+ def get_option(self, key: str) -> object:
19
+ return self.get(key, None)
20
+
21
+
22
+ class UnsandboxedTrivialEvaluator:
23
+ def eval_function(
24
+ self,
25
+ function: object,
26
+ *args: object,
27
+ **kwargs: object,
28
+ ) -> object:
29
+ if callable(function):
30
+ return function(*args, **kwargs)
31
+ raise RuntimeError(f"the Python value {function!r} is not callable")
32
+
33
+
34
+ BUILTINS_TO_EXPOSE: Final = {
35
+ k: getattr(builtins, k)
36
+ for k in [
37
+ "abs",
38
+ "any",
39
+ "all",
40
+ "bool",
41
+ "bytes",
42
+ "dict",
43
+ "dir",
44
+ "enumerate",
45
+ "float",
46
+ "getattr",
47
+ "hasattr",
48
+ "hash",
49
+ "int",
50
+ "len",
51
+ "list",
52
+ "max",
53
+ "min",
54
+ "print",
55
+ "range",
56
+ "repr",
57
+ "reversed",
58
+ "sorted",
59
+ "str",
60
+ "tuple",
61
+ "type",
62
+ "zip",
63
+ ]
64
+ }
65
+
66
+
67
+ def _fail_helper(*args: object) -> NoReturn:
68
+ raise RuntimeError(f"fail: {''.join(str(x) for x in args)}")
69
+
70
+
71
+ class UnsandboxedPluginHostContext(
72
+ PluginHostContext[UnsandboxedModuleDict, UnsandboxedTrivialEvaluator]
73
+ ):
74
+ def make_loader(
75
+ self,
76
+ originating_file: pathlib.Path,
77
+ module_cache: MutableMapping[str, UnsandboxedModuleDict],
78
+ is_cmd: bool,
79
+ ) -> BasePluginLoader[UnsandboxedModuleDict]:
80
+ return UnsandboxedRuyiPluginLoader(self, originating_file, module_cache, is_cmd)
81
+
82
+ def make_evaluator(self) -> UnsandboxedTrivialEvaluator:
83
+ return UnsandboxedTrivialEvaluator()
84
+
85
+
86
+ def _is_name_private(n: str) -> bool:
87
+ return n.startswith("_")
88
+
89
+
90
+ def _assert_name_is_public(n: str) -> None | NoReturn:
91
+ if _is_name_private(n):
92
+ raise RuntimeError(f"error: trying to load private name {n}")
93
+ return None
94
+
95
+
96
+ class UnsandboxedRuyiPluginLoader(BasePluginLoader[UnsandboxedModuleDict]):
97
+ def do_load_module(
98
+ self,
99
+ resolved_path: pathlib.Path,
100
+ program: str,
101
+ ruyi_host_bridge: Callable[[object], RuyiHostAPI],
102
+ ) -> UnsandboxedModuleDict:
103
+ self.host_logger.D(f"unsandboxed module load: path {resolved_path}")
104
+
105
+ sub_loader = self.make_sub_loader(resolved_path)
106
+
107
+ def _load_stmt_helper(
108
+ spec: str,
109
+ *values_to_bind: str,
110
+ **renamed_values_to_bind: str,
111
+ ) -> None:
112
+ mod = sub_loader.load(spec)
113
+
114
+ curr_frame = inspect.currentframe()
115
+ if curr_frame is None:
116
+ raise RuntimeError(
117
+ "cannot inspect the Python runtime for the current frame"
118
+ )
119
+
120
+ parent_frame = curr_frame.f_back
121
+ if parent_frame is None:
122
+ raise RuntimeError(
123
+ "internal error: no parent frame for load() statement"
124
+ )
125
+
126
+ g = parent_frame.f_locals
127
+ for name in values_to_bind:
128
+ _assert_name_is_public(name)
129
+ g[name] = mod[name]
130
+ for dst_name, src_name in renamed_values_to_bind.items():
131
+ _assert_name_is_public(src_name)
132
+ g[dst_name] = mod[src_name]
133
+ return None
134
+
135
+ code = self.source_to_code(program, resolved_path)
136
+ mod_globals: dict[str, object] = {
137
+ "__builtins__": BUILTINS_TO_EXPOSE,
138
+ "fail": _fail_helper,
139
+ "load": _load_stmt_helper,
140
+ "ruyi_plugin_rev": ruyi_host_bridge,
141
+ }
142
+ # pylint: disable-next=exec-used
143
+ exec(code, mod_globals)
144
+ return UnsandboxedModuleDict(mod_globals)
145
+
146
+ # intentionally follows the importlib.abc.InspectLoader protocol, for
147
+ # easier refactoring whenever necessary.
148
+ @staticmethod
149
+ def source_to_code(
150
+ data: "Buffer | str | ast.Module",
151
+ path: "Buffer | str | os.PathLike[str]" = "<string>",
152
+ ) -> CodeType:
153
+ mod_ast: ast.Module
154
+ if isinstance(data, ast.Module):
155
+ mod_ast = data
156
+ else: # isinstance(data, str) or isinstance(data, Buffer)
157
+ mod_ast = ast.parse(data, path, "exec")
158
+
159
+ # lint the module on a best-effort basis to help fight syntax feature
160
+ # creep
161
+ lint_module(mod_ast)
162
+
163
+ return compile(mod_ast, path, "exec")
164
+
165
+
166
+ def lint_module(mod: ast.Module) -> None:
167
+ if node := GatedLanguageFeaturesPass().visit(mod):
168
+ raise RuntimeError(f"line {node.lineno}: language feature is gated")
169
+
170
+
171
+ class GatedLanguageFeaturesPass(ast.NodeVisitor):
172
+ def visit(self, node: ast.AST) -> ast.expr | ast.stmt | None:
173
+ return cast(ast.expr | ast.stmt | None, super().visit(node))
174
+
175
+ def generic_visit(self, node: ast.AST) -> ast.expr | ast.stmt | None:
176
+ """Traverses all types of nodes, bailing if non-minimal language
177
+ features are found."""
178
+
179
+ for _, value in ast.iter_fields(node):
180
+ if isinstance(value, list):
181
+ for item in value:
182
+ if isinstance(item, ast.AST):
183
+ if x := self.visit(item):
184
+ return x
185
+ elif isinstance(value, ast.AST):
186
+ if x := self.visit(value):
187
+ return x
188
+ return None
189
+
190
+ def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.NamedExpr:
191
+ return node
192
+
193
+ def visit_Raise(self, node: ast.Raise) -> ast.Raise:
194
+ return node
195
+
196
+ def visit_Assert(self, node: ast.Assert) -> ast.Assert:
197
+ return node
198
+
199
+ def visit_Import(self, node: ast.Import) -> ast.Import:
200
+ return node
201
+
202
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom:
203
+ return node
204
+
205
+ def visit_Try(self, node: ast.Try) -> ast.Try:
206
+ return node
207
+
208
+ if sys.version_info >= (3, 11):
209
+
210
+ def visit_TryStar(self, node: ast.TryStar) -> ast.TryStar:
211
+ return node
212
+
213
+ def visit_With(self, node: ast.With) -> ast.With:
214
+ return node
215
+
216
+ def visit_Match(self, node: ast.Match) -> ast.Match:
217
+ return node
218
+
219
+ def visit_Yield(self, node: ast.Yield) -> ast.Yield:
220
+ return node
221
+
222
+ def visit_YieldFrom(self, node: ast.YieldFrom) -> ast.YieldFrom:
223
+ return node
224
+
225
+ def visit_Global(self, node: ast.Global) -> ast.Global:
226
+ return node
227
+
228
+ def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.Nonlocal:
229
+ return node
230
+
231
+ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
232
+ return node
233
+
234
+ def visit_AsyncFunctionDef(
235
+ self, node: ast.AsyncFunctionDef
236
+ ) -> ast.AsyncFunctionDef:
237
+ return node
238
+
239
+ def visit_Await(self, node: ast.Await) -> ast.Await:
240
+ return node
241
+
242
+ def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AsyncFor:
243
+ return node
244
+
245
+ def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AsyncWith:
246
+ return node
ruyi/py.typed ADDED
File without changes
@@ -0,0 +1,20 @@
1
+ import base64
2
+ import zlib
3
+
4
+ from .data import RESOURCES, TEMPLATES
5
+
6
+
7
+ def _unpack_payload(x: bytes) -> str:
8
+ return zlib.decompress(base64.b64decode(x)).decode("utf-8")
9
+
10
+
11
+ def get_resource_str(template_name: str) -> str | None:
12
+ if t := RESOURCES.get(template_name):
13
+ return _unpack_payload(t)
14
+ return None
15
+
16
+
17
+ def get_template_str(template_name: str) -> str | None:
18
+ if t := TEMPLATES.get(template_name):
19
+ return _unpack_payload(t)
20
+ return None