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 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
@@ -24,7 +24,7 @@ from bake.utils.constants import (
24
24
  from .obj import BakefileObject
25
25
 
26
26
  rich_markup_mode: MarkupMode = "rich" if not console.out.no_color else None
27
- add_completion = False
27
+ add_completion = True
28
28
 
29
29
 
30
30
  class BakefileApp(typer.Typer):
@@ -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: str,
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: list[str] | tuple[str, ...],
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: str | list[str] | tuple[str, ...],
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
@@ -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
- if sample_module.__file__ is None:
16
- raise ValueError(f"Could not find `{sample_module.__name__}`")
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
- original_bakefile_content = Path(sample_module.__file__).read_text()
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
@@ -1,5 +1,4 @@
1
- from bake.bakebook.bakebook import Bakebook
2
- from bake.ui import console
1
+ from bake import Bakebook, console
3
2
 
4
3
  __bakebook__ = Bakebook()
5
4
 
bake/ui/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
- from bake.ui import console
1
+ from bake.ui import console, params
2
2
  from bake.ui.logger.setup import setup_logging
3
3
  from bake.ui.run import run, run_uv
4
4
 
5
5
  __all__ = [
6
6
  "console",
7
+ "params",
7
8
  "run",
8
9
  "run_uv",
9
10
  "setup_logging",
bake/ui/params.py ADDED
@@ -0,0 +1,5 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+
5
+ verbose_bool = Annotated[bool, typer.Option("-v", "--verbose", help="Run with verbose output")]
bake/ui/run/__init__.py CHANGED
@@ -1,11 +1,5 @@
1
- """Subprocess execution utilities for bake UI.
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
- result = _run_with_stream(
290
- cmd=cmd,
291
- shell=shell,
292
- cwd=cwd,
293
- capture_output=capture_output,
294
- env=env,
295
- _encoding=_encoding,
296
- **kwargs,
297
- )
298
- else:
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
- ) -> tuple[subprocess.Popen, OutputSplitter]:
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, slave_fd = pty.openpty()
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=slave_fd,
411
- stderr=subprocess.PIPE if capture_output else None,
414
+ stdout=slave_stdout,
415
+ stderr=slave_stderr,
412
416
  shell=shell,
413
417
  env=env,
414
418
  **kwargs,
415
419
  )
416
- os.close(slave_fd)
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, capture=capture_output, pty_fd=stdout_fd, encoding=_encoding
426
+ stream=True,
427
+ capture=capture_output,
428
+ pty_fd=stdout_fd,
429
+ stderr_pty_fd=stderr_fd,
430
+ encoding=_encoding,
420
431
  )
421
- return proc, splitter
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
- ) -> tuple[subprocess.Popen, OutputSplitter, list]:
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
- proc, splitter = _setup_pty_stream(
467
- cmd=cmd,
468
- shell=shell,
469
- cwd=cwd,
470
- capture_output=capture_output,
471
- env=env,
472
- _encoding=_encoding,
473
- **kwargs,
474
- )
475
- threads = splitter.attach(proc)
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
- return subprocess.run(
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
- return subprocess.run(
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 (regular pipe)
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.5
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=SG-g8MVoBbXURlAP-OhtjMUMnRPTfi9lQ9WQD0EX7IE,338
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=5O08RTx8tKHTMio8-RdEsLC6bUD7AKQ6rPNLBZqQiJ0,1352
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=ViRqRX2DsLoKAunGKd6vOiwk4Xqmug1Gkux4_1RZX_U,1444
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=p3_YCExH4NIIgB4ZTax2fYFOjI__Ig4FffbqegtPE8Q,3699
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=O23DCxdoAV7Pujxk_ylrSKHL-7DSS1-7BZuxpa0Hv5M,7016
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=efGViLk7sh-QX9Mox7yQw_A1Tp7EOuc_vmSTbFmXUm0,736
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=tTL2OVqQXMYwIlgd1K5gtUKDzxx2CDEb24JLzXkadYg,192
37
- bake/ui/__init__.py,sha256=DeuVKFwrmhEcwcZnstItPecnvYugpLcPM_a7YEhnqD8,187
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/run/__init__.py,sha256=RLQN4f6mY2wyWtqe2e9B1jKwR5P9IyBcZGLdOwj91Ds,336
44
- bake/ui/run/run.py,sha256=5ve7qbl37hRTpbLFf-wDtmG3RuVKYPlSUOsRlSZ9nUU,17390
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=92h4KaomqoYsxi9xZoLoSvk-7JdBF0YL-QNPpVaXqEA,8139
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=HzJYzwbeX5YN6Esmxt4fSDRGJNUk4OfZ3Zf9m0bNsX4,2103
56
- bakelib/space/python.py,sha256=rlEJW5y1q00lGMHwYXicwTt1a4NUXfSei-T_pg88oMA,1223
57
- bakelib/space/utils.py,sha256=SfNRFeAm35zfF01-fdxKXTlw2uL6DyU0TJXDFkUG7Zk,1471
58
- bakefile-0.0.5.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
59
- bakefile-0.0.5.dist-info/entry_points.txt,sha256=Ecvvh7BYHCPJ0UdntrDc3Od6AZdRPXN5Z7o_7ok_0Qw,107
60
- bakefile-0.0.5.dist-info/METADATA,sha256=vBmhYvnlTTuO2x4IpgNudcEBITAy23VZGvmYahT99kY,2302
61
- bakefile-0.0.5.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.25
2
+ Generator: uv 0.9.26
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
bakelib/space/base.py CHANGED
@@ -1,30 +1,49 @@
1
- from typing import Annotated
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 remove_git_clean_candidates
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
- _ = ctx
26
- console.error("No implementation")
27
- raise typer.Exit(1)
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
- console.error("No implementation")
73
- raise typer.Exit(1)
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 bake import Context
1
+ from pathlib import Path
2
2
 
3
- from .base import BaseSpace
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
- "uv",
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(["uv", "run", "ruff", "format", "--exit-non-zero-on-format", "."])
22
- ctx.run(["uv", "run", "ruff", "check", "--fix", "--exit-non-zero-on-fix", "."])
23
- ctx.run(["uv", "run", "ty", "check", "--error-on-warning", "."])
24
- ctx.run(["uv", "run", "deptry", "."])
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 test(self, ctx: Context) -> None:
27
- ctx.run(
28
- [
29
- "uv",
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
- def setup_dev(self, ctx: Context) -> None:
41
- super().clean(ctx=ctx)
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}")