git-goose 0.0.1__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.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Anton Agestam
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.1
2
+ Name: git-goose
3
+ Version: 0.0.1
4
+ Summary: A picky and eager Git hook runner.
5
+ Author-email: Anton Agestam <git@antonagestam.se>
6
+ License: BSD 3-Clause
7
+ Project-URL: Source Repository, https://github.com/antonagestam/goose
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: <3.13,>3.12
13
+ Description-Content-Type: text/markdown; charset=UTF-8
14
+ License-File: LICENSE
15
+ Requires-Dist: pydantic
16
+ Requires-Dist: PyYAML
17
+ Requires-Dist: nodeenv
18
+ Requires-Dist: identify
19
+ Requires-Dist: typer
20
+ Requires-Dist: rich
21
+ Requires-Dist: shellingham
22
+
23
+ <!-- prettier-ignore-start -->
24
+ <!-- https://github.com/prettier/prettier/issues/15479 -->
25
+
26
+ > [!NOTE]
27
+ > This project is in its infancy. Please try it out and report and help fix any issues or missing features, but expect a somewhat broken experience.
28
+ >
29
+ > Breaking CLI changes is to be expected without notice between arbitrary versions until version 1.0 is released.
30
+
31
+ <!-- prettier-ignore-end -->
32
+
33
+ <h1 align=center>goose<br>🦆🧪💻</h1>
34
+
35
+ <p align=center>A <i>picky</i> and <i>eager</i> <a href=https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks>Git hook</a> runner.</p>
36
+
37
+ - Reproducible builds.
38
+ - Dynamic parallelism.
39
+ - Small file-system footprint.
40
+
41
+ ### Features
42
+
43
+ - Smart parallelism schedules hooks across CPUs while avoiding concurrent writes.
44
+ - Deterministic environments by using ecosystem-specific lock files.
45
+ - Environments are shared across hooks.
46
+ - Self-contained definitions means there's no need to push tool-specific configuration
47
+ upstream, or to maintain brittle mirroring schemes.
48
+
49
+ ### Parallelism
50
+
51
+ Goose takes care to keep your CPUs as busy as possible, optimizing to have the full
52
+ suite of hooks finish as soon as possible. It does this by distributing units of work to
53
+ all available processing cores.
54
+
55
+ Parameterized hooks, or hooks that take files as command line arguments, are divided to
56
+ one unit of work per available core. Whenever a core becomes available for more work, a
57
+ new unit is chosen for execution.
58
+
59
+ The scheduler takes care to never run more than one mutating hook on the same file. It
60
+ does this by taking into account hooks marked as `read_only` and by comparing sets of
61
+ files a unit of work is assigned to. Two incompatible hooks can be simultaneously
62
+ working on two separate parts of the code-base.
63
+
64
+ ### Deterministic environments
65
+
66
+ Goose uses lock files to facilitate deterministic results across developer environments
67
+ and CI. You specify dependencies in `goose.yaml`, and invoking `goose run` will produce
68
+ the appropriate lock files under a `.goose/` directory. The `.goose/` directory is meant
69
+ to be checked into git, so that future invocations of `goose run` can use the lock files
70
+ it contains to produce identical environments for hooks to run in.
71
+
72
+ ## Usage
73
+
74
+ Create a `goose.yaml` file in the repository root.
75
+
76
+ ```yaml
77
+ environments:
78
+ - id: python
79
+ ecosystem:
80
+ language: python
81
+ version: "3.12"
82
+ dependencies:
83
+ - ruff
84
+
85
+ hooks:
86
+ - id: ruff
87
+ environment: python
88
+ command: ruff
89
+ args: [check, --force-exclude, --fix]
90
+ types: [python]
91
+
92
+ - id: ruff-format
93
+ environment: python
94
+ command: ruff
95
+ args: [format, --force-exclude]
96
+ types: [python]
97
+ ```
98
+
99
+ Bootstrap environments, generate lock files, and install dependencies.
100
+
101
+ ```sh
102
+ $ python -m goose upgrade
103
+ ```
104
+
105
+ Run all hooks over all files.
106
+
107
+ ```sh
108
+ $ python -m goose run --select=all
109
+ ```
110
+
111
+ Commit configuration and lock files.
112
+
113
+ ```sh
114
+ $ git add goose.yaml .goose
115
+ $ git commit -m 'Add goose configuration'
116
+ ```
117
+
118
+ ### Upgrading hook versions
119
+
120
+ As pinning of hook versions is handled with lock files, there's no need to change
121
+ configuration to upgrade hook dependency versions, instead you just run the upgrade
122
+ command.
123
+
124
+ ```sh
125
+ $ python -m goose upgrade
126
+ $ git add .goose
127
+ $ git commit -m 'Bump goose dependencies'
128
+ ```
129
+
130
+ ### Example node hook
131
+
132
+ Goose currently supports Python and Node environments, here's an example using
133
+ [Prettier] to format Markdown files.
134
+
135
+ [Prettier]: https://prettier.io/
136
+
137
+ ```yaml
138
+ environments:
139
+ - id: node
140
+ ecosystem:
141
+ language: node
142
+ version: "21.7.1"
143
+ dependencies:
144
+ - prettier
145
+
146
+ hooks:
147
+ - id: prettier
148
+ environment: node
149
+ command: prettier
150
+ types: [markdown]
151
+ args:
152
+ - --write
153
+ - --ignore-unknown
154
+ - --parser=markdown
155
+ - --print-width=88
156
+ - --prose-wrap=always
157
+ ```
158
+
159
+ ### Read-only hooks
160
+
161
+ You will likely want to use a mix of pure linters, as well as formatters and
162
+ auto-fixers. Tools that don't mutate files can be more heavily parallelized by Goose,
163
+ because they can inspect overlapping sets of files simultaneously as other tools. To
164
+ enable this you set `read_only: true` in hook configuration.
165
+
166
+ ```yaml
167
+ environments:
168
+ - id: python
169
+ ecosystem:
170
+ language: python
171
+ version: "3.12"
172
+ dependencies:
173
+ - pre-commit-hooks
174
+
175
+ hooks:
176
+ - id: check-case-conflict
177
+ environment: python
178
+ command: check-case-conflict
179
+ read_only: true
180
+
181
+ - id: check-merge-conflict
182
+ environment: python
183
+ command: check-merge-conflict
184
+ read_only: true
185
+ types: [text]
186
+
187
+ - id: python-debug-statements
188
+ environment: python
189
+ command: debug-statement-hook
190
+ read_only: true
191
+ types: [python]
192
+
193
+ - id: detect-private-key
194
+ environment: python
195
+ command: detect-private-key
196
+ read_only: true
197
+ types: [text]
198
+
199
+ - id: end-of-file-fixer
200
+ environment: python
201
+ command: end-of-file-fixer
202
+ types: [text]
203
+
204
+ - id: trailing-whitespace-fixer
205
+ environment: python
206
+ command: trailing-whitespace-fixer
207
+ types: [text]
208
+ ```
209
+
210
+ Hooks that do not specify `read_only: true` will never run simultaneously as other tools
211
+ over the same file.
212
+
213
+ ### Non-parameterized hooks
214
+
215
+ Some tools don't support passing files, or just work better if given the responsibility
216
+ to parallelize work itself. One such tool is mypy. You can instruct goose to not pass
217
+ filenames to a hook (and as a consequence, also not spawn multiple parallel jobs for
218
+ this hook).
219
+
220
+ ```yaml
221
+ environments:
222
+ - id: mypy
223
+ ecosystem:
224
+ language: python
225
+ version: "3.12"
226
+ dependencies:
227
+ - mypy
228
+
229
+ hooks:
230
+ - id: mypy
231
+ environment: mypy
232
+ command: mypy
233
+ read_only: true
234
+ parameterize: false
235
+ ```
@@ -0,0 +1,30 @@
1
+ goose/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ goose/__main__.py,sha256=k-FvWXMtgVJy8P8y-ZZLmZwX_fVWHqepDaMs3V5pePI,7050
3
+ goose/asyncio.py,sha256=bOhXmv7yaGqoqMGiBB0-HH3XcXfBZPQ28dObnafLJ0U,444
4
+ goose/config.py,sha256=O44J4w6bfA1KH8hxZlu_BudgTCH6EWk9-b5cxaZmc0g,2023
5
+ goose/context.py,sha256=GHTGkCyTrkL2v05ch7X3n12msSH-hvoIO0QP-lLQzMg,983
6
+ goose/environment.py,sha256=rckXiaUaM1KGdAHF3RwyAUJ1CsbYQKe0aO5ExJgcSN8,8182
7
+ goose/executable_unit.py,sha256=K1_GlBpCFOgQqrbmawe6gHkADUpKyMwARp_qbkF0YKM,241
8
+ goose/filter.py,sha256=HHpRGeA8dlZWX4Y-Xm0JtmWWCvVU7YTurlOnf-Ur5Jg,252
9
+ goose/manifest.py,sha256=yYwMqUwM7z8OdjT7Mm5grsfq-FBZRGxzggahL7fGqFo,4613
10
+ goose/orphan_environments.py,sha256=XQ1KhzTFqsrobamjgk4aoe_GpF53aKUVQD8yoamHC7U,638
11
+ goose/parallel.py,sha256=CDVpVU4YFJqsJEDQ2W36B7MvU4LcUcaTtioUuiFuMDw,1212
12
+ goose/paths.py,sha256=skxJJ1r2QZjA64q_X1A73z24SSftw-5ciXmwFYMnlUg,580
13
+ goose/process.py,sha256=HKc--KzGP-i61vzDwJz-vAeSH1kw2aeDVyGZXZJH2FQ,735
14
+ goose/scheduler.py,sha256=-z1qmMlp9xhGZgQBccKbWp6ylOBJP8US-L_1si6f5Gc,5268
15
+ goose/targets.py,sha256=Cji6Faah9psmBnyWnBf9o9giEhBjVHht1ZkAtp6NqU8,2692
16
+ goose/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ goose/_utils/pydantic.py,sha256=mkTODVaAT2XxcG7-GpERbDTBU6q2G2_IRS86Gj2ueNk,139
18
+ goose/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ goose/backend/base.py,sha256=-MH6aj9M4oRQuAQ88n0KeUWjQ0HCbSVJQiVUrTkq12c,1546
20
+ goose/backend/index.py,sha256=E7S8qm-rw_PlUATzGp_ZoqvHOuNS6WrZVTBzMkbSiK0,442
21
+ goose/backend/node.py,sha256=B9vKfbOrFZG5R5EyrfwuGoD7LLlqo2A4wFoxyNhuM3g,5166
22
+ goose/backend/python.py,sha256=bJvMjfsIbsrm7M9YHbkeTMqYzTpQ2BN4gpbhOrs7JeI,5087
23
+ goose/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ goose/git/shared.py,sha256=OzpGVKnigWdIuHghj31Vdno-8gEMpScnb1TSxpzGfjs,1238
25
+ goose/git/status.py,sha256=T-TAzmDm0vY6WOfpNY8CCmttI2aEmroHsZLQ9gunYLY,4254
26
+ git_goose-0.0.1.dist-info/LICENSE,sha256=TI5yLazmd6njfFDJi4xrvKB71AjMuNSXLZrrZ112hn0,1521
27
+ git_goose-0.0.1.dist-info/METADATA,sha256=jQM293oOk5zMl3qSuzWxZ_yZFYctWhV5NSdFnHm3XoU,6280
28
+ git_goose-0.0.1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
29
+ git_goose-0.0.1.dist-info/top_level.txt,sha256=H7wC3j2XnCev-Pw5Zx1OB1QaR1u0kxIEmhfgWu7VcCE,6
30
+ git_goose-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (70.3.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ goose
goose/__init__.py ADDED
File without changes
goose/__main__.py ADDED
@@ -0,0 +1,251 @@
1
+ import asyncio
2
+ import enum
3
+ import sys
4
+ import time
5
+ from collections.abc import Collection
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+ from typing import Final
10
+ from typing import Optional
11
+ from typing import TypeAlias
12
+ from typing import assert_never
13
+
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.live import Live
17
+ from rich.panel import Panel
18
+ from rich.spinner import Spinner
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+
22
+ from goose.backend.base import RunResult
23
+ from goose.config import HookConfig
24
+
25
+ from .asyncio import asyncio_entrypoint
26
+ from .context import gather_context
27
+ from .environment import NeedsFreeze
28
+ from .environment import prepare_environment
29
+ from .orphan_environments import probe_orphan_environments
30
+ from .scheduler import Scheduler
31
+ from .scheduler import UnitFinished
32
+ from .scheduler import UnitScheduled
33
+ from .targets import Selector
34
+ from .targets import get_targets
35
+
36
+ ConfigOption: TypeAlias = Annotated[
37
+ Path,
38
+ typer.Option(
39
+ "--config",
40
+ resolve_path=True,
41
+ readable=True,
42
+ exists=True,
43
+ dir_okay=False,
44
+ ),
45
+ ]
46
+ default_config: Final = Path("goose.yaml")
47
+
48
+
49
+ cli = typer.Typer(pretty_exceptions_enable=False)
50
+
51
+
52
+ @cli.command()
53
+ @asyncio_entrypoint
54
+ async def upgrade(
55
+ config_path: ConfigOption = default_config,
56
+ ) -> None:
57
+ ctx = gather_context(config_path)
58
+ await asyncio.gather(
59
+ *[
60
+ asyncio.create_task(prepare_environment(environment, upgrade=True))
61
+ for environment in ctx.environments.values()
62
+ ]
63
+ )
64
+ print("All environments up-to-date", file=sys.stderr)
65
+
66
+
67
+ def format_unit_state(
68
+ state: RunResult | asyncio.Task[RunResult] | None,
69
+ spinner: Table,
70
+ ) -> Text | Table:
71
+ match state:
72
+ case None:
73
+ return Text("[ ]", style="dim")
74
+ case asyncio.Task():
75
+ return spinner
76
+ case RunResult.ok:
77
+ return Text("[✓]", style="green")
78
+ case RunResult.error:
79
+ return Text("[✗]", style="red")
80
+ case RunResult.modified:
81
+ return Text("[✎]", style="red")
82
+ case no_match:
83
+ assert_never(no_match)
84
+
85
+
86
+ def format_hook_name(
87
+ hook: HookConfig,
88
+ states: Collection[RunResult | asyncio.Task[RunResult] | None],
89
+ ) -> Text:
90
+ if RunResult.error in states:
91
+ return Text(hook.id, style="red")
92
+ if any(isinstance(state, asyncio.Task) for state in states):
93
+ return Text(hook.id)
94
+ if RunResult.ok in states:
95
+ return Text(hook.id, style="green")
96
+ return Text(hook.id, style="dim")
97
+
98
+
99
+ def generate_live_process_table(scheduler: Scheduler) -> Iterator[Panel]:
100
+ spinner = Table.grid()
101
+ spinner.add_row(
102
+ "[blue][[/blue]",
103
+ Spinner("dots4", style="blue"),
104
+ "[blue]][/blue]",
105
+ )
106
+
107
+ while True:
108
+ hooks_table = Table(
109
+ show_header=False,
110
+ show_edge=False,
111
+ )
112
+ hooks_table.add_column("Hook")
113
+ hooks_table.add_column("Processes")
114
+ for hook, hook_units in scheduler.state().items():
115
+ process_table = Table.grid(padding=2)
116
+ process_table.add_row(
117
+ *(
118
+ format_unit_state(unit_state, spinner)
119
+ for unit, unit_state in hook_units.items()
120
+ )
121
+ )
122
+ hooks_table.add_row(
123
+ format_hook_name(hook, hook_units.values()),
124
+ process_table,
125
+ )
126
+ yield Panel(
127
+ hooks_table,
128
+ title="Running hooks",
129
+ border_style="magenta",
130
+ )
131
+
132
+
133
+ async def display_live_table(scheduler: Scheduler) -> None:
134
+ live_table = generate_live_process_table(scheduler)
135
+
136
+ with Live(refresh_per_second=10) as live:
137
+ live.update(next(live_table), refresh=True)
138
+ async for _ in scheduler.until_complete():
139
+ live.update(next(live_table), refresh=True)
140
+
141
+
142
+ def print_summary(console: Console, scheduler: Scheduler) -> None:
143
+ any_error = False
144
+ any_modified = False
145
+ for units in scheduler.state().values():
146
+ for unit_state in units.values():
147
+ if unit_state is RunResult.error:
148
+ any_error = True
149
+ elif unit_state is RunResult.modified:
150
+ any_modified = True
151
+ if any_error and any_modified:
152
+ break
153
+
154
+ if any_error:
155
+ console.print("Some hooks errored.", style="red")
156
+ if any_modified:
157
+ console.print("Some hooks made changes.", style="red")
158
+ if any_error or any_modified:
159
+ sys.exit(1)
160
+
161
+ console.print("All ok!", style="green")
162
+
163
+
164
+ @cli.command()
165
+ @asyncio_entrypoint
166
+ async def run(
167
+ selected_hook: Optional[str] = typer.Argument(default=None), # noqa
168
+ config_path: ConfigOption = default_config,
169
+ delete_orphan_environments: bool = False,
170
+ select: Selector = typer.Option(default="diff"),
171
+ ) -> None:
172
+ console = Console(stderr=True)
173
+ ctx = gather_context(config_path)
174
+
175
+ probe_orphan_environments(ctx, delete=delete_orphan_environments)
176
+
177
+ prepare_tasks = [
178
+ asyncio.create_task(prepare_environment(environment))
179
+ for environment in ctx.environments.values()
180
+ ]
181
+ done, pending = await asyncio.wait(
182
+ prepare_tasks,
183
+ return_when=asyncio.FIRST_EXCEPTION,
184
+ )
185
+ for task in done:
186
+ try:
187
+ task.result()
188
+ except NeedsFreeze:
189
+ console.print(
190
+ "Missing lock files, run `goose upgrade` first.",
191
+ style="red",
192
+ )
193
+ sys.exit(1)
194
+
195
+ print("All environments ready", file=sys.stderr)
196
+
197
+ scheduler = Scheduler(
198
+ context=ctx,
199
+ targets=await get_targets(ctx.config, select),
200
+ selected_hook=selected_hook,
201
+ )
202
+
203
+ if sys.stdout.isatty():
204
+ await display_live_table(scheduler)
205
+ else:
206
+ async for event in scheduler.until_complete():
207
+ t = time.time_ns()
208
+ if isinstance(event, UnitScheduled):
209
+ print(
210
+ f"[{event.unit.hook.id}@{event.unit.id}] [{t}] Unit scheduled",
211
+ file=sys.stderr,
212
+ )
213
+ elif isinstance(event, UnitFinished):
214
+ print(
215
+ f"[{event.unit.hook.id}@{event.unit.id}] [{t}] Unit finished: {event.result.name}",
216
+ file=sys.stderr,
217
+ )
218
+ else:
219
+ assert_never(event)
220
+
221
+ print_summary(console, scheduler)
222
+
223
+
224
+ template = """\
225
+ #!/usr/bin/env bash
226
+ set -euo pipefail
227
+ python -m goose run '--config={config_path}'
228
+ """
229
+
230
+
231
+ class GitHookType(enum.Enum):
232
+ pre_commit = "pre-commit"
233
+ pre_push = "pre-push"
234
+
235
+
236
+ @cli.command()
237
+ @asyncio_entrypoint
238
+ async def git_hook(
239
+ hook: GitHookType,
240
+ config_path: ConfigOption = default_config,
241
+ ) -> None:
242
+ hooks_path = Path(".git/hooks")
243
+ assert hooks_path.exists()
244
+ assert hooks_path.is_dir()
245
+ hook_path = hooks_path / hook.value
246
+ hook_path.write_text(template.format(config_path=str(config_path)))
247
+ hook_path.chmod(0o755)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ cli()
File without changes
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ import pydantic
4
+
5
+
6
+ class BaseModel(
7
+ pydantic.BaseModel,
8
+ frozen=True,
9
+ extra="forbid",
10
+ ): ...
goose/asyncio.py ADDED
@@ -0,0 +1,19 @@
1
+ import asyncio
2
+ import functools
3
+ from collections.abc import Callable
4
+ from collections.abc import Coroutine
5
+ from typing import ParamSpec
6
+ from typing import TypeVar
7
+
8
+ P = ParamSpec("P")
9
+ R = TypeVar("R")
10
+
11
+
12
+ def asyncio_entrypoint(
13
+ fn: Callable[P, Coroutine[None, None, R]],
14
+ ) -> Callable[P, R]:
15
+ @functools.wraps(fn)
16
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
17
+ return asyncio.run(fn(*args, **kwargs))
18
+
19
+ return wrapper
File without changes
goose/backend/base.py ADDED
@@ -0,0 +1,76 @@
1
+ import enum
2
+ from collections.abc import Awaitable
3
+ from collections.abc import Coroutine
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+ from typing import final
8
+
9
+ from goose.config import EnvironmentConfig
10
+ from goose.executable_unit import ExecutableUnit
11
+
12
+
13
+ class Bootstrap(Protocol):
14
+ def __call__(
15
+ self,
16
+ *,
17
+ config: EnvironmentConfig,
18
+ env_path: Path,
19
+ ) -> Awaitable[None]: ...
20
+
21
+
22
+ class Freeze(Protocol):
23
+ def __call__(
24
+ self,
25
+ *,
26
+ config: EnvironmentConfig,
27
+ env_path: Path,
28
+ lock_files_path: Path,
29
+ ) -> Awaitable[None]: ...
30
+
31
+
32
+ class Sync(Protocol):
33
+ def __call__(
34
+ self,
35
+ *,
36
+ config: EnvironmentConfig,
37
+ env_path: Path,
38
+ lock_files_path: Path,
39
+ ) -> Awaitable[None]: ...
40
+
41
+
42
+ class RunResult(enum.Enum):
43
+ ok = enum.auto()
44
+ error = enum.auto()
45
+ modified = enum.auto()
46
+
47
+
48
+ class Run(Protocol):
49
+ def __call__(
50
+ self,
51
+ *,
52
+ config: EnvironmentConfig,
53
+ env_path: Path,
54
+ unit: ExecutableUnit,
55
+ ) -> Coroutine[None, None, RunResult]: ...
56
+
57
+
58
+ @final
59
+ @dataclass(frozen=True, slots=True, kw_only=True)
60
+ class Backend:
61
+ ecosystem: str
62
+ bootstrap: Bootstrap
63
+ """
64
+ - Create environment if it does not exist.
65
+ - Install basic dependencies to enable freezing.
66
+ """
67
+ freeze: Freeze
68
+ """
69
+ - Update lock file.
70
+ """
71
+ sync: Sync
72
+ """
73
+ - Install missing dependencies.
74
+ - Uninstall obsolete dependencies.
75
+ """
76
+ run: Run
goose/backend/index.py ADDED
@@ -0,0 +1,20 @@
1
+ from collections.abc import Mapping
2
+ from types import MappingProxyType
3
+ from typing import Final
4
+
5
+ from goose.config import EcosystemConfig
6
+
7
+ from . import node
8
+ from . import python
9
+ from .base import Backend
10
+
11
+ backends: Final[Mapping[str, Backend]] = MappingProxyType(
12
+ {
13
+ "node": node.backend,
14
+ "python": python.backend,
15
+ }
16
+ )
17
+
18
+
19
+ def load_backend(ecosystem: EcosystemConfig) -> Backend:
20
+ return backends[ecosystem.language]