amca 3.0.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 (52) hide show
  1. amca/__init__.py +17 -0
  2. amca/api.py +120 -0
  3. amca/cli/__init__.py +1 -0
  4. amca/cli/amca_cli.py +190 -0
  5. amca/cli/amcapl_cli.py +388 -0
  6. amca/cli/commands/__init__.py +1 -0
  7. amca/cli/commands/args_edit.py +67 -0
  8. amca/cli/commands/completions_cmd.py +136 -0
  9. amca/cli/commands/config_cmd.py +270 -0
  10. amca/cli/commands/doctor.py +161 -0
  11. amca/cli/commands/root_cmd.py +102 -0
  12. amca/cli/commands/run_cmd.py +36 -0
  13. amca/cli/common.py +142 -0
  14. amca/cli/completions.py +588 -0
  15. amca/cli/prompt.py +138 -0
  16. amca/config/__init__.py +1 -0
  17. amca/config/migrate.py +152 -0
  18. amca/config/schema.py +206 -0
  19. amca/config/store.py +256 -0
  20. amca/core/__init__.py +1 -0
  21. amca/core/context.py +162 -0
  22. amca/core/dirparse.py +76 -0
  23. amca/core/logger.py +233 -0
  24. amca/core/paths.py +103 -0
  25. amca/core/proc.py +114 -0
  26. amca/plugins/__init__.py +1 -0
  27. amca/plugins/argfiles.py +46 -0
  28. amca/plugins/loader.py +199 -0
  29. amca/plugins/markers.py +170 -0
  30. amca/plugins/registry.py +201 -0
  31. amca/plugins/sources.py +191 -0
  32. amca-3.0.0.dist-info/METADATA +548 -0
  33. amca-3.0.0.dist-info/RECORD +52 -0
  34. amca-3.0.0.dist-info/WHEEL +5 -0
  35. amca-3.0.0.dist-info/entry_points.txt +3 -0
  36. amca-3.0.0.dist-info/licenses/LICENSE +674 -0
  37. amca-3.0.0.dist-info/top_level.txt +2 -0
  38. amca_presets/__init__.py +14 -0
  39. amca_presets/autoscript/__init__.py +1 -0
  40. amca_presets/autoscript/_impl/__init__.py +1 -0
  41. amca_presets/autoscript/_impl/scripts.py +61 -0
  42. amca_presets/autoscript/plugin.py +152 -0
  43. amca_presets/example/__init__.py +1 -0
  44. amca_presets/example/plugin.py +106 -0
  45. amca_presets/meson/__init__.py +1 -0
  46. amca_presets/meson/_impl/__init__.py +1 -0
  47. amca_presets/meson/_impl/args.py +145 -0
  48. amca_presets/meson/_impl/project.py +93 -0
  49. amca_presets/meson/_impl/source_cache.py +93 -0
  50. amca_presets/meson/_impl/steps.py +248 -0
  51. amca_presets/meson/meson.build.template +55 -0
  52. amca_presets/meson/plugin.py +97 -0
