bakefile 0.0.5__py3-none-any.whl → 0.0.7__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.
- bake/__init__.py +2 -2
- bake/cli/bakefile/export.py +212 -0
- bake/cli/bakefile/main.py +2 -0
- bake/cli/common/app.py +1 -1
- bake/cli/common/context.py +33 -33
- bake/cli/common/obj.py +3 -1
- bake/manage/write_bakefile.py +22 -3
- bake/samples/simple.py +1 -2
- bake/ui/__init__.py +2 -1
- bake/ui/params.py +5 -0
- bake/ui/run/__init__.py +2 -8
- bake/ui/run/run.py +60 -55
- bake/ui/run/splitter.py +13 -1
- {bakefile-0.0.5.dist-info → bakefile-0.0.7.dist-info}/METADATA +2 -1
- {bakefile-0.0.5.dist-info → bakefile-0.0.7.dist-info}/RECORD +20 -18
- {bakefile-0.0.5.dist-info → bakefile-0.0.7.dist-info}/WHEEL +2 -2
- bakelib/space/base.py +135 -15
- bakelib/space/python.py +67 -29
- bakelib/space/utils.py +63 -0
- {bakefile-0.0.5.dist-info → bakefile-0.0.7.dist-info}/entry_points.txt +0 -0
bake/__init__.py
CHANGED
|
@@ -2,8 +2,8 @@ from bake.bakebook.bakebook import Bakebook
|
|
|
2
2
|
from bake.bakebook.decorator import command
|
|
3
3
|
from bake.cli.common.context import BakeCommand, Context
|
|
4
4
|
from bake.cli.utils.version import _get_version
|
|
5
|
-
from bake.ui import console
|
|
5
|
+
from bake.ui import console, params
|
|
6
6
|
|
|
7
7
|
__version__ = _get_version()
|
|
8
8
|
|
|
9
|
-
__all__ = ["BakeCommand", "Bakebook", "Context", "__version__", "command", "console"]
|
|
9
|
+
__all__ = ["BakeCommand", "Bakebook", "Context", "__version__", "command", "console", "params"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from collections.abc import Callable, Hashable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Any, Literal
|
|
5
|
+
|
|
6
|
+
import orjson
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
from bake.cli.common.context import Context
|
|
12
|
+
from bake.ui import console
|
|
13
|
+
|
|
14
|
+
ExportFormat = Literal["sh", "dotenv", "json", "yaml"]
|
|
15
|
+
JsonValue = str | float | bool | None | list[Any] | dict[Hashable, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _format_shell_value(value: JsonValue) -> str:
|
|
19
|
+
"""Format a value for shell export.
|
|
20
|
+
|
|
21
|
+
Expects JSON-serializable types (str, int, float, bool, None, list, dict).
|
|
22
|
+
Raises TypeError for unexpected types.
|
|
23
|
+
|
|
24
|
+
SecretStr values are masked for security.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
value : Any
|
|
29
|
+
The value to format for shell export
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
str
|
|
34
|
+
Shell-formatted string ready for export
|
|
35
|
+
|
|
36
|
+
Raises
|
|
37
|
+
------
|
|
38
|
+
TypeError
|
|
39
|
+
If value is not one of the expected types
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if isinstance(value, (list, dict)):
|
|
43
|
+
# Complex types: JSON string, then shell-quote it
|
|
44
|
+
return shlex.quote(orjson.dumps(value).decode())
|
|
45
|
+
elif isinstance(value, str):
|
|
46
|
+
# Strings: shell-quote directly
|
|
47
|
+
return shlex.quote(value)
|
|
48
|
+
elif value is None:
|
|
49
|
+
# None becomes empty string
|
|
50
|
+
return ""
|
|
51
|
+
elif isinstance(value, bool):
|
|
52
|
+
# Booleans: lowercase true/false for shell compatibility
|
|
53
|
+
return str(value).lower()
|
|
54
|
+
elif isinstance(value, (int, float)):
|
|
55
|
+
# Numbers: convert to string, no quoting needed
|
|
56
|
+
return str(value)
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"Unexpected type for shell export: {type(value).__name__}. "
|
|
59
|
+
f"Expected one of: str, int, float, bool, None, list, dict"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_dotenv_value(value: JsonValue) -> str:
|
|
64
|
+
"""Format a value for dotenv export.
|
|
65
|
+
|
|
66
|
+
Uses smart quote selection to produce valid dotenv format that
|
|
67
|
+
python-dotenv's parser can handle.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
value : JsonValue
|
|
72
|
+
The value to format for dotenv export
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
str
|
|
77
|
+
Dotenv-formatted string ready for export
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
TypeError
|
|
82
|
+
If value is not one of the expected types
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(value, (list, dict)):
|
|
85
|
+
# Complex types: JSON string, then wrap in double quotes
|
|
86
|
+
json_str = orjson.dumps(value).decode()
|
|
87
|
+
return '"' + json_str.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
88
|
+
elif isinstance(value, str):
|
|
89
|
+
# Strings: use smart quote selection
|
|
90
|
+
if value.isalnum():
|
|
91
|
+
return value
|
|
92
|
+
if "'" in value and '"' not in value:
|
|
93
|
+
# Has single quotes only: use double quotes
|
|
94
|
+
return f'"{value}"'
|
|
95
|
+
if '"' in value and "'" not in value:
|
|
96
|
+
# Has double quotes only: use single quotes
|
|
97
|
+
return f"'{value}'"
|
|
98
|
+
# Has both or special chars: use double quotes with escaping
|
|
99
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
100
|
+
elif value is None:
|
|
101
|
+
return ""
|
|
102
|
+
elif isinstance(value, bool):
|
|
103
|
+
return str(value).lower()
|
|
104
|
+
elif isinstance(value, (int, float)):
|
|
105
|
+
return str(value)
|
|
106
|
+
raise TypeError(
|
|
107
|
+
f"Unexpected type for dotenv export: {type(value).__name__}. "
|
|
108
|
+
f"Expected one of: str, int, float, bool, None, list, dict"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _format_vars(data: dict, value_formatter: Callable[[JsonValue], str], prefix: str = "") -> str:
|
|
113
|
+
lines: list[str] = []
|
|
114
|
+
for field_name, value in data.items():
|
|
115
|
+
formatted_val = value_formatter(value)
|
|
116
|
+
lines.append(f"{prefix}{field_name.upper()}={formatted_val}")
|
|
117
|
+
return "\n".join(lines)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ExportFormatter:
|
|
121
|
+
def __call__(self, data: dict[str, Any]) -> str:
|
|
122
|
+
raise NotImplementedError("....")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ShExportFormatter(ExportFormatter):
|
|
126
|
+
def __call__(self, data: dict[str, Any]) -> str:
|
|
127
|
+
return _format_vars(data, value_formatter=_format_shell_value, prefix="export ")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class DotEnvExportFormatter(ExportFormatter):
|
|
131
|
+
def __call__(self, data: dict[str, Any]) -> str:
|
|
132
|
+
return _format_vars(data, value_formatter=_format_dotenv_value, prefix="")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class JsonExportFormatter(ExportFormatter):
|
|
136
|
+
def __call__(self, data: dict[str, Any]) -> str:
|
|
137
|
+
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class YamlExportFormatter(ExportFormatter):
|
|
141
|
+
def __call__(self, data: dict[str, Any]) -> str:
|
|
142
|
+
return yaml.dump(data, default_flow_style=False, sort_keys=False)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _export(
|
|
146
|
+
bakebook: BaseSettings,
|
|
147
|
+
format: ExportFormat = "sh",
|
|
148
|
+
output: Path | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
formatters: dict[str, ExportFormatter] = {
|
|
151
|
+
"sh": ShExportFormatter(),
|
|
152
|
+
"dotenv": DotEnvExportFormatter(),
|
|
153
|
+
"json": JsonExportFormatter(),
|
|
154
|
+
"yaml": YamlExportFormatter(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
formatter = formatters.get(format)
|
|
158
|
+
if formatter is None:
|
|
159
|
+
raise ValueError(f"Unknown format: {format}")
|
|
160
|
+
|
|
161
|
+
data: dict[str, Any] = bakebook.model_dump(mode="json")
|
|
162
|
+
content = formatter(data)
|
|
163
|
+
|
|
164
|
+
if output:
|
|
165
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
output.write_text(content, encoding="utf-8")
|
|
167
|
+
elif content != "":
|
|
168
|
+
console.echo(content, overflow="ignore", crop=False)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def export(
|
|
172
|
+
ctx: Context,
|
|
173
|
+
format: Annotated[
|
|
174
|
+
ExportFormat,
|
|
175
|
+
typer.Option(
|
|
176
|
+
"--format",
|
|
177
|
+
"-f",
|
|
178
|
+
help="Output format",
|
|
179
|
+
),
|
|
180
|
+
] = "sh",
|
|
181
|
+
output: Annotated[
|
|
182
|
+
Path | None,
|
|
183
|
+
typer.Option(
|
|
184
|
+
"--output",
|
|
185
|
+
"-o",
|
|
186
|
+
help="Output file path (default: stdout)",
|
|
187
|
+
exists=False,
|
|
188
|
+
),
|
|
189
|
+
] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Export bakebook args to external formats.
|
|
192
|
+
|
|
193
|
+
Export Pydantic-validated bakebook args to various formats for use
|
|
194
|
+
outside Python runtime (shell scripts, GitHub Actions, .env files, etc.).
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
# Export to shell for eval
|
|
198
|
+
bakefile export --format sh
|
|
199
|
+
|
|
200
|
+
# Export to dotenv file
|
|
201
|
+
bakefile export --format dotenv --output .env
|
|
202
|
+
|
|
203
|
+
# Export to JSON
|
|
204
|
+
bakefile export --format json --output config.json
|
|
205
|
+
"""
|
|
206
|
+
if ctx.obj.bakebook is None:
|
|
207
|
+
ctx.obj.get_bakebook(allow_missing=False)
|
|
208
|
+
|
|
209
|
+
if ctx.obj.bakebook is None:
|
|
210
|
+
raise RuntimeError("Bakebook not found.")
|
|
211
|
+
|
|
212
|
+
_export(bakebook=ctx.obj.bakebook, format=format, output=output)
|
bake/cli/bakefile/main.py
CHANGED
|
@@ -8,6 +8,7 @@ from bake.cli.common.obj import get_bakefile_object
|
|
|
8
8
|
|
|
9
9
|
from . import uv
|
|
10
10
|
from .add_inline import add_inline
|
|
11
|
+
from .export import export
|
|
11
12
|
from .find_python import find_python
|
|
12
13
|
from .init import init
|
|
13
14
|
from .lint import lint
|
|
@@ -33,6 +34,7 @@ def main():
|
|
|
33
34
|
bakefile_app.command()(add_inline)
|
|
34
35
|
bakefile_app.command()(find_python)
|
|
35
36
|
bakefile_app.command()(lint)
|
|
37
|
+
bakefile_app.command()(export)
|
|
36
38
|
bakefile_app.command(context_settings=uv_commands_context_settings)(uv.sync)
|
|
37
39
|
bakefile_app.command(context_settings=uv_commands_context_settings)(uv.lock)
|
|
38
40
|
bakefile_app.command(context_settings=uv_commands_context_settings)(uv.add)
|
bake/cli/common/app.py
CHANGED
bake/cli/common/context.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import subprocess
|
|
2
|
+
from collections.abc import Generator
|
|
3
|
+
from contextlib import contextmanager
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import TYPE_CHECKING, Literal, overload
|
|
4
6
|
|
|
@@ -6,6 +8,7 @@ import click
|
|
|
6
8
|
import typer
|
|
7
9
|
from typer.core import TyperCommand
|
|
8
10
|
|
|
11
|
+
from bake.ui.run import CmdType
|
|
9
12
|
from bake.ui.run import run as _run
|
|
10
13
|
from bake.ui.run.script import run_script as _run_script
|
|
11
14
|
|
|
@@ -22,6 +25,19 @@ class Context(typer.Context):
|
|
|
22
25
|
def dry_run(self) -> bool:
|
|
23
26
|
return self.obj.dry_run
|
|
24
27
|
|
|
28
|
+
@dry_run.setter
|
|
29
|
+
def dry_run(self, value: bool) -> None:
|
|
30
|
+
self.obj.dry_run = value
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def override_dry_run(self, dry_run: bool) -> Generator[None, None, None]:
|
|
34
|
+
original = self.obj.dry_run
|
|
35
|
+
self.obj.dry_run = dry_run
|
|
36
|
+
try:
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
self.obj.dry_run = original
|
|
40
|
+
|
|
25
41
|
@property
|
|
26
42
|
def verbosity(self) -> int:
|
|
27
43
|
return self.obj.verbosity
|
|
@@ -33,37 +49,7 @@ class Context(typer.Context):
|
|
|
33
49
|
@overload
|
|
34
50
|
def run(
|
|
35
51
|
self,
|
|
36
|
-
cmd:
|
|
37
|
-
*,
|
|
38
|
-
capture_output: Literal[True] = True,
|
|
39
|
-
check: bool = True,
|
|
40
|
-
cwd: Path | str | None = None,
|
|
41
|
-
stream: bool = True,
|
|
42
|
-
shell: bool | None = None,
|
|
43
|
-
echo: bool = True,
|
|
44
|
-
dry_run: bool | None = None,
|
|
45
|
-
**kwargs,
|
|
46
|
-
) -> subprocess.CompletedProcess[str]: ...
|
|
47
|
-
|
|
48
|
-
@overload
|
|
49
|
-
def run(
|
|
50
|
-
self,
|
|
51
|
-
cmd: str,
|
|
52
|
-
*,
|
|
53
|
-
capture_output: Literal[False],
|
|
54
|
-
check: bool = True,
|
|
55
|
-
cwd: Path | str | None = None,
|
|
56
|
-
stream: bool = True,
|
|
57
|
-
shell: bool | None = None,
|
|
58
|
-
echo: bool = True,
|
|
59
|
-
dry_run: bool | None = None,
|
|
60
|
-
**kwargs,
|
|
61
|
-
) -> subprocess.CompletedProcess[None]: ...
|
|
62
|
-
|
|
63
|
-
@overload
|
|
64
|
-
def run(
|
|
65
|
-
self,
|
|
66
|
-
cmd: list[str] | tuple[str, ...],
|
|
52
|
+
cmd: CmdType,
|
|
67
53
|
*,
|
|
68
54
|
capture_output: Literal[True] = True,
|
|
69
55
|
check: bool = True,
|
|
@@ -72,13 +58,15 @@ class Context(typer.Context):
|
|
|
72
58
|
shell: bool | None = None,
|
|
73
59
|
echo: bool = True,
|
|
74
60
|
dry_run: bool | None = None,
|
|
61
|
+
keep_temp_file: bool = False,
|
|
62
|
+
env: dict[str, str] | None = None,
|
|
75
63
|
**kwargs,
|
|
76
64
|
) -> subprocess.CompletedProcess[str]: ...
|
|
77
65
|
|
|
78
66
|
@overload
|
|
79
67
|
def run(
|
|
80
68
|
self,
|
|
81
|
-
cmd:
|
|
69
|
+
cmd: CmdType,
|
|
82
70
|
*,
|
|
83
71
|
capture_output: Literal[False],
|
|
84
72
|
check: bool = True,
|
|
@@ -87,12 +75,14 @@ class Context(typer.Context):
|
|
|
87
75
|
shell: bool | None = None,
|
|
88
76
|
echo: bool = True,
|
|
89
77
|
dry_run: bool | None = None,
|
|
78
|
+
keep_temp_file: bool = False,
|
|
79
|
+
env: dict[str, str] | None = None,
|
|
90
80
|
**kwargs,
|
|
91
81
|
) -> subprocess.CompletedProcess[None]: ...
|
|
92
82
|
|
|
93
83
|
def run(
|
|
94
84
|
self,
|
|
95
|
-
cmd:
|
|
85
|
+
cmd: CmdType,
|
|
96
86
|
*,
|
|
97
87
|
capture_output: bool = True,
|
|
98
88
|
check: bool = True,
|
|
@@ -101,6 +91,9 @@ class Context(typer.Context):
|
|
|
101
91
|
shell: bool | None = None,
|
|
102
92
|
echo: bool = True,
|
|
103
93
|
dry_run: bool | None = None,
|
|
94
|
+
keep_temp_file: bool = False,
|
|
95
|
+
env: dict[str, str] | None = None,
|
|
96
|
+
_encoding: str | None = None,
|
|
104
97
|
**kwargs,
|
|
105
98
|
) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
|
|
106
99
|
return _run(
|
|
@@ -112,6 +105,9 @@ class Context(typer.Context):
|
|
|
112
105
|
shell=shell,
|
|
113
106
|
echo=echo,
|
|
114
107
|
dry_run=self.obj.dry_run if dry_run is None else dry_run,
|
|
108
|
+
keep_temp_file=keep_temp_file,
|
|
109
|
+
env=env,
|
|
110
|
+
_encoding=_encoding,
|
|
115
111
|
**kwargs,
|
|
116
112
|
)
|
|
117
113
|
|
|
@@ -126,6 +122,8 @@ class Context(typer.Context):
|
|
|
126
122
|
stream: bool = True,
|
|
127
123
|
echo: bool = True,
|
|
128
124
|
dry_run: bool | None = None,
|
|
125
|
+
keep_temp_file: bool = False,
|
|
126
|
+
env: dict[str, str] | None = None,
|
|
129
127
|
**kwargs,
|
|
130
128
|
) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
|
|
131
129
|
return _run_script(
|
|
@@ -137,6 +135,8 @@ class Context(typer.Context):
|
|
|
137
135
|
stream=stream,
|
|
138
136
|
echo=echo,
|
|
139
137
|
dry_run=self.obj.dry_run if dry_run is None else dry_run,
|
|
138
|
+
keep_temp_file=keep_temp_file,
|
|
139
|
+
env=env,
|
|
140
140
|
**kwargs,
|
|
141
141
|
)
|
|
142
142
|
|
bake/cli/common/obj.py
CHANGED
|
@@ -81,9 +81,11 @@ class BakefileObject:
|
|
|
81
81
|
self.bakebook = get_bakebook_from_target_dir_path(
|
|
82
82
|
target_dir_path=self.bakefile_path, bakebook_name=self.bakebook_name
|
|
83
83
|
)
|
|
84
|
-
except BakefileNotFoundError:
|
|
84
|
+
except BakefileNotFoundError as e:
|
|
85
85
|
if allow_missing:
|
|
86
86
|
return
|
|
87
|
+
console.error(str(e))
|
|
88
|
+
raise SystemExit(1) from e
|
|
87
89
|
except BakebookError as e:
|
|
88
90
|
if allow_missing:
|
|
89
91
|
return
|
bake/manage/write_bakefile.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import types
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
+
from bake.samples import simple
|
|
4
5
|
from bake.utils.constants import BAKEBOOK_NAME_IN_SAMPLES
|
|
5
6
|
|
|
7
|
+
# Allowed sample modules
|
|
8
|
+
# This dictionary acts as a whitelist for security - only these modules can be used
|
|
9
|
+
ALLOWED_SAMPLE_MODULES: dict[str, types.ModuleType] = {
|
|
10
|
+
simple.__name__: simple,
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
|
|
7
14
|
def write_bakefile(
|
|
8
15
|
bakefile_path: Path, bakebook_name: str, sample_module: types.ModuleType
|
|
@@ -12,9 +19,21 @@ def write_bakefile(
|
|
|
12
19
|
f"Module `{sample_module.__name__}` must have `{BAKEBOOK_NAME_IN_SAMPLES}` attribute"
|
|
13
20
|
)
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
module_name = sample_module.__name__
|
|
23
|
+
if module_name not in ALLOWED_SAMPLE_MODULES:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Module `{module_name}` is not in the allowed sample modules list. "
|
|
26
|
+
f"Allowed modules: {list(ALLOWED_SAMPLE_MODULES.keys())}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
allowed_module = ALLOWED_SAMPLE_MODULES[module_name]
|
|
30
|
+
if sample_module is not allowed_module:
|
|
31
|
+
raise ValueError(f"Module `{module_name}` does not match the allowed module object")
|
|
32
|
+
|
|
33
|
+
if allowed_module.__file__ is None:
|
|
34
|
+
raise ValueError(f"Could not find file for module `{module_name}`")
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
source_file_path = Path(allowed_module.__file__)
|
|
37
|
+
original_bakefile_content = source_file_path.read_text()
|
|
19
38
|
customized_content = original_bakefile_content.replace(BAKEBOOK_NAME_IN_SAMPLES, bakebook_name)
|
|
20
39
|
bakefile_path.write_text(customized_content)
|
bake/samples/simple.py
CHANGED
bake/ui/__init__.py
CHANGED
bake/ui/params.py
ADDED
bake/ui/run/__init__.py
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Provides functions for running commands with real-time output streaming
|
|
4
|
-
and capture capabilities.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from bake.ui.run.run import OutputSplitter, run
|
|
1
|
+
from bake.ui.run.run import CmdType, OutputSplitter, run
|
|
8
2
|
from bake.ui.run.script import run_script
|
|
9
3
|
from bake.ui.run.uv import run_uv
|
|
10
4
|
|
|
11
|
-
__all__ = ["OutputSplitter", "run", "run_script", "run_uv"]
|
|
5
|
+
__all__ = ["CmdType", "OutputSplitter", "run", "run_script", "run_uv"]
|
bake/ui/run/run.py
CHANGED
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Literal, overload
|
|
11
12
|
|
|
@@ -25,6 +26,13 @@ logger = logging.getLogger(__name__)
|
|
|
25
26
|
_subprocess_create_lock = threading.Lock()
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class StreamSetup:
|
|
31
|
+
proc: subprocess.Popen
|
|
32
|
+
splitter: OutputSplitter
|
|
33
|
+
threads: list
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def _parse_shebang(script: str) -> str | None:
|
|
29
37
|
"""Parse shebang line, return interpreter path or None."""
|
|
30
38
|
lines = script.strip().splitlines()
|
|
@@ -285,26 +293,17 @@ def run(
|
|
|
285
293
|
logger.debug(f"[run] {cmd_str}", extra={"cwd": cwd})
|
|
286
294
|
start = time.perf_counter()
|
|
287
295
|
|
|
288
|
-
if stream
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
result = _run_without_stream(
|
|
300
|
-
cmd=cmd,
|
|
301
|
-
shell=shell,
|
|
302
|
-
cwd=cwd,
|
|
303
|
-
capture_output=capture_output,
|
|
304
|
-
env=env,
|
|
305
|
-
_encoding=_encoding,
|
|
306
|
-
**kwargs,
|
|
307
|
-
)
|
|
296
|
+
_run = _run_with_stream if stream else _run_without_stream
|
|
297
|
+
|
|
298
|
+
result = _run(
|
|
299
|
+
cmd=cmd,
|
|
300
|
+
shell=shell,
|
|
301
|
+
cwd=cwd,
|
|
302
|
+
capture_output=capture_output,
|
|
303
|
+
env=env,
|
|
304
|
+
_encoding=_encoding,
|
|
305
|
+
**kwargs,
|
|
306
|
+
)
|
|
308
307
|
|
|
309
308
|
_check_exit_code(returncode=result.returncode, check=check, cmd_str=cmd_str)
|
|
310
309
|
|
|
@@ -398,27 +397,41 @@ def _setup_pty_stream(
|
|
|
398
397
|
env: dict[str, str] | None = None,
|
|
399
398
|
_encoding: str | None = None,
|
|
400
399
|
**kwargs,
|
|
401
|
-
) ->
|
|
400
|
+
) -> StreamSetup:
|
|
402
401
|
# subprocess.Popen is not thread-safe, protect with lock
|
|
403
402
|
# See: https://bugs.python.org/issue2320
|
|
404
403
|
with _subprocess_create_lock:
|
|
405
|
-
stdout_fd,
|
|
404
|
+
stdout_fd, slave_stdout = pty.openpty()
|
|
405
|
+
|
|
406
|
+
# Always create stderr PTY when streaming to ensure output goes through
|
|
407
|
+
# our thread which writes to sys.stderr (allows pytest to capture it)
|
|
408
|
+
stderr_fd, slave_stderr = pty.openpty()
|
|
409
|
+
|
|
406
410
|
env = _prepare_subprocess_env(env)
|
|
407
411
|
proc = subprocess.Popen(
|
|
408
412
|
cmd,
|
|
409
413
|
cwd=cwd,
|
|
410
|
-
stdout=
|
|
411
|
-
stderr=
|
|
414
|
+
stdout=slave_stdout,
|
|
415
|
+
stderr=slave_stderr,
|
|
412
416
|
shell=shell,
|
|
413
417
|
env=env,
|
|
414
418
|
**kwargs,
|
|
415
419
|
)
|
|
416
|
-
os.close(
|
|
420
|
+
os.close(slave_stdout)
|
|
421
|
+
os.close(slave_stderr)
|
|
417
422
|
|
|
423
|
+
# Attach threads BEFORE releasing lock to ensure reader is ready
|
|
424
|
+
# when fast-exiting processes complete
|
|
418
425
|
splitter = OutputSplitter(
|
|
419
|
-
stream=True,
|
|
426
|
+
stream=True,
|
|
427
|
+
capture=capture_output,
|
|
428
|
+
pty_fd=stdout_fd,
|
|
429
|
+
stderr_pty_fd=stderr_fd,
|
|
430
|
+
encoding=_encoding,
|
|
420
431
|
)
|
|
421
|
-
|
|
432
|
+
threads = splitter.attach(proc)
|
|
433
|
+
|
|
434
|
+
return StreamSetup(proc=proc, splitter=splitter, threads=threads)
|
|
422
435
|
|
|
423
436
|
|
|
424
437
|
def _setup_pipe_stream(
|
|
@@ -429,7 +442,7 @@ def _setup_pipe_stream(
|
|
|
429
442
|
env: dict[str, str] | None = None,
|
|
430
443
|
_encoding: str | None = None,
|
|
431
444
|
**kwargs,
|
|
432
|
-
) ->
|
|
445
|
+
) -> StreamSetup:
|
|
433
446
|
# subprocess.Popen is not thread-safe, protect with lock
|
|
434
447
|
# See: https://bugs.python.org/issue2320
|
|
435
448
|
with _subprocess_create_lock:
|
|
@@ -448,7 +461,7 @@ def _setup_pipe_stream(
|
|
|
448
461
|
splitter = OutputSplitter(stream=True, capture=capture_output, encoding=_encoding)
|
|
449
462
|
threads = splitter.attach(proc)
|
|
450
463
|
|
|
451
|
-
return proc, splitter, threads
|
|
464
|
+
return StreamSetup(proc=proc, splitter=splitter, threads=threads)
|
|
452
465
|
|
|
453
466
|
|
|
454
467
|
def _run_with_stream(
|
|
@@ -462,32 +475,22 @@ def _run_with_stream(
|
|
|
462
475
|
) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
|
|
463
476
|
use_pty = sys.platform != "win32"
|
|
464
477
|
|
|
465
|
-
if use_pty
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
else:
|
|
477
|
-
proc, splitter, threads = _setup_pipe_stream(
|
|
478
|
-
cmd=cmd,
|
|
479
|
-
shell=shell,
|
|
480
|
-
cwd=cwd,
|
|
481
|
-
capture_output=capture_output,
|
|
482
|
-
env=env,
|
|
483
|
-
_encoding=_encoding,
|
|
484
|
-
**kwargs,
|
|
485
|
-
)
|
|
478
|
+
_setup = _setup_pty_stream if use_pty else _setup_pipe_stream
|
|
479
|
+
|
|
480
|
+
setup = _setup(
|
|
481
|
+
cmd=cmd,
|
|
482
|
+
shell=shell,
|
|
483
|
+
cwd=cwd,
|
|
484
|
+
capture_output=capture_output,
|
|
485
|
+
env=env,
|
|
486
|
+
_encoding=_encoding,
|
|
487
|
+
**kwargs,
|
|
488
|
+
)
|
|
486
489
|
|
|
487
|
-
proc.wait()
|
|
488
|
-
splitter.finalize(threads)
|
|
490
|
+
setup.proc.wait()
|
|
491
|
+
setup.splitter.finalize(setup.threads)
|
|
489
492
|
|
|
490
|
-
return _process_stream_output(splitter, proc, cmd, capture_output)
|
|
493
|
+
return _process_stream_output(setup.splitter, setup.proc, cmd, capture_output)
|
|
491
494
|
|
|
492
495
|
|
|
493
496
|
def _run_without_stream(
|
|
@@ -504,7 +507,7 @@ def _run_without_stream(
|
|
|
504
507
|
|
|
505
508
|
# Use specified encoding with errors="replace", or fall back to text=True (platform default)
|
|
506
509
|
if _encoding:
|
|
507
|
-
|
|
510
|
+
result = subprocess.run(
|
|
508
511
|
cmd,
|
|
509
512
|
cwd=cwd,
|
|
510
513
|
capture_output=capture_output,
|
|
@@ -516,7 +519,7 @@ def _run_without_stream(
|
|
|
516
519
|
**kwargs,
|
|
517
520
|
)
|
|
518
521
|
else:
|
|
519
|
-
|
|
522
|
+
result = subprocess.run(
|
|
520
523
|
cmd,
|
|
521
524
|
cwd=cwd,
|
|
522
525
|
capture_output=capture_output,
|
|
@@ -527,6 +530,8 @@ def _run_without_stream(
|
|
|
527
530
|
**kwargs,
|
|
528
531
|
)
|
|
529
532
|
|
|
533
|
+
return result
|
|
534
|
+
|
|
530
535
|
|
|
531
536
|
def _log_completion(cmd_str: str, result: subprocess.CompletedProcess, start: float) -> None:
|
|
532
537
|
elapsed_seconds = time.perf_counter() - start
|
bake/ui/run/splitter.py
CHANGED
|
@@ -15,11 +15,13 @@ class OutputSplitter:
|
|
|
15
15
|
stream: bool = True,
|
|
16
16
|
capture: bool = True,
|
|
17
17
|
pty_fd: int | None = None,
|
|
18
|
+
stderr_pty_fd: int | None = None,
|
|
18
19
|
encoding: str | None = None,
|
|
19
20
|
):
|
|
20
21
|
self._stream = stream
|
|
21
22
|
self._capture = capture
|
|
22
23
|
self._pty_fd = pty_fd
|
|
24
|
+
self._stderr_pty_fd = stderr_pty_fd
|
|
23
25
|
self._encoding = encoding
|
|
24
26
|
self._stdout_data = b""
|
|
25
27
|
self._stderr_data = b""
|
|
@@ -208,7 +210,17 @@ class OutputSplitter:
|
|
|
208
210
|
t.start()
|
|
209
211
|
threads.append((t, stdout_list, "stdout"))
|
|
210
212
|
|
|
211
|
-
# Handle stderr (
|
|
213
|
+
# Handle PTY stderr (for color-preserving stderr on Unix)
|
|
214
|
+
if self._stderr_pty_fd is not None:
|
|
215
|
+
stderr_list = []
|
|
216
|
+
t = threading.Thread(
|
|
217
|
+
target=self._read_pty, args=(self._stderr_pty_fd, sys.stderr, stderr_list, proc)
|
|
218
|
+
)
|
|
219
|
+
t.daemon = True
|
|
220
|
+
t.start()
|
|
221
|
+
threads.append((t, stderr_list, "stderr"))
|
|
222
|
+
|
|
223
|
+
# Handle stderr (regular pipe) - use separate if, not elif
|
|
212
224
|
if proc.stderr:
|
|
213
225
|
stderr_list = []
|
|
214
226
|
t = threading.Thread(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bakefile
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: Wisaroot Lertthaweedech
|
|
6
6
|
Author-email: Wisaroot Lertthaweedech <l.wisaroot@gmail.com>
|
|
@@ -10,6 +10,7 @@ Requires-Dist: loguru>=0.7.3
|
|
|
10
10
|
Requires-Dist: orjson>=3.11.5
|
|
11
11
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
12
12
|
Requires-Dist: pydantic>=2.12.5
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
13
14
|
Requires-Dist: rich>=14.2.0
|
|
14
15
|
Requires-Dist: ruff>=0.14.10
|
|
15
16
|
Requires-Dist: tomli>=2.0.0 ; python_full_version < '3.11'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
bake/__init__.py,sha256=
|
|
1
|
+
bake/__init__.py,sha256=ikK9AMD9ELxzcH3_JRZt3bnNLS0vCzlaH82gpGi_dNE,356
|
|
2
2
|
bake/bakebook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
bake/bakebook/bakebook.py,sha256=o1sp8Wewxdqr7k12qT-Q4ZrKQ7c7jDliiNCYrVG93NA,2939
|
|
4
4
|
bake/bakebook/decorator.py,sha256=t2I6Pf1vdTk7k8lTzZCqq71-hzcXFvFYbuFV_Wvg45Q,1545
|
|
@@ -11,17 +11,18 @@ bake/cli/bake/reinvocation.py,sha256=Ifqc8ZAM7NMyrKU2jkmw1agCEI-Sx7yvLLBiFRi23_0
|
|
|
11
11
|
bake/cli/bakefile/__init__.py,sha256=_zD3rXQHLr6EWHADdPLAmnc2A5C3dhmBuvP5uJ-_A58,60
|
|
12
12
|
bake/cli/bakefile/__main__.py,sha256=FVntzkZdzdygSWjMzyneXCXsM-MDTPmC3GUk4JZiYFU,137
|
|
13
13
|
bake/cli/bakefile/add_inline.py,sha256=V98T50SLMPqnWVtyEO_6hL17r4n3ZtkSC8NSEqdyHzc,919
|
|
14
|
+
bake/cli/bakefile/export.py,sha256=m9X0u6FgbjUzneQuh39H1CaFUT444jOPTFBNjnjs_Dg,6326
|
|
14
15
|
bake/cli/bakefile/find_python.py,sha256=J2HDs_nfNODqCHBZCNM64ESB4kVZK-C04i-KNmVUoSs,539
|
|
15
16
|
bake/cli/bakefile/init.py,sha256=0QuvADFOZZUBN2BUJfK90aEY1oUzoSNVRiljlUSjLu0,1825
|
|
16
17
|
bake/cli/bakefile/lint.py,sha256=DJkIJNBOef6JvgwQ3iL9jTrLqgUyn66Mhv6cuAgqXk0,2509
|
|
17
|
-
bake/cli/bakefile/main.py,sha256=
|
|
18
|
+
bake/cli/bakefile/main.py,sha256=jbpzNQa55thbzhpcmEtys1M1CvNUJBvi5UmgVzSbOM8,1414
|
|
18
19
|
bake/cli/bakefile/uv.py,sha256=PMFG3BdofzGWkor4fMEi3GE4G7hGtclCgPm2xlaPDso,4013
|
|
19
20
|
bake/cli/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
bake/cli/common/app.py,sha256=
|
|
21
|
+
bake/cli/common/app.py,sha256=_XzBOy0OK2GB14E-cyfLV6CBCn70G62QqjUscj_DHfA,1443
|
|
21
22
|
bake/cli/common/callback.py,sha256=NmrZUl5eRr95nluomTwcKjTU7dSKjWcQVli5VEdZk-4,439
|
|
22
|
-
bake/cli/common/context.py,sha256=
|
|
23
|
+
bake/cli/common/context.py,sha256=RtFHUDCZLcD88Ys17u_zXoHUq-12jkoXc9f_D4jh_7M,3871
|
|
23
24
|
bake/cli/common/exception_handler.py,sha256=2vLbqMeZlLxKqNWUkTs3cA-8l6IjK0dU3SyZlRb96YI,1759
|
|
24
|
-
bake/cli/common/obj.py,sha256=
|
|
25
|
+
bake/cli/common/obj.py,sha256=ShDsQtHCxex17IrKb9kSdC1t459qBsam53SFCUB_DSA,7094
|
|
25
26
|
bake/cli/common/params.py,sha256=rhLa34SY92nXfUaKo0SQMKK__xRnrmHejHa25tRyKdg,2002
|
|
26
27
|
bake/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
bake/cli/utils/version.py,sha256=aiweLD0vDezBlJAcCC99oMms71WGD9CWSJuZ4i3VLHA,390
|
|
@@ -30,20 +31,21 @@ bake/manage/add_inline.py,sha256=yefHmF33ghCB8NZ-v61ybeVsaeE8iDFvfRGeTAKg4I8,224
|
|
|
30
31
|
bake/manage/find_python.py,sha256=oVmd8KaSsgDQWHuGZpYiQx-DHn50P9EkRi6-YIad99E,7165
|
|
31
32
|
bake/manage/lint.py,sha256=OqwYFF8GGvzHGVPuJcWMRAv5esXEIX4nQXdGcChnkqA,2394
|
|
32
33
|
bake/manage/run_uv.py,sha256=QzlKeVpr20dXNDcwUgyJqnXT4MofRqK-6XkWpzBbUhE,3234
|
|
33
|
-
bake/manage/write_bakefile.py,sha256=
|
|
34
|
+
bake/manage/write_bakefile.py,sha256=ZlBL2XBy0XIY8_4t56szvq2c6-DwEyRuNljjZI3m0ls,1487
|
|
34
35
|
bake/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
36
|
bake/samples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
bake/samples/simple.py,sha256=
|
|
37
|
-
bake/ui/__init__.py,sha256=
|
|
37
|
+
bake/samples/simple.py,sha256=hP2TW-D7BQBGJseqRPpilxkoQ8ScTuZZePICupyvFKA,155
|
|
38
|
+
bake/ui/__init__.py,sha256=6OhZVKjfC9aumbhraxkGtx7KLpV1ouepeHRA2dUVoSo,209
|
|
38
39
|
bake/ui/console.py,sha256=C5wrbsOc-wwcx0hGmCozHvGCNTWgGhsh-5vxl880xS4,1689
|
|
39
40
|
bake/ui/logger/__init__.py,sha256=bup2cssTHhergh47s6uYbGtY2dJNxlKKH6otBc4ECFM,728
|
|
40
41
|
bake/ui/logger/capsys.py,sha256=KZL6k7Werp_8styfJKfIvQyv0-gJq54vY3hSJFIacEM,5267
|
|
41
42
|
bake/ui/logger/setup.py,sha256=OrX9UiY0iBGfWWfhMJCdfqCRJsL5yC3rIdIEOn7rveo,1377
|
|
42
43
|
bake/ui/logger/utils.py,sha256=dcppxoS_pX92AFcHIerJGI2_JBHBNghRQmQqlZmmj2Q,7218
|
|
43
|
-
bake/ui/
|
|
44
|
-
bake/ui/run/
|
|
44
|
+
bake/ui/params.py,sha256=yNDChJQkbeZSxQzXTSBrAPCbwsJ5zOK4s4sFHQPSnHs,140
|
|
45
|
+
bake/ui/run/__init__.py,sha256=A671l5YVTRAtS47ewvaMCNwPRim_Wkof1am0WibxA2I,205
|
|
46
|
+
bake/ui/run/run.py,sha256=qfDgy-YqcexJyHhSjnQ5IXipBDoK-umwKq-wAn8ZITU,17504
|
|
45
47
|
bake/ui/run/script.py,sha256=fk7KiDklYDYpFGkH3wu-hZGI4OnvgcB8z5jtNt41Hg0,2263
|
|
46
|
-
bake/ui/run/splitter.py,sha256=
|
|
48
|
+
bake/ui/run/splitter.py,sha256=sQt0prFGR6WCCMDr1wqk1GXBQmUSV9MNHzYzMu9Pwik,8643
|
|
47
49
|
bake/ui/run/uv.py,sha256=3NpnjgAwQNijJiUT_H6U-3mTHQgBZPlJbNWEeYCZY1g,2077
|
|
48
50
|
bake/ui/style.py,sha256=v9dferzV317Acb0GHpVK_niCj_s2HtL-yiToBZtXky4,70
|
|
49
51
|
bake/utils/__init__.py,sha256=GUu_xlJy3RAHo6UcZXu2x4khxGqLHMA9Zos4hDiQIY8,326
|
|
@@ -52,10 +54,10 @@ bake/utils/env.py,sha256=bzNdH_2bTJebQaw7D0uVJv-vzZ-uYl0pCAS8oQONVsA,190
|
|
|
52
54
|
bake/utils/exceptions.py,sha256=pwsQnKH5ljMNxmqEREutXa7TohiBHATHg_D5kQUPT30,519
|
|
53
55
|
bakelib/__init__.py,sha256=sZeRiNINWL8xI3b1MxkGyF3f2lKMjyhjKt7qyCCAufs,126
|
|
54
56
|
bakelib/space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
-
bakelib/space/base.py,sha256=
|
|
56
|
-
bakelib/space/python.py,sha256=
|
|
57
|
-
bakelib/space/utils.py,sha256=
|
|
58
|
-
bakefile-0.0.
|
|
59
|
-
bakefile-0.0.
|
|
60
|
-
bakefile-0.0.
|
|
61
|
-
bakefile-0.0.
|
|
57
|
+
bakelib/space/base.py,sha256=bJlpPkP85xBu8X5fJoVaHrMzX27rQrYTEPSObwBANJ8,6008
|
|
58
|
+
bakelib/space/python.py,sha256=UEr4Jo76T2cbAQdClVu7RvJYzflLE9i_xFohcNhjFjw,2597
|
|
59
|
+
bakelib/space/utils.py,sha256=xx4X_txhDH_p97CKJ-KuvFpgfNBC0y_din1IBlUVusU,2983
|
|
60
|
+
bakefile-0.0.7.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
61
|
+
bakefile-0.0.7.dist-info/entry_points.txt,sha256=Ecvvh7BYHCPJ0UdntrDc3Od6AZdRPXN5Z7o_7ok_0Qw,107
|
|
62
|
+
bakefile-0.0.7.dist-info/METADATA,sha256=zVQg9_SCgUUvIT_rosZIW8pf658SG3pDTfyp4I5qMek,2331
|
|
63
|
+
bakefile-0.0.7.dist-info/RECORD,,
|
bakelib/space/base.py
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
from
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, Literal
|
|
2
3
|
|
|
4
|
+
import orjson
|
|
3
5
|
import typer
|
|
4
6
|
|
|
5
7
|
from bake import Bakebook, Context, command
|
|
6
8
|
from bake.ui import console
|
|
7
9
|
|
|
8
|
-
from .utils import
|
|
10
|
+
from .utils import (
|
|
11
|
+
HOMWBREW_BIN,
|
|
12
|
+
LOCAL_BIN,
|
|
13
|
+
VENV_BIN,
|
|
14
|
+
PlatformType,
|
|
15
|
+
ToolInfo,
|
|
16
|
+
get_expected_paths,
|
|
17
|
+
get_platform,
|
|
18
|
+
remove_git_clean_candidates,
|
|
19
|
+
setup_brew,
|
|
20
|
+
setup_bun,
|
|
21
|
+
setup_uv,
|
|
22
|
+
setup_uv_tool,
|
|
23
|
+
)
|
|
9
24
|
|
|
10
25
|
|
|
11
26
|
class BaseSpace(Bakebook):
|
|
27
|
+
def _no_implementation(self, ctx: Context | None = None, *args, **kwargs):
|
|
28
|
+
_ = ctx, args, kwargs
|
|
29
|
+
console.error("No implementation")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
12
32
|
@command(help="Run linters and formatters")
|
|
13
33
|
def lint(self, ctx: Context) -> None:
|
|
14
|
-
ctx.run(
|
|
15
|
-
[
|
|
16
|
-
"bunx",
|
|
17
|
-
"prettier@latest",
|
|
18
|
-
"--write",
|
|
19
|
-
"**/*.{js,jsx,ts,tsx,css,json,json5,yaml,yml,md}",
|
|
20
|
-
]
|
|
21
|
-
)
|
|
34
|
+
ctx.run('bunx prettier@latest --write "**/*.{js,jsx,ts,tsx,css,json,json5,yaml,yml,md\'}"')
|
|
22
35
|
|
|
23
36
|
@command(help="Run unit tests")
|
|
24
37
|
def test(self, ctx: Context) -> None:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
self._no_implementation(ctx)
|
|
39
|
+
|
|
40
|
+
@command(help="Run integration tests")
|
|
41
|
+
def test_integration(self, ctx: Context) -> None:
|
|
42
|
+
self._no_implementation(ctx)
|
|
43
|
+
|
|
44
|
+
@command(help="Run all tests")
|
|
45
|
+
def test_all(self, ctx: Context) -> None:
|
|
46
|
+
self._no_implementation(ctx)
|
|
28
47
|
|
|
29
48
|
@command(help="Clean gitignored files with optional exclusions")
|
|
30
49
|
def clean(
|
|
@@ -66,8 +85,109 @@ class BaseSpace(Bakebook):
|
|
|
66
85
|
def clean_all(self, ctx: Context) -> None:
|
|
67
86
|
ctx.run("git clean -fdX")
|
|
68
87
|
|
|
88
|
+
def setup_tool_managers(self, ctx: Context, platform: PlatformType) -> None:
|
|
89
|
+
_ = platform
|
|
90
|
+
setup_brew(ctx)
|
|
91
|
+
|
|
92
|
+
def setup_tools(self, ctx: Context, platform: PlatformType) -> None:
|
|
93
|
+
_ = platform
|
|
94
|
+
setup_bun(ctx)
|
|
95
|
+
setup_uv(ctx)
|
|
96
|
+
setup_uv_tool(ctx)
|
|
97
|
+
|
|
98
|
+
def setup_project(self, ctx: Context) -> None:
|
|
99
|
+
ctx.run("uv run pre-commit install")
|
|
100
|
+
|
|
69
101
|
@command(help="Setup development environment")
|
|
70
102
|
def setup_dev(self, ctx: Context) -> None:
|
|
103
|
+
platform = get_platform()
|
|
104
|
+
console.echo(f"Detected platform: {platform}")
|
|
105
|
+
|
|
106
|
+
if platform != "macos":
|
|
107
|
+
console.warning(f"Platform '{platform}' is not supported. Running in dry-run mode.")
|
|
108
|
+
overridden_dry_run = True
|
|
109
|
+
else:
|
|
110
|
+
overridden_dry_run = ctx.dry_run
|
|
111
|
+
|
|
112
|
+
with ctx.override_dry_run(overridden_dry_run):
|
|
113
|
+
self.clean(ctx=ctx)
|
|
114
|
+
self.setup_tool_managers(ctx=ctx, platform=platform)
|
|
115
|
+
self.setup_tools(ctx=ctx, platform=platform)
|
|
116
|
+
self.setup_project(ctx=ctx)
|
|
117
|
+
|
|
118
|
+
def _assert_which_path(
|
|
119
|
+
self,
|
|
120
|
+
ctx: Context,
|
|
121
|
+
tool_name: str,
|
|
122
|
+
tool_info: ToolInfo,
|
|
123
|
+
) -> bool:
|
|
124
|
+
result = ctx.run(f"which {tool_name}", stream=False)
|
|
125
|
+
if ctx.dry_run:
|
|
126
|
+
return True
|
|
127
|
+
actual_path = Path(result.stdout.strip())
|
|
128
|
+
|
|
129
|
+
if actual_path in set(tool_info.expected_paths):
|
|
130
|
+
console.success(f"{tool_name}: {actual_path}")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
console.warning(f"{tool_name}: unexpected location (got {actual_path})")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
137
|
+
return {
|
|
138
|
+
# homebrew only
|
|
139
|
+
"bun": ToolInfo(expected_paths=get_expected_paths("bun", {HOMWBREW_BIN})),
|
|
140
|
+
# homebrew or venv
|
|
141
|
+
"uv": ToolInfo(expected_paths=get_expected_paths("uv", {HOMWBREW_BIN, VENV_BIN})),
|
|
142
|
+
# local or venv
|
|
143
|
+
"bakefile": ToolInfo(
|
|
144
|
+
expected_paths=get_expected_paths("bakefile", {LOCAL_BIN, VENV_BIN})
|
|
145
|
+
),
|
|
146
|
+
"pre-commit": ToolInfo(
|
|
147
|
+
expected_paths=get_expected_paths("pre-commit", {LOCAL_BIN, VENV_BIN})
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@command(help="List development tools")
|
|
152
|
+
def tools(
|
|
153
|
+
self,
|
|
154
|
+
ctx: Context,
|
|
155
|
+
format: Annotated[
|
|
156
|
+
Literal["json", "names"],
|
|
157
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
158
|
+
] = "json",
|
|
159
|
+
) -> None:
|
|
71
160
|
_ = ctx
|
|
72
|
-
|
|
73
|
-
|
|
161
|
+
tools = self._get_tools()
|
|
162
|
+
if format == "json":
|
|
163
|
+
output: dict[str, dict[str, str | None]] = {k: v.model_dump() for k, v in tools.items()}
|
|
164
|
+
console.echo(orjson.dumps(output, option=orjson.OPT_INDENT_2).decode())
|
|
165
|
+
else:
|
|
166
|
+
console.echo("\n".join(sorted(tools.keys())))
|
|
167
|
+
|
|
168
|
+
@command(help="Assert development environment setup")
|
|
169
|
+
def assert_setup_dev(
|
|
170
|
+
self,
|
|
171
|
+
ctx: Context,
|
|
172
|
+
skip_test: Annotated[
|
|
173
|
+
bool,
|
|
174
|
+
typer.Option(
|
|
175
|
+
"--skip-test",
|
|
176
|
+
"-s",
|
|
177
|
+
help="Skip running tests",
|
|
178
|
+
is_flag=True,
|
|
179
|
+
),
|
|
180
|
+
] = False,
|
|
181
|
+
) -> None:
|
|
182
|
+
tools = self._get_tools()
|
|
183
|
+
for tool_name, tool_info in tools.items():
|
|
184
|
+
self._assert_which_path(ctx, tool_name, tool_info)
|
|
185
|
+
|
|
186
|
+
self.lint(ctx)
|
|
187
|
+
if not skip_test:
|
|
188
|
+
self.test(ctx)
|
|
189
|
+
|
|
190
|
+
@command(help="Upgrade all dependencies")
|
|
191
|
+
def update(self, ctx: Context) -> None:
|
|
192
|
+
ctx.run("uv python upgrade")
|
|
193
|
+
ctx.run("uv tool upgrade --all")
|
bakelib/space/python.py
CHANGED
|
@@ -1,42 +1,80 @@
|
|
|
1
|
-
from
|
|
1
|
+
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from bake import Context, params
|
|
4
|
+
|
|
5
|
+
from .base import BaseSpace, ToolInfo
|
|
6
|
+
from .utils import VENV_BIN, get_expected_paths
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_python_version() -> str | None:
|
|
10
|
+
path = Path(".python-version")
|
|
11
|
+
if not path.exists():
|
|
12
|
+
return None
|
|
13
|
+
return path.read_text().strip()
|
|
4
14
|
|
|
5
15
|
|
|
6
16
|
class PythonSpace(BaseSpace):
|
|
17
|
+
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
18
|
+
tools = super()._get_tools()
|
|
19
|
+
tools["python"] = ToolInfo(
|
|
20
|
+
version=_get_python_version(),
|
|
21
|
+
expected_paths=list(get_expected_paths("python", {VENV_BIN})),
|
|
22
|
+
)
|
|
23
|
+
return tools
|
|
24
|
+
|
|
7
25
|
def lint(self, ctx: Context) -> None:
|
|
8
26
|
super().lint(ctx=ctx)
|
|
9
27
|
|
|
10
28
|
ctx.run(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"run",
|
|
14
|
-
"toml-sort",
|
|
15
|
-
"--sort-inline-arrays",
|
|
16
|
-
"--in-place",
|
|
17
|
-
"--sort-first=project,dependency-groups",
|
|
18
|
-
"pyproject.toml",
|
|
19
|
-
]
|
|
29
|
+
"uv run toml-sort --sort-inline-arrays --in-place "
|
|
30
|
+
"--sort-first=project,dependency-groups pyproject.toml"
|
|
20
31
|
)
|
|
21
|
-
ctx.run(
|
|
22
|
-
ctx.run(
|
|
23
|
-
ctx.run(
|
|
24
|
-
ctx.run(
|
|
32
|
+
ctx.run("uv run ruff format --exit-non-zero-on-format .")
|
|
33
|
+
ctx.run("uv run ruff check --fix --exit-non-zero-on-fix .")
|
|
34
|
+
ctx.run("uv run ty check --error-on-warning --no-progress .")
|
|
35
|
+
ctx.run("uv run deptry .")
|
|
25
36
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"run",
|
|
31
|
-
"pytest",
|
|
32
|
-
"tests/",
|
|
33
|
-
"--cov=src",
|
|
34
|
-
"--cov-report=html",
|
|
35
|
-
"--cov-report=term-missing",
|
|
36
|
-
"--cov-report=xml",
|
|
37
|
-
]
|
|
37
|
+
def _test(self, ctx: Context, *, tests_path: str, verbose: bool = False) -> None:
|
|
38
|
+
cmd = (
|
|
39
|
+
f"uv run pytest {tests_path} --cov=src --cov-report=html"
|
|
40
|
+
" --cov-report=term-missing --cov-report=xml"
|
|
38
41
|
)
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
if verbose:
|
|
44
|
+
cmd += " -s -v"
|
|
45
|
+
|
|
46
|
+
ctx.run(cmd)
|
|
47
|
+
|
|
48
|
+
def test_integration(
|
|
49
|
+
self,
|
|
50
|
+
ctx: Context,
|
|
51
|
+
verbose: params.verbose_bool = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
integration_tests_path = "tests/integration/"
|
|
54
|
+
if Path(integration_tests_path).exists():
|
|
55
|
+
tests_path = integration_tests_path
|
|
56
|
+
self._test(ctx, tests_path=tests_path, verbose=verbose)
|
|
57
|
+
else:
|
|
58
|
+
self._no_implementation(ctx)
|
|
59
|
+
|
|
60
|
+
def test(self, ctx: Context) -> None:
|
|
61
|
+
unit_tests_path = "tests/unit/"
|
|
62
|
+
tests_path = unit_tests_path if Path(unit_tests_path).exists() else "tests/"
|
|
63
|
+
self._test(ctx, tests_path=tests_path)
|
|
64
|
+
|
|
65
|
+
def test_all(self, ctx: Context) -> None:
|
|
66
|
+
unit_tests_path = "tests/unit/"
|
|
67
|
+
if Path(unit_tests_path).exists():
|
|
68
|
+
tests_path = "tests/"
|
|
69
|
+
self._test(ctx, tests_path=tests_path)
|
|
70
|
+
else:
|
|
71
|
+
self._no_implementation(ctx)
|
|
72
|
+
|
|
73
|
+
def setup_project(self, ctx: Context) -> None:
|
|
74
|
+
super().setup_project(ctx=ctx)
|
|
42
75
|
ctx.run("uv sync --all-extras --all-groups --frozen")
|
|
76
|
+
|
|
77
|
+
def update(self, ctx: Context) -> None:
|
|
78
|
+
super().update(ctx=ctx)
|
|
79
|
+
ctx.run("uv lock --upgrade")
|
|
80
|
+
ctx.run("uv sync --all-extras --all-groups")
|
bakelib/space/utils.py
CHANGED
|
@@ -1,12 +1,75 @@
|
|
|
1
1
|
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
from enum import Enum
|
|
2
4
|
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
3
6
|
|
|
4
7
|
import pathspec
|
|
5
8
|
from pathspec.patterns.gitignore.basic import GitIgnoreBasicPattern
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
6
10
|
|
|
11
|
+
from bake import Context
|
|
7
12
|
from bake.ui import console
|
|
8
13
|
|
|
9
14
|
|
|
15
|
+
def setup_brew(ctx: Context) -> None:
|
|
16
|
+
ctx.run("brew update")
|
|
17
|
+
ctx.run("brew upgrade")
|
|
18
|
+
ctx.run("brew cleanup")
|
|
19
|
+
ctx.run("brew list")
|
|
20
|
+
ctx.run("brew leaves")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolInfo(BaseModel):
|
|
24
|
+
version: str | None = None
|
|
25
|
+
expected_paths: list[Path] = Field(default_factory=list, exclude=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Platform(Enum):
|
|
29
|
+
MACOS = "macos"
|
|
30
|
+
LINUX = "linux"
|
|
31
|
+
WINDOWS = "windows"
|
|
32
|
+
OTHER = "other"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PlatformType = Literal["macos", "linux", "windows", "other"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_platform() -> PlatformType:
|
|
39
|
+
if sys.platform == "darwin":
|
|
40
|
+
return Platform.MACOS.value
|
|
41
|
+
elif sys.platform == "linux":
|
|
42
|
+
return Platform.LINUX.value
|
|
43
|
+
elif sys.platform == "win32":
|
|
44
|
+
return Platform.WINDOWS.value
|
|
45
|
+
return Platform.OTHER.value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def setup_uv(ctx: Context) -> None:
|
|
49
|
+
ctx.run("brew install uv")
|
|
50
|
+
ctx.run("uv python upgrade")
|
|
51
|
+
ctx.run("uv tool upgrade --all")
|
|
52
|
+
ctx.run("uv tool update-shell")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def setup_bun(ctx: Context) -> None:
|
|
56
|
+
ctx.run("brew install oven-sh/bun/bun")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_uv_tool(ctx: Context) -> None:
|
|
60
|
+
ctx.run("uv tool install bakefile")
|
|
61
|
+
ctx.run("uv tool install pre-commit")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
HOMWBREW_BIN = Path("/opt/homebrew/bin")
|
|
65
|
+
LOCAL_BIN = Path.home() / ".local" / "bin"
|
|
66
|
+
VENV_BIN = Path.cwd() / ".venv" / "bin"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_expected_paths(tool: str, locations: set[Path]) -> list[Path]:
|
|
70
|
+
return [loc / tool for loc in locations]
|
|
71
|
+
|
|
72
|
+
|
|
10
73
|
def _skip_msg(path: Path, suffix: str, dry_run: bool) -> None:
|
|
11
74
|
verb = "Would skip" if dry_run else "Skipping"
|
|
12
75
|
console.echo(f"[yellow]~[/yellow] {verb} {suffix}{path}")
|
|
File without changes
|