magmascript 3.1.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {magmascript-3.1.0 → magmascript-3.2.0}/PKG-INFO +30 -1
- {magmascript-3.1.0 → magmascript-3.2.0}/README.md +29 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/__init__.py +1 -1
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/cli.py +12 -2
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/config.py +19 -16
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/runner.py +8 -2
- magmascript-3.2.0/magmascript/lang/domain_bridge.py +198 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/interpreter.py +18 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/PKG-INFO +30 -1
- {magmascript-3.1.0 → magmascript-3.2.0}/pyproject.toml +1 -1
- magmascript-3.2.0/tests/test_domain_bridge.py +368 -0
- magmascript-3.1.0/magmascript/lang/domain_bridge.py +0 -119
- magmascript-3.1.0/tests/test_domain_bridge.py +0 -178
- {magmascript-3.1.0 → magmascript-3.2.0}/LICENSE +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/cache.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/commands.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/exceptions.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/github.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/output.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/registry.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/core/rpc.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/archive/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/archive/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/gh/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/gh/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/gh/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/lastfm/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/lastfm/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mac/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mac/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mac/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mb/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mb/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mc1/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mc1/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mc1/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mcp/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mcp/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/mcp/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/archive.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/met_museum.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/openverse.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/pexels.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/pixabay.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/providers/smithsonian.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/media/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/pi/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/pi/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/pi/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/rights/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/rights/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/rights/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/scores/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/scores/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/scores/tools.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/search/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/domains/search/client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/ast_nodes.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/astheno/__init__.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/astheno/arena.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/astheno/dump.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/astheno/floorplan.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/astheno/numeric.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/builtins.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/environment.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/hypnagogia.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/lexer.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/parser.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/pygments_mgs.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/tokens.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/lang/util.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript/repl.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/SOURCES.txt +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/dependency_links.txt +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/entry_points.txt +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/requires.txt +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/magmascript.egg-info/top_level.txt +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/setup.cfg +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_astheno.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_cache.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_classes.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_cli.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_commands.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_import.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_lang.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_mac.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_mc1.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_mcp_client.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_rpc.py +0 -0
- {magmascript-3.1.0 → magmascript-3.2.0}/tests/test_try_catch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magmascript
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Scripting toolkit with domain-first subcommands
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -584,6 +584,35 @@ magmascript toast search-index # Remove search index
|
|
|
584
584
|
magmascript toast all # Clear everything
|
|
585
585
|
```
|
|
586
586
|
|
|
587
|
+
### Domains from other packages
|
|
588
|
+
|
|
589
|
+
A package can publish its own domain by declaring an entry point; it then
|
|
590
|
+
appears in `.mgs` scripts and the REPL without any change to magmascript.
|
|
591
|
+
|
|
592
|
+
```toml
|
|
593
|
+
# in the other package's pyproject.toml
|
|
594
|
+
[project.entry-points."magmascript.domains"]
|
|
595
|
+
texastoast = "texastoast.mgs:TexastoastDomain"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
The value names a class taking a single `config` argument. It is constructed on
|
|
599
|
+
first use, not at startup, so a domain may hold real resources. Built-in domains
|
|
600
|
+
win a name clash, and an entry point that fails to import is skipped rather than
|
|
601
|
+
breaking the interpreter.
|
|
602
|
+
|
|
603
|
+
[texastoast](https://github.com/magmacrunchmedia/texastoast) is the first:
|
|
604
|
+
`pip install texastoast` and `.mgs` scripts can drive a game engine. It
|
|
605
|
+
registers under two names, `texastoast` and the shorter `tt`.
|
|
606
|
+
|
|
607
|
+
```magmascript
|
|
608
|
+
g = tt.game({"title": "hello", "width": 400, "height": 300})
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
A package may register the same class under several names this way; nothing
|
|
612
|
+
stops it, and the clients stay independent. Note that none of this touches the
|
|
613
|
+
`magmascript toast` CLI verb, which still clears caches — registry domains are
|
|
614
|
+
not dispatched as subcommands.
|
|
615
|
+
|
|
587
616
|
## Python Library
|
|
588
617
|
|
|
589
618
|
```python
|
|
@@ -549,6 +549,35 @@ magmascript toast search-index # Remove search index
|
|
|
549
549
|
magmascript toast all # Clear everything
|
|
550
550
|
```
|
|
551
551
|
|
|
552
|
+
### Domains from other packages
|
|
553
|
+
|
|
554
|
+
A package can publish its own domain by declaring an entry point; it then
|
|
555
|
+
appears in `.mgs` scripts and the REPL without any change to magmascript.
|
|
556
|
+
|
|
557
|
+
```toml
|
|
558
|
+
# in the other package's pyproject.toml
|
|
559
|
+
[project.entry-points."magmascript.domains"]
|
|
560
|
+
texastoast = "texastoast.mgs:TexastoastDomain"
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The value names a class taking a single `config` argument. It is constructed on
|
|
564
|
+
first use, not at startup, so a domain may hold real resources. Built-in domains
|
|
565
|
+
win a name clash, and an entry point that fails to import is skipped rather than
|
|
566
|
+
breaking the interpreter.
|
|
567
|
+
|
|
568
|
+
[texastoast](https://github.com/magmacrunchmedia/texastoast) is the first:
|
|
569
|
+
`pip install texastoast` and `.mgs` scripts can drive a game engine. It
|
|
570
|
+
registers under two names, `texastoast` and the shorter `tt`.
|
|
571
|
+
|
|
572
|
+
```magmascript
|
|
573
|
+
g = tt.game({"title": "hello", "width": 400, "height": 300})
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
A package may register the same class under several names this way; nothing
|
|
577
|
+
stops it, and the clients stay independent. Note that none of this touches the
|
|
578
|
+
`magmascript toast` CLI verb, which still clears caches — registry domains are
|
|
579
|
+
not dispatched as subcommands.
|
|
580
|
+
|
|
552
581
|
## Python Library
|
|
553
582
|
|
|
554
583
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""magmascript — a scripting toolkit with domain-first subcommands."""
|
|
2
2
|
|
|
3
|
-
__version__ = "3.
|
|
3
|
+
__version__ = "3.2.0"
|
|
4
4
|
|
|
5
5
|
from magmascript.core.config import Config, GHConfig, MediaConfig, MC1Config, PIConfig, get_config, load_config, set_config
|
|
6
6
|
from magmascript.core.registry import get_domain, list_domains, register_domain
|
|
@@ -409,17 +409,27 @@ def _dispatch_configure(action: str, args: list[str]):
|
|
|
409
409
|
Fetch the API key from the MCP server and write to config file.
|
|
410
410
|
|
|
411
411
|
Options:
|
|
412
|
-
--host HOST SSH host to fetch key from
|
|
412
|
+
--host HOST SSH host to fetch the key from, as user@host (required)
|
|
413
413
|
--help Show this help
|
|
414
414
|
""")
|
|
415
415
|
sys.exit(0)
|
|
416
416
|
|
|
417
|
-
host = "
|
|
417
|
+
host = ""
|
|
418
418
|
if "--host" in args:
|
|
419
419
|
idx = args.index("--host")
|
|
420
420
|
if idx + 1 < len(args):
|
|
421
421
|
host = args[idx + 1]
|
|
422
422
|
|
|
423
|
+
if not host:
|
|
424
|
+
print(
|
|
425
|
+
"Error: configure needs an explicit SSH target.\n"
|
|
426
|
+
" magmascript configure --host user@host\n"
|
|
427
|
+
"Or set the MCP API key directly: MAGMA_API_KEY in the environment, "
|
|
428
|
+
"or [mcp] api_key in ~/.config/magmascript/config.toml",
|
|
429
|
+
file=sys.stderr,
|
|
430
|
+
)
|
|
431
|
+
sys.exit(1)
|
|
432
|
+
|
|
423
433
|
print(f"Fetching API key from {host}...")
|
|
424
434
|
|
|
425
435
|
try:
|
|
@@ -10,6 +10,9 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
# The MCP server is a public endpoint, so it stays a usable default. SSH
|
|
14
|
+
# host/user defaults are intentionally empty — a fresh install must be told
|
|
15
|
+
# which machines to reach rather than probing someone else's network.
|
|
13
16
|
DEFAULT_MCP_URL = "https://magmacrunch.duckdns.org/mcp"
|
|
14
17
|
CONFIG_DIR = Path.home() / ".config" / "magmascript"
|
|
15
18
|
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
@@ -27,24 +30,24 @@ class MCPConfig:
|
|
|
27
30
|
class PIConfig:
|
|
28
31
|
"""Raspberry Pi SSH configuration."""
|
|
29
32
|
|
|
30
|
-
host: str = "
|
|
31
|
-
user: str = "
|
|
33
|
+
host: str = ""
|
|
34
|
+
user: str = ""
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
@dataclass
|
|
35
38
|
class MC1Config:
|
|
36
39
|
"""MC1 Windows PC SSH configuration."""
|
|
37
40
|
|
|
38
|
-
host: str = "
|
|
39
|
-
user: str = "
|
|
41
|
+
host: str = ""
|
|
42
|
+
user: str = ""
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
@dataclass
|
|
43
46
|
class MacConfig:
|
|
44
47
|
"""Mac SSH configuration (a MacBook on the Tailnet)."""
|
|
45
48
|
|
|
46
|
-
host: str = "
|
|
47
|
-
user: str = "
|
|
49
|
+
host: str = ""
|
|
50
|
+
user: str = ""
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
@dataclass
|
|
@@ -52,8 +55,8 @@ class GHConfig:
|
|
|
52
55
|
"""GitHub API configuration."""
|
|
53
56
|
|
|
54
57
|
token: str = ""
|
|
55
|
-
owner: str = "
|
|
56
|
-
repo: str = "
|
|
58
|
+
owner: str = ""
|
|
59
|
+
repo: str = ""
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
@dataclass
|
|
@@ -146,23 +149,23 @@ def load_config(*, config_path: Path | None = None, env_prefix: str = "MAGMA_")
|
|
|
146
149
|
or mcp_section.get("api_key", ""),
|
|
147
150
|
),
|
|
148
151
|
pi=PIConfig(
|
|
149
|
-
host=os.environ.get(f"{env_prefix}PI_HOST", pi_section.get("host", "
|
|
150
|
-
user=os.environ.get(f"{env_prefix}PI_USER", pi_section.get("user", "
|
|
152
|
+
host=os.environ.get(f"{env_prefix}PI_HOST", pi_section.get("host", "")),
|
|
153
|
+
user=os.environ.get(f"{env_prefix}PI_USER", pi_section.get("user", "")),
|
|
151
154
|
),
|
|
152
155
|
mc1=MC1Config(
|
|
153
|
-
host=os.environ.get(f"{env_prefix}MC1_HOST", mc1_section.get("host", "
|
|
154
|
-
user=os.environ.get(f"{env_prefix}MC1_USER", mc1_section.get("user", "
|
|
156
|
+
host=os.environ.get(f"{env_prefix}MC1_HOST", mc1_section.get("host", "")),
|
|
157
|
+
user=os.environ.get(f"{env_prefix}MC1_USER", mc1_section.get("user", "")),
|
|
155
158
|
),
|
|
156
159
|
mac=MacConfig(
|
|
157
|
-
host=os.environ.get(f"{env_prefix}MAC_HOST", mac_section.get("host", "
|
|
158
|
-
user=os.environ.get(f"{env_prefix}MAC_USER", mac_section.get("user", "
|
|
160
|
+
host=os.environ.get(f"{env_prefix}MAC_HOST", mac_section.get("host", "")),
|
|
161
|
+
user=os.environ.get(f"{env_prefix}MAC_USER", mac_section.get("user", "")),
|
|
159
162
|
),
|
|
160
163
|
gh=GHConfig(
|
|
161
164
|
token=os.environ.get(f"{env_prefix}GH_TOKEN")
|
|
162
165
|
or os.environ.get("GITHUB_TOKEN", "")
|
|
163
166
|
or gh_section.get("token", ""),
|
|
164
|
-
owner=os.environ.get(f"{env_prefix}GH_OWNER", gh_section.get("owner", "
|
|
165
|
-
repo=os.environ.get(f"{env_prefix}GH_REPO", gh_section.get("repo", "
|
|
167
|
+
owner=os.environ.get(f"{env_prefix}GH_OWNER", gh_section.get("owner", "")),
|
|
168
|
+
repo=os.environ.get(f"{env_prefix}GH_REPO", gh_section.get("repo", "")),
|
|
166
169
|
),
|
|
167
170
|
media=MediaConfig(
|
|
168
171
|
pexels_key=os.environ.get(f"{env_prefix}PEXELS_KEY", media_section.get("pexels_key", "")),
|
|
@@ -16,8 +16,8 @@ class CommandRunner:
|
|
|
16
16
|
"""Execute shell commands locally or via SSH.
|
|
17
17
|
|
|
18
18
|
Args:
|
|
19
|
-
host: Remote host (e.g. "
|
|
20
|
-
user: Remote user
|
|
19
|
+
host: Remote host (e.g. "pi.local" or a Tailscale IP). Ignored in local mode.
|
|
20
|
+
user: Remote user. Ignored in local mode.
|
|
21
21
|
local: If True, run commands locally via subprocess instead of SSH.
|
|
22
22
|
"""
|
|
23
23
|
|
|
@@ -33,6 +33,12 @@ class CommandRunner:
|
|
|
33
33
|
"""
|
|
34
34
|
if self._local:
|
|
35
35
|
return self._local_run(cmd, timeout=timeout)
|
|
36
|
+
if not self._host:
|
|
37
|
+
raise SSHError(
|
|
38
|
+
"no SSH host configured — set the domain's host in "
|
|
39
|
+
"~/.config/magmascript/config.toml or the MAGMA_*_HOST env var",
|
|
40
|
+
host="",
|
|
41
|
+
)
|
|
36
42
|
return self._ssh(cmd, timeout=timeout)
|
|
37
43
|
|
|
38
44
|
def _local_run(self, cmd: str, *, timeout: int = 15) -> str:
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import fields, is_dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from magmascript.core.registry import get_domain, list_domains, register_domain
|
|
7
|
+
|
|
8
|
+
# Packages outside this repo publish domains under this entry-point group.
|
|
9
|
+
ENTRY_POINT_GROUP = "magmascript.domains"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DomainProxy:
|
|
13
|
+
"""A domain, exposed to scripts under its name.
|
|
14
|
+
|
|
15
|
+
The client is built on first use, not at interpreter startup. Every domain
|
|
16
|
+
used to be constructed for every script and every REPL session, which cost
|
|
17
|
+
startup time for domains nobody touched and ruled out any domain whose
|
|
18
|
+
construction does something — opening a window, holding a socket.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, name: str, client: Any = None, *,
|
|
22
|
+
factory: Any = None) -> None:
|
|
23
|
+
object.__setattr__(self, "_name", name)
|
|
24
|
+
object.__setattr__(self, "_factory", factory)
|
|
25
|
+
object.__setattr__(self, "_client_cache", client)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def _client(self) -> Any:
|
|
29
|
+
client = object.__getattribute__(self, "_client_cache")
|
|
30
|
+
if client is None:
|
|
31
|
+
factory = object.__getattribute__(self, "_factory")
|
|
32
|
+
if factory is None:
|
|
33
|
+
name = object.__getattribute__(self, "_name")
|
|
34
|
+
raise AttributeError(f"domain '{name}' has no client")
|
|
35
|
+
client = factory()
|
|
36
|
+
object.__setattr__(self, "_client_cache", client)
|
|
37
|
+
return client
|
|
38
|
+
|
|
39
|
+
def __getattr__(self, name: str) -> Any:
|
|
40
|
+
if name.startswith("_"):
|
|
41
|
+
raise AttributeError(name)
|
|
42
|
+
|
|
43
|
+
method = getattr(self._client, name, None)
|
|
44
|
+
if method is None:
|
|
45
|
+
raise AttributeError(f"'{self._name}' has no method '{name}'")
|
|
46
|
+
|
|
47
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
48
|
+
result = method(*args, **kwargs)
|
|
49
|
+
return wrap_result(result)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
# Only a client that was actually built can need closing, and touching
|
|
55
|
+
# self._client here would construct one just to tear it down.
|
|
56
|
+
client = object.__getattribute__(self, "_client_cache")
|
|
57
|
+
if client is not None and hasattr(client, "close"):
|
|
58
|
+
try:
|
|
59
|
+
client.close()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def __del__(self) -> None:
|
|
64
|
+
self.close()
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"<domain:{self._name}>"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DataclassWrapper:
|
|
71
|
+
def __init__(self, obj: Any) -> None:
|
|
72
|
+
object.__setattr__(self, "_obj", obj)
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, name: str) -> Any:
|
|
75
|
+
obj = object.__getattribute__(self, "_obj")
|
|
76
|
+
if name.startswith("_"):
|
|
77
|
+
raise AttributeError(name)
|
|
78
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
79
|
+
for f in fields(obj):
|
|
80
|
+
if f.name == name:
|
|
81
|
+
return wrap_result(getattr(obj, name))
|
|
82
|
+
# Not a field, but dataclasses carry properties and methods too, and a
|
|
83
|
+
# script has no way to tell which is which. InputState.dx and
|
|
84
|
+
# ControllerState.up are properties over the fields; without this a
|
|
85
|
+
# script could read the raw buttons byte but not the buttons.
|
|
86
|
+
try:
|
|
87
|
+
return wrap_result(getattr(obj, name))
|
|
88
|
+
except AttributeError:
|
|
89
|
+
raise AttributeError(
|
|
90
|
+
f"'{type(obj).__name__}' has no attribute '{name}'"
|
|
91
|
+
) from None
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
obj = object.__getattribute__(self, "_obj")
|
|
95
|
+
return repr(obj)
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
obj = object.__getattribute__(self, "_obj")
|
|
99
|
+
return str(obj)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ListWrapper:
|
|
103
|
+
def __init__(self, items: list[Any]) -> None:
|
|
104
|
+
self._items = [wrap_result(item) for item in items]
|
|
105
|
+
|
|
106
|
+
def __getitem__(self, index: int) -> Any:
|
|
107
|
+
return self._items[index]
|
|
108
|
+
|
|
109
|
+
def __len__(self) -> int:
|
|
110
|
+
return len(self._items)
|
|
111
|
+
|
|
112
|
+
def __iter__(self) -> Any:
|
|
113
|
+
return iter(self._items)
|
|
114
|
+
|
|
115
|
+
def __repr__(self) -> str:
|
|
116
|
+
return repr(self._items)
|
|
117
|
+
|
|
118
|
+
def __str__(self) -> str:
|
|
119
|
+
return str(self._items)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def wrap_result(result: Any) -> Any:
|
|
123
|
+
from magmascript.lang.interpreter import MgsInstance, MgsClass
|
|
124
|
+
from magmascript.lang.astheno import Fixed, Pine
|
|
125
|
+
if result is None:
|
|
126
|
+
return None
|
|
127
|
+
if isinstance(result, (int, float, str, bool)):
|
|
128
|
+
return result
|
|
129
|
+
# Asthenosphere values are dataclasses but are language primitives, not
|
|
130
|
+
# host objects to be introspected. They must not be wrapped.
|
|
131
|
+
if isinstance(result, (Fixed, Pine)):
|
|
132
|
+
return result
|
|
133
|
+
if callable(result):
|
|
134
|
+
return result
|
|
135
|
+
if isinstance(result, (MgsInstance, MgsClass)):
|
|
136
|
+
return result
|
|
137
|
+
if isinstance(result, list):
|
|
138
|
+
return ListWrapper(result)
|
|
139
|
+
if is_dataclass(result) and not isinstance(result, type):
|
|
140
|
+
return DataclassWrapper(result)
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _domain_factory(name: str) -> Any:
|
|
145
|
+
"""Build the thunk that constructs one domain's client on first use.
|
|
146
|
+
|
|
147
|
+
A constructor that raises now surfaces at the call that needed it, rather
|
|
148
|
+
than the domain silently not existing and the script reporting an undefined
|
|
149
|
+
variable several lines later.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def build() -> Any:
|
|
153
|
+
from magmascript.core.config import get_config
|
|
154
|
+
|
|
155
|
+
return get_domain(name)(get_config())
|
|
156
|
+
|
|
157
|
+
return build
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def discover_domains() -> None:
|
|
161
|
+
"""Register domains published by other installed packages.
|
|
162
|
+
|
|
163
|
+
A package declares one in its own metadata::
|
|
164
|
+
|
|
165
|
+
[project.entry-points."magmascript.domains"]
|
|
166
|
+
texastoast = "texastoast.mgs:TexastoastDomain"
|
|
167
|
+
|
|
168
|
+
which is what lets a domain live in the project it belongs to rather than
|
|
169
|
+
in this repo. Built-ins win on a name clash, and a broken entry point is
|
|
170
|
+
skipped rather than taking the interpreter down with it.
|
|
171
|
+
"""
|
|
172
|
+
from importlib.metadata import entry_points
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
found = entry_points(group=ENTRY_POINT_GROUP)
|
|
176
|
+
except Exception:
|
|
177
|
+
# Broken installed metadata is not this interpreter's problem to solve,
|
|
178
|
+
# and must not stop a script that uses only built-in domains.
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
builtins = set(list_domains())
|
|
182
|
+
for entry in found:
|
|
183
|
+
if entry.name in builtins:
|
|
184
|
+
continue
|
|
185
|
+
try:
|
|
186
|
+
register_domain(entry.name, entry.load())
|
|
187
|
+
except Exception:
|
|
188
|
+
# A third-party package that fails to import must not stop a script
|
|
189
|
+
# that never mentions it from running.
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def create_domain_proxies() -> dict[str, DomainProxy]:
|
|
194
|
+
discover_domains()
|
|
195
|
+
return {
|
|
196
|
+
name: DomainProxy(name, factory=_domain_factory(name))
|
|
197
|
+
for name in list_domains()
|
|
198
|
+
}
|
|
@@ -1110,6 +1110,24 @@ class Interpreter:
|
|
|
1110
1110
|
obj[node.property] = value
|
|
1111
1111
|
return value
|
|
1112
1112
|
|
|
1113
|
+
# A host object from a domain — a game entity, a config, anything the
|
|
1114
|
+
# bridge handed back. Reading its attributes already works
|
|
1115
|
+
# (exec_PropertyAccess falls through to getattr), so writing them has
|
|
1116
|
+
# to as well, or a script can inspect an object and never change it.
|
|
1117
|
+
if not node.property.startswith("_") and hasattr(obj, node.property):
|
|
1118
|
+
if isinstance(value, astheno.Fixed):
|
|
1119
|
+
# Widths are an Asthenosphere concept. Handing one to a host
|
|
1120
|
+
# object poisons its arithmetic downstream, so store the number.
|
|
1121
|
+
value = value.value
|
|
1122
|
+
try:
|
|
1123
|
+
setattr(obj, node.property, value)
|
|
1124
|
+
except AttributeError as e:
|
|
1125
|
+
raise self.error(
|
|
1126
|
+
f"Cannot set '{node.property}' on {type(obj).__name__}: {e}",
|
|
1127
|
+
node,
|
|
1128
|
+
) from None
|
|
1129
|
+
return value
|
|
1130
|
+
|
|
1113
1131
|
raise self.error(
|
|
1114
1132
|
f"Cannot set property on {type(obj).__name__}",
|
|
1115
1133
|
node,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magmascript
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Scripting toolkit with domain-first subcommands
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -584,6 +584,35 @@ magmascript toast search-index # Remove search index
|
|
|
584
584
|
magmascript toast all # Clear everything
|
|
585
585
|
```
|
|
586
586
|
|
|
587
|
+
### Domains from other packages
|
|
588
|
+
|
|
589
|
+
A package can publish its own domain by declaring an entry point; it then
|
|
590
|
+
appears in `.mgs` scripts and the REPL without any change to magmascript.
|
|
591
|
+
|
|
592
|
+
```toml
|
|
593
|
+
# in the other package's pyproject.toml
|
|
594
|
+
[project.entry-points."magmascript.domains"]
|
|
595
|
+
texastoast = "texastoast.mgs:TexastoastDomain"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
The value names a class taking a single `config` argument. It is constructed on
|
|
599
|
+
first use, not at startup, so a domain may hold real resources. Built-in domains
|
|
600
|
+
win a name clash, and an entry point that fails to import is skipped rather than
|
|
601
|
+
breaking the interpreter.
|
|
602
|
+
|
|
603
|
+
[texastoast](https://github.com/magmacrunchmedia/texastoast) is the first:
|
|
604
|
+
`pip install texastoast` and `.mgs` scripts can drive a game engine. It
|
|
605
|
+
registers under two names, `texastoast` and the shorter `tt`.
|
|
606
|
+
|
|
607
|
+
```magmascript
|
|
608
|
+
g = tt.game({"title": "hello", "width": 400, "height": 300})
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
A package may register the same class under several names this way; nothing
|
|
612
|
+
stops it, and the clients stay independent. Note that none of this touches the
|
|
613
|
+
`magmascript toast` CLI verb, which still clears caches — registry domains are
|
|
614
|
+
not dispatched as subcommands.
|
|
615
|
+
|
|
587
616
|
## Python Library
|
|
588
617
|
|
|
589
618
|
```python
|