amca/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """amca — directory-aware auto-executer.
2
+
3
+ Importing this package has no side effects: no config is read, no directory is
4
+ scanned, no prompt is shown. Everything that touches the filesystem happens
5
+ inside an explicitly constructed :class:`amca.core.context.AmcaContext`.
6
+
7
+ That is a deliberate constraint, not an accident. In the previous version,
8
+ importing ``impl.util.globals`` walked the directory tree and could block on an
9
+ interactive y/n prompt, which meant ``amca --help`` prompted you before it
10
+ printed help. Keep this module free of work.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __version__ = "3.0.0"
16
+
17
+ __all__ = ["__version__"]
amca/api.py ADDED
@@ -0,0 +1,120 @@
1
+ """The plugin SDK. This module is the public, stable surface for plugin authors.
2
+
3
+ A plugin is a directory containing ``plugin.py`` (or ``init.py``, still
4
+ accepted) that defines one subclass of :class:`Plugin`:
5
+
6
+ from amca.api import Plugin, PluginContext
7
+
8
+ class hello(Plugin):
9
+ name = "hello"
10
+
11
+ def should_load(self, ctx: PluginContext) -> bool:
12
+ return ctx.working_dir.has_file("Makefile")
13
+
14
+ def load(self, ctx: PluginContext) -> int:
15
+ ctx.log.log(f"args: {ctx.args}")
16
+ return 0
17
+
18
+ Two things changed from the previous plugin API, both for the same reason.
19
+
20
+ First, plugins now ``import amca.api`` instead of importing from a copy of
21
+ amca's internals vendored into every plugin directory. Those copies existed
22
+ because the old amca was a frozen binary with no importable package: each
23
+ preset shipped its own ``amca/logger.py``, ``amca/dirparse.py`` and
24
+ ``amca/plugin_base.py``, which then drifted from the originals. amca is a real
25
+ package now, so a plugin can just import it.
26
+
27
+ Second, the five positional parameters became one :class:`PluginContext`.
28
+ Adding a sixth piece of context used to be a breaking change for every plugin
29
+ in existence; now it is a new attribute. Old-style five-argument plugins are
30
+ still detected and adapted — see ``amca.plugins.loader``.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import abc
36
+ from dataclasses import dataclass
37
+ from pathlib import Path
38
+ from typing import TYPE_CHECKING
39
+
40
+ from .core import proc
41
+ from .core.dirparse import DirInfo, DirParser
42
+ from .core.logger import Logger
43
+
44
+ if TYPE_CHECKING: # pragma: no cover
45
+ from .core.context import AmcaRoot
46
+
47
+ __all__ = [
48
+ "DirInfo",
49
+ "DirParser",
50
+ "Logger",
51
+ "Plugin",
52
+ "PluginContext",
53
+ "PluginError",
54
+ "proc",
55
+ ]
56
+
57
+
58
+ class PluginError(RuntimeError):
59
+ """Raise from a plugin to fail cleanly with a message and no traceback."""
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class PluginContext:
64
+ """Everything a plugin is given for one invocation."""
65
+
66
+ #: Arguments routed to this plugin via its ``---name`` marker.
67
+ args: list[str]
68
+ #: Directory amca was invoked from.
69
+ working_dir: DirInfo
70
+ #: The resolved amca root, or None when the user is outside a project.
71
+ root: AmcaRoot | None
72
+ #: This plugin's private per-project directory
73
+ #: (``<root>/.Amca/plugins/<name>/``), or None when there is no root.
74
+ #: Created before the plugin runs. Put caches and per-project state here.
75
+ plugin_dir: Path | None
76
+ #: Shared directory reader — use this instead of ``Path.iterdir`` so
77
+ #: repeated lookups within one run stay cached.
78
+ dirs: DirParser
79
+ #: Logger honouring the user's verbosity settings.
80
+ log: Logger
81
+ #: True when the user passed ``--dry-run``. Plugins must print what they
82
+ #: would do and change nothing.
83
+ dry_run: bool = False
84
+
85
+ @property
86
+ def project_dir(self) -> Path:
87
+ """Root directory if inside a project, else the working directory."""
88
+ return self.root.path if self.root is not None else self.working_dir.path
89
+
90
+ def project_dir_info(self) -> DirInfo:
91
+ return self.dirs.parse_dir(self.project_dir)
92
+
93
+
94
+ class Plugin(abc.ABC):
95
+ """Base class for plugins.
96
+
97
+ Subclasses must be constructible with no arguments. amca instantiates the
98
+ class once per invocation, then calls ``should_load`` and, if that returns
99
+ True and the plugin is enabled, ``load``.
100
+ """
101
+
102
+ #: Display name. Defaults to the class name when unset. The *folder* name
103
+ #: is what the user types after the marker prefix; this is what appears in
104
+ #: log messages.
105
+ name: str = ""
106
+
107
+ #: Optional one-line summary shown by ``amcapl list``.
108
+ description: str = ""
109
+
110
+ @abc.abstractmethod
111
+ def should_load(self, ctx: PluginContext) -> bool:
112
+ """Cheap test for whether this plugin applies here.
113
+
114
+ Must not run subprocesses, write files, or take noticeable time — it is
115
+ called for every enabled plugin on every invocation.
116
+ """
117
+
118
+ @abc.abstractmethod
119
+ def load(self, ctx: PluginContext) -> int | None:
120
+ """Do the work. Return a process exit status; 0 or None means success."""
amca/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Command-line entry points and shared CLI plumbing."""
amca/cli/amca_cli.py ADDED
@@ -0,0 +1,190 @@
1
+ """``amca`` — run the plugins that apply to the current directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from ..config.schema import SCHEMA
10
+ from ..config.store import ConfigError
11
+ from ..core.context import AmcaContext
12
+ from ..plugins.markers import UnknownMarker, split_argv
13
+ from ..plugins.registry import Registry
14
+ from . import common
15
+ from .commands import args_edit, completions_cmd, config_cmd, doctor, root_cmd, run_cmd
16
+
17
+ __all__ = ["build_parser", "main"]
18
+
19
+ _GLOB_CHARS = "*?[]{}~"
20
+
21
+
22
+ def _epilog(prefix: str, markers: list[str]) -> str:
23
+ """Build the help epilog from the *effective* configuration.
24
+
25
+ The previous version hardcoded '---' and a made-up example. After
26
+ `config set plugins.marker_prefix ?` the help still told you to type
27
+ `---meson`, which is the one thing that no longer worked.
28
+ """
29
+ listed = " ".join(markers) if markers else "(no plugins installed)"
30
+ example = (
31
+ f"amca {markers[0]} -s" if markers else f"amca {prefix}<plugin> <plugin args>"
32
+ )
33
+ warning = ""
34
+ if any(char in prefix for char in _GLOB_CHARS):
35
+ warning = (
36
+ f"\n WARNING: the marker prefix {prefix!r} contains shell glob characters.\n"
37
+ f" You will have to quote every marker (amca '{prefix}meson'), because the\n"
38
+ f" shell expands it before amca sees it. Consider a prefix like --- or +++.\n"
39
+ )
40
+ return f"""\
41
+ plugin passthrough:
42
+ Arguments after a plugin marker go to that plugin, up to the next marker.
43
+
44
+ {example}
45
+
46
+ Current marker prefix: {prefix!r}
47
+ Markers here: {listed}
48
+ {warning}
49
+ Markers are the prefix plus the plugin's folder name, with underscores
50
+ written as dashes. An unknown marker is an error, not a silently discarded
51
+ argument. A bare -- stops marker parsing, for values starting with the prefix:
52
+
53
+ amca config set plugins.marker_prefix -- +++
54
+
55
+ troubleshooting:
56
+ amca config list --origin show every setting and which layer it came from
57
+ amca plugins --markers the exact markers this directory accepts
58
+ amca doctor check tools, paths, and plugin health
59
+ """
60
+
61
+
62
+ def build_parser(
63
+ prefix: str = "---", markers: list[str] | None = None
64
+ ) -> argparse.ArgumentParser:
65
+ parser = argparse.ArgumentParser(
66
+ prog="amca",
67
+ description="Run the build/script pipeline that fits the current directory.",
68
+ epilog=_epilog(prefix, markers or []),
69
+ formatter_class=argparse.RawDescriptionHelpFormatter,
70
+ )
71
+ common.add_global_flags(parser, marker_prefix=prefix)
72
+ parser.add_argument("--dry-run", action="store_true",
73
+ help="show what would run without running it")
74
+
75
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
76
+
77
+ run_parser = sub.add_parser("run", aliases=["r"],
78
+ help="run applicable plugins (default)")
79
+ run_parser.set_defaults(handler=run_cmd.handle)
80
+
81
+ root_cmd.register(sub)
82
+ args_edit.register(sub)
83
+ config_cmd.register(sub)
84
+ doctor.register(sub)
85
+
86
+ plugins_parser = sub.add_parser("plugins", help="list plugins and their markers")
87
+ # Machine-readable forms, for shell completion. One name per line, no
88
+ # decoration, no headers, and never an error exit — a completion function
89
+ # that has to parse a table is a completion function that breaks.
90
+ plugins_parser.add_argument("--names", action="store_true",
91
+ help="print installed plugin names, one per line")
92
+ plugins_parser.add_argument("--markers", action="store_true",
93
+ help="print plugin markers, one per line")
94
+ plugins_parser.add_argument("--enabled-only", action="store_true",
95
+ help="restrict --names/--markers to enabled plugins")
96
+ plugins_parser.set_defaults(handler=_handle_plugins)
97
+
98
+ completions_cmd.register(sub)
99
+
100
+ return parser
101
+
102
+
103
+ def _handle_plugins(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
104
+ registry = Registry(ctx)
105
+ prefix = ctx.config.get_str("plugins.marker_prefix")
106
+ entries = registry.entries
107
+
108
+ if getattr(args, "names", False) or getattr(args, "markers", False):
109
+ wanted = [e for e in entries if e.enabled or not args.enabled_only]
110
+ for entry in wanted:
111
+ if args.markers:
112
+ print(f"{prefix}{entry.name.replace('_', '-')}")
113
+ else:
114
+ print(entry.name)
115
+ return 0
116
+
117
+ if not entries:
118
+ print(f"No plugins installed in {ctx.plugins_dir}")
119
+ print("Install one with: amcapl install meson")
120
+ return 0
121
+ width = max(len(entry.name) for entry in entries)
122
+ for entry in entries:
123
+ state = "enabled " if entry.enabled else "disabled"
124
+ print(f" {state} {entry.name:<{width}} {prefix}{entry.name.replace('_', '-')}")
125
+ missing = registry.missing_enabled()
126
+ if missing:
127
+ print(f"\n enabled but not installed: {', '.join(missing)}")
128
+ return 0
129
+
130
+
131
+ def _main(argv: Sequence[str] | None = None) -> int:
132
+ raw = list(sys.argv[1:] if argv is None else argv)
133
+
134
+ # The marker prefix must be known before argv can be split, but it lives in
135
+ # config, which lives behind --config-dir. A hand-rolled scan rather than a
136
+ # throwaway argparse: argparse assigns `--` a special meaning and would
137
+ # print its own bare usage block on a malformed flag, before amca has even
138
+ # decided what the command is.
139
+ pre = common.pre_scan(raw)
140
+
141
+ try:
142
+ ctx = AmcaContext(config_dir_override=pre.get("config-dir"))
143
+ except ConfigError as exc:
144
+ print(f"amca: {exc}", file=sys.stderr)
145
+ return 2
146
+
147
+ prefix = pre.get("marker-prefix") or ctx.config.get_str("plugins.marker_prefix")
148
+ try:
149
+ SCHEMA["plugins.marker_prefix"].check(prefix)
150
+ except ConfigError as exc:
151
+ print(f"amca: {exc}", file=sys.stderr)
152
+ return 2
153
+ registry_names = [entry.name for entry in Registry(ctx).entries]
154
+
155
+ try:
156
+ split = split_argv(raw, registry_names, prefix)
157
+ except UnknownMarker as exc:
158
+ print(f"amca: {exc}", file=sys.stderr)
159
+ return 2
160
+ except ValueError as exc:
161
+ print(f"amca: {exc}", file=sys.stderr)
162
+ return 2
163
+
164
+ parser = build_parser(prefix, [f"{prefix}{n.replace('_', '-')}" for n in registry_names])
165
+ args = parser.parse_args(split.main)
166
+
167
+ try:
168
+ common.apply_global_flags(ctx, args)
169
+ except ConfigError as exc:
170
+ print(f"amca: {exc}", file=sys.stderr)
171
+ return 2
172
+
173
+ common.first_run_completion_notice(ctx)
174
+
175
+ if ctx.config.get_bool("core.greet"):
176
+ print("Hello Master")
177
+
178
+ handler = getattr(args, "handler", None)
179
+ if handler is None:
180
+ handler = run_cmd.handle
181
+ return int(handler(ctx, args, split.per_plugin) or 0)
182
+
183
+
184
+ def main(argv: Sequence[str] | None = None) -> int:
185
+ """Console-script entry point. Never lets a bare traceback reach the user."""
186
+ return common.run_entry(_main, argv)
187
+
188
+
189
+ if __name__ == "__main__": # pragma: no cover
190
+ raise SystemExit(main())