bakefile 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. bake/__init__.py +9 -0
  2. bake/bakebook/bakebook.py +85 -0
  3. bake/bakebook/decorator.py +50 -0
  4. bake/bakebook/get.py +175 -0
  5. bake/cli/bake/__init__.py +3 -0
  6. bake/cli/bake/__main__.py +5 -0
  7. bake/cli/bake/main.py +74 -0
  8. bake/cli/bake/reinvocation.py +63 -0
  9. bake/cli/bakefile/__init__.py +3 -0
  10. bake/cli/bakefile/__main__.py +5 -0
  11. bake/cli/bakefile/add_inline.py +29 -0
  12. bake/cli/bakefile/export.py +212 -0
  13. bake/cli/bakefile/find_python.py +18 -0
  14. bake/cli/bakefile/init.py +56 -0
  15. bake/cli/bakefile/lint.py +77 -0
  16. bake/cli/bakefile/main.py +43 -0
  17. bake/cli/bakefile/uv.py +146 -0
  18. bake/cli/common/app.py +54 -0
  19. bake/cli/common/callback.py +13 -0
  20. bake/cli/common/context.py +145 -0
  21. bake/cli/common/exception_handler.py +57 -0
  22. bake/cli/common/obj.py +216 -0
  23. bake/cli/common/params.py +72 -0
  24. bake/cli/utils/__init__.py +0 -0
  25. bake/cli/utils/version.py +18 -0
  26. bake/manage/__init__.py +0 -0
  27. bake/manage/add_inline.py +71 -0
  28. bake/manage/find_python.py +210 -0
  29. bake/manage/lint.py +101 -0
  30. bake/manage/run_uv.py +88 -0
  31. bake/manage/write_bakefile.py +20 -0
  32. bake/py.typed +0 -0
  33. bake/samples/__init__.py +0 -0
  34. bake/samples/simple.py +8 -0
  35. bake/ui/__init__.py +11 -0
  36. bake/ui/console.py +58 -0
  37. bake/ui/logger/__init__.py +33 -0
  38. bake/ui/logger/capsys.py +158 -0
  39. bake/ui/logger/setup.py +53 -0
  40. bake/ui/logger/utils.py +215 -0
  41. bake/ui/params.py +5 -0
  42. bake/ui/run/__init__.py +5 -0
  43. bake/ui/run/run.py +546 -0
  44. bake/ui/run/script.py +74 -0
  45. bake/ui/run/splitter.py +249 -0
  46. bake/ui/run/uv.py +83 -0
  47. bake/ui/style.py +2 -0
  48. bake/utils/__init__.py +11 -0
  49. bake/utils/constants.py +21 -0
  50. {bakefile → bake/utils}/env.py +3 -1
  51. bake/utils/exceptions.py +17 -0
  52. {bakefile-0.0.4.dist-info → bakefile-0.0.6.dist-info}/METADATA +15 -2
  53. bakefile-0.0.6.dist-info/RECORD +63 -0
  54. {bakefile-0.0.4.dist-info → bakefile-0.0.6.dist-info}/WHEEL +2 -2
  55. bakefile-0.0.6.dist-info/entry_points.txt +5 -0
  56. bakelib/__init__.py +4 -0
  57. bakelib/space/__init__.py +0 -0
  58. bakelib/space/base.py +193 -0
  59. bakelib/space/python.py +80 -0
  60. bakelib/space/utils.py +118 -0
  61. bakefile/__init__.py +0 -13
  62. bakefile/cli/bake/__init__.py +0 -3
  63. bakefile/cli/bake/main.py +0 -127
  64. bakefile/cli/bake/resolve_bakebook.py +0 -103
  65. bakefile/cli/bake/utils.py +0 -25
  66. bakefile/cli/bakefile.py +0 -19
  67. bakefile/cli/utils/version.py +0 -9
  68. bakefile/exceptions.py +0 -9
  69. bakefile-0.0.4.dist-info/RECORD +0 -16
  70. bakefile-0.0.4.dist-info/entry_points.txt +0 -4
  71. {bakefile/cli/utils → bake/bakebook}/__init__.py +0 -0
  72. {bakefile → bake}/cli/__init__.py +0 -0
  73. /bakefile/py.typed → /bake/cli/common/__init__.py +0 -0
@@ -0,0 +1,145 @@
1
+ import subprocess
2
+ from collections.abc import Generator
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Literal, overload
6
+
7
+ import click
8
+ import typer
9
+ from typer.core import TyperCommand
10
+
11
+ from bake.ui.run import CmdType
12
+ from bake.ui.run import run as _run
13
+ from bake.ui.run.script import run_script as _run_script
14
+
15
+ from .obj import BakefileObject
16
+
17
+ if TYPE_CHECKING:
18
+ from bake.bakebook.bakebook import Bakebook
19
+
20
+
21
+ class Context(typer.Context):
22
+ obj: BakefileObject
23
+
24
+ @property
25
+ def dry_run(self) -> bool:
26
+ return self.obj.dry_run
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
+
41
+ @property
42
+ def verbosity(self) -> int:
43
+ return self.obj.verbosity
44
+
45
+ @property
46
+ def bakebook(self) -> "Bakebook | None":
47
+ return self.obj.bakebook
48
+
49
+ @overload
50
+ def run(
51
+ self,
52
+ cmd: CmdType,
53
+ *,
54
+ capture_output: Literal[True] = True,
55
+ check: bool = True,
56
+ cwd: Path | str | None = None,
57
+ stream: bool = True,
58
+ shell: bool | None = None,
59
+ echo: bool = True,
60
+ dry_run: bool | None = None,
61
+ keep_temp_file: bool = False,
62
+ env: dict[str, str] | None = None,
63
+ **kwargs,
64
+ ) -> subprocess.CompletedProcess[str]: ...
65
+
66
+ @overload
67
+ def run(
68
+ self,
69
+ cmd: CmdType,
70
+ *,
71
+ capture_output: Literal[False],
72
+ check: bool = True,
73
+ cwd: Path | str | None = None,
74
+ stream: bool = True,
75
+ shell: bool | None = None,
76
+ echo: bool = True,
77
+ dry_run: bool | None = None,
78
+ keep_temp_file: bool = False,
79
+ env: dict[str, str] | None = None,
80
+ **kwargs,
81
+ ) -> subprocess.CompletedProcess[None]: ...
82
+
83
+ def run(
84
+ self,
85
+ cmd: CmdType,
86
+ *,
87
+ capture_output: bool = True,
88
+ check: bool = True,
89
+ cwd: Path | str | None = None,
90
+ stream: bool = True,
91
+ shell: bool | None = None,
92
+ echo: bool = True,
93
+ dry_run: bool | None = None,
94
+ keep_temp_file: bool = False,
95
+ env: dict[str, str] | None = None,
96
+ _encoding: str | None = None,
97
+ **kwargs,
98
+ ) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
99
+ return _run(
100
+ cmd,
101
+ capture_output=capture_output,
102
+ check=check,
103
+ cwd=cwd,
104
+ stream=stream,
105
+ shell=shell,
106
+ echo=echo,
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,
111
+ **kwargs,
112
+ )
113
+
114
+ def run_script(
115
+ self,
116
+ title: str,
117
+ script: str,
118
+ *,
119
+ capture_output: bool = True,
120
+ check: bool = True,
121
+ cwd: Path | str | None = None,
122
+ stream: bool = True,
123
+ echo: bool = True,
124
+ dry_run: bool | None = None,
125
+ keep_temp_file: bool = False,
126
+ env: dict[str, str] | None = None,
127
+ **kwargs,
128
+ ) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
129
+ return _run_script(
130
+ title,
131
+ script,
132
+ capture_output=capture_output,
133
+ check=check,
134
+ cwd=cwd,
135
+ stream=stream,
136
+ echo=echo,
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
+ **kwargs,
141
+ )
142
+
143
+
144
+ class BakeCommand(TyperCommand):
145
+ context_class: type[click.Context] = Context
@@ -0,0 +1,57 @@
1
+ import errno
2
+ import sys
3
+ from contextlib import contextmanager
4
+ from gettext import gettext
5
+ from typing import TextIO, cast
6
+
7
+ import click
8
+ from typer.core import HAS_RICH, MarkupMode
9
+
10
+
11
+ @contextmanager
12
+ def typer_exception_handler(
13
+ *,
14
+ standalone_mode: bool,
15
+ rich_markup_mode: MarkupMode,
16
+ ):
17
+ # Reference code: https://github.com/fastapi/typer/blob/da9c4c67f3d8e4acd5f76e8909503bb999f1b751/typer/core.py#L186-L248
18
+ try:
19
+ try:
20
+ yield
21
+ except EOFError as e:
22
+ click.echo(file=sys.stderr)
23
+ raise click.Abort() from e
24
+ except KeyboardInterrupt as e:
25
+ raise click.exceptions.Exit(130) from e
26
+ except click.ClickException as e:
27
+ if not standalone_mode:
28
+ raise
29
+ if HAS_RICH and rich_markup_mode is not None:
30
+ from typer import rich_utils
31
+
32
+ rich_utils.rich_format_error(e)
33
+ else:
34
+ e.show()
35
+ sys.exit(e.exit_code)
36
+ except OSError as e:
37
+ if e.errno == errno.EPIPE:
38
+ sys.stdout = cast(TextIO, click.utils.PacifyFlushWrapper(sys.stdout))
39
+ sys.stderr = cast(TextIO, click.utils.PacifyFlushWrapper(sys.stderr))
40
+ sys.exit(1)
41
+ raise
42
+ except click.exceptions.Exit as e:
43
+ if standalone_mode:
44
+ sys.exit(e.exit_code)
45
+ else:
46
+ # return exit code to caller
47
+ raise
48
+ except click.Abort:
49
+ if not standalone_mode:
50
+ raise
51
+ if HAS_RICH and rich_markup_mode is not None:
52
+ from typer import rich_utils
53
+
54
+ rich_utils.rich_abort_error()
55
+ else:
56
+ click.echo(gettext("Aborted!"), file=sys.stderr)
57
+ sys.exit(1)
bake/cli/common/obj.py ADDED
@@ -0,0 +1,216 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import click
10
+ import typer
11
+ from pydantic import ValidationError
12
+ from rich.traceback import Traceback
13
+ from typer.core import MarkupMode
14
+ from typer.main import get_command_from_info
15
+
16
+ from bake.bakebook.get import (
17
+ get_bakebook_from_target_dir_path,
18
+ resolve_bakefile_path,
19
+ )
20
+ from bake.ui import console, setup_logging
21
+ from bake.utils.constants import (
22
+ DEFAULT_BAKEBOOK_NAME,
23
+ DEFAULT_CHDIR,
24
+ DEFAULT_FILE_NAME,
25
+ DEFAULT_IS_CHAIN_COMMAND,
26
+ GET_BAKEFILE_OBJECT,
27
+ )
28
+ from bake.utils.exceptions import BakebookError, BakefileNotFoundError
29
+
30
+ from .callback import validate_file_name
31
+ from .exception_handler import typer_exception_handler
32
+ from .params import (
33
+ bakebook_name_option,
34
+ chdir_option,
35
+ dry_run_option,
36
+ file_name_option,
37
+ is_chain_commands_option,
38
+ remaining_args_argument,
39
+ verbosity_option,
40
+ )
41
+
42
+ if TYPE_CHECKING:
43
+ from bake.bakebook.bakebook import Bakebook
44
+
45
+ logger = logging.Logger(__name__)
46
+
47
+
48
+ @dataclass
49
+ class BakefileObject:
50
+ chdir: Path
51
+ file_name: str
52
+ bakebook_name: str
53
+ bakefile_path: Path | None = None
54
+ bakebook: "Bakebook | None" = None
55
+ verbosity: int = 0
56
+ dry_run: bool = False
57
+ remaining_args: list[str] | None = None
58
+ is_chain_commands: bool = False
59
+
60
+ def __post_init__(self):
61
+ validate_file_name(self.file_name)
62
+
63
+ def resolve_bakefile_path(self) -> Path | None:
64
+ if self.bakefile_path is not None:
65
+ return self.bakefile_path
66
+
67
+ with contextlib.suppress(BakefileNotFoundError):
68
+ self.bakefile_path = resolve_bakefile_path(chdir=self.chdir, file_name=self.file_name)
69
+
70
+ return self.bakefile_path
71
+
72
+ def get_bakebook(self, allow_missing: bool):
73
+ if self.bakebook is not None:
74
+ return
75
+
76
+ try:
77
+ if self.bakefile_path is None:
78
+ self.bakefile_path = resolve_bakefile_path(
79
+ chdir=self.chdir, file_name=self.file_name
80
+ )
81
+ self.bakebook = get_bakebook_from_target_dir_path(
82
+ target_dir_path=self.bakefile_path, bakebook_name=self.bakebook_name
83
+ )
84
+ except BakefileNotFoundError as e:
85
+ if allow_missing:
86
+ return
87
+ console.error(str(e))
88
+ raise SystemExit(1) from e
89
+ except BakebookError as e:
90
+ if allow_missing:
91
+ return
92
+ exc_to_show = e.__cause__ if e.__cause__ else e
93
+
94
+ if exc_to_show.__class__ in {ValidationError, BakebookError}:
95
+ console.err.print(
96
+ f"[bold red]{exc_to_show.__class__.__name__}:[/bold red]", end=" "
97
+ )
98
+ console.err.print(exc_to_show)
99
+ console.err.print(f"Searched in: {self.chdir.resolve()}\n")
100
+ else:
101
+ console.err.print(
102
+ Traceback.from_exception(
103
+ type(exc_to_show), exc_to_show, exc_to_show.__traceback__
104
+ )
105
+ )
106
+ raise SystemExit(1) from e
107
+
108
+ def warn_if_no_bakebook(self, color_echo: bool):
109
+ if self.bakebook is None:
110
+ _ = color_echo # Color handled by console module
111
+ console.warning(f"Bakebook `{self.bakebook_name}` not found in `{self.file_name}`")
112
+ console.echo(f"Searched in: {self.chdir.resolve()}\n")
113
+
114
+ def setup_logging(self):
115
+ level_map = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}
116
+ log_level = level_map.get(self.verbosity, logging.WARNING)
117
+ setup_logging(level_per_module={"": log_level}, is_pretty_log=True)
118
+
119
+
120
+ bakefile_obj_app = typer.Typer()
121
+
122
+
123
+ def get_args(args: list[str] | None = None, windows_expand_args: bool = True) -> list[str]:
124
+ if args is None:
125
+ args = sys.argv[1:]
126
+
127
+ # Covered in Click tests
128
+ if os.name == "nt" and windows_expand_args: # pragma: no cover
129
+ args = click.utils._expand_args(args)
130
+ else:
131
+ args = list(args)
132
+
133
+ return args
134
+
135
+
136
+ def bakefile_obj_app_args(
137
+ args: list[str] | None = None,
138
+ windows_expand_args: bool = True,
139
+ ) -> list[str]:
140
+ # source from https://github.com/fastapi/typer/blob/b7f39eaad60141988f5d9a58df72c44d6128cd53/typer/core.py#L175-L185
141
+
142
+ args = get_args(args=args, windows_expand_args=windows_expand_args)
143
+
144
+ prohibited_non_bakefile_obj_app_args: list[str] = ["--help", "--version"]
145
+
146
+ args = [arg for arg in args if arg not in prohibited_non_bakefile_obj_app_args]
147
+ return args
148
+
149
+
150
+ def is_bakebook_optional(remaining_args: list[str] | None) -> bool:
151
+ args = get_args()
152
+
153
+ some_args: list[str] = ["--help", "--version"]
154
+ is_some_args = len([arg for arg in args if arg in some_args]) > 0
155
+ return is_some_args or remaining_args is None or remaining_args == []
156
+
157
+
158
+ @bakefile_obj_app.command(
159
+ name=GET_BAKEFILE_OBJECT,
160
+ hidden=True,
161
+ context_settings={
162
+ "allow_extra_args": True,
163
+ "allow_interspersed_args": False,
164
+ "ignore_unknown_options": True,
165
+ },
166
+ )
167
+ def _get_bakefile_object(
168
+ ctx: typer.Context,
169
+ chdir: chdir_option = DEFAULT_CHDIR,
170
+ file_name: file_name_option = DEFAULT_FILE_NAME,
171
+ bakebook_name: bakebook_name_option = DEFAULT_BAKEBOOK_NAME,
172
+ is_chain_commands: is_chain_commands_option = DEFAULT_IS_CHAIN_COMMAND,
173
+ remaining_args: remaining_args_argument = None,
174
+ verbosity: verbosity_option = 0,
175
+ dry_run: dry_run_option = False,
176
+ ):
177
+ _ = ctx
178
+ return BakefileObject(
179
+ chdir=chdir,
180
+ file_name=file_name,
181
+ bakebook_name=bakebook_name,
182
+ verbosity=verbosity,
183
+ dry_run=dry_run,
184
+ remaining_args=remaining_args,
185
+ is_chain_commands=is_chain_commands,
186
+ )
187
+
188
+
189
+ def get_bakefile_object(rich_markup_mode: MarkupMode) -> BakefileObject:
190
+ with typer_exception_handler(standalone_mode=True, rich_markup_mode=rich_markup_mode):
191
+ args = bakefile_obj_app_args()
192
+
193
+ for registered_command in bakefile_obj_app.registered_commands:
194
+ if registered_command.name != GET_BAKEFILE_OBJECT:
195
+ continue
196
+
197
+ command = get_command_from_info(
198
+ registered_command,
199
+ pretty_exceptions_short=bakefile_obj_app.pretty_exceptions_short,
200
+ rich_markup_mode=bakefile_obj_app.rich_markup_mode,
201
+ )
202
+
203
+ with command.make_context(info_name=GET_BAKEFILE_OBJECT, args=args) as ctx:
204
+ bakefile_obj = command.invoke(ctx)
205
+ if not isinstance(bakefile_obj, BakefileObject):
206
+ msg = (
207
+ f"Expected `bakefile_obj` to be an instance of "
208
+ f"{BakefileObject.__name__}, got {type(bakefile_obj).__name__}"
209
+ )
210
+ raise TypeError(msg)
211
+ return bakefile_obj
212
+
213
+ raise RuntimeError(
214
+ f"Failed to find the `{GET_BAKEFILE_OBJECT}` command in registered commands. "
215
+ f"This should never happen - please report this bug."
216
+ )
@@ -0,0 +1,72 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from bake.cli.common.callback import validate_file_name_callback
7
+ from bake.cli.utils.version import version_callback
8
+
9
+
10
+ def verbosity_callback(_ctx: typer.Context, _param: typer.CallbackParam, value: int) -> int:
11
+ """Validate verbosity level (max 2)."""
12
+ if value > 2:
13
+ raise typer.BadParameter("Maximum verbosity is -vv")
14
+ return value
15
+
16
+
17
+ # ==========================================================
18
+ # Bakefile CLI Parameters
19
+ # ==========================================================
20
+ chdir_option = Annotated[
21
+ Path,
22
+ typer.Option(
23
+ "-C",
24
+ "--chdir",
25
+ help="Change directory before running",
26
+ ),
27
+ ]
28
+ file_name_option = Annotated[
29
+ str,
30
+ typer.Option(
31
+ "--file-name",
32
+ "-f",
33
+ help="Path to bakefile.py",
34
+ callback=validate_file_name_callback,
35
+ ),
36
+ ]
37
+ bakebook_name_option = Annotated[
38
+ str, typer.Option("--book-name", "-b", help="Name of bakebook object to retrieve")
39
+ ]
40
+ version_option = Annotated[
41
+ bool,
42
+ typer.Option(
43
+ "--version",
44
+ help="Show version",
45
+ callback=version_callback,
46
+ is_eager=True,
47
+ ),
48
+ ]
49
+ is_chain_commands_option = Annotated[bool, typer.Option("--chain", "-c", help="Chain commands")]
50
+ remaining_args_argument = Annotated[list[str] | None, typer.Argument()]
51
+
52
+ verbosity_option = Annotated[
53
+ int,
54
+ typer.Option(
55
+ "-v",
56
+ "--verbose",
57
+ help="Increase verbosity (-v for info, -vv for debug)",
58
+ count=True,
59
+ callback=verbosity_callback,
60
+ ),
61
+ ]
62
+ dry_run_option = Annotated[
63
+ bool,
64
+ typer.Option("-n", "--dry-run", help="Dry run (show what would be done without executing)"),
65
+ ]
66
+
67
+ # ==========================================================
68
+ # Bakefile Local CLI Frequently Used Params
69
+ # ==========================================================
70
+ force_option = Annotated[
71
+ bool | None, typer.Option("--force/--no-force", "-f", help="Force execution")
72
+ ]
File without changes
@@ -0,0 +1,18 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ import typer
4
+
5
+ from bake.ui import console
6
+
7
+
8
+ def _get_version() -> str:
9
+ try:
10
+ return version("bakefile")
11
+ except PackageNotFoundError:
12
+ return "0.0.0"
13
+
14
+
15
+ def version_callback(value: bool) -> None:
16
+ if value:
17
+ console.out.print(_get_version(), style=None, highlight=False)
18
+ raise typer.Exit()
File without changes
@@ -0,0 +1,71 @@
1
+ import logging
2
+ import re
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from bake.ui import console, run_uv
8
+ from bake.utils.exceptions import BakebookError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ if sys.version_info >= (3, 11):
13
+ import tomllib
14
+ else:
15
+ import tomli as tomllib # type: ignore[import-not-found]
16
+
17
+
18
+ def add_inline_metadata(bakefile_path: Path) -> None:
19
+ if not bakefile_path.exists():
20
+ logger.error(f"Bakefile not found at {bakefile_path}")
21
+ raise BakebookError(
22
+ f"Bakefile not found at {bakefile_path}. "
23
+ f"Run 'bakefile init --inline' to create a new bakefile with PEP 723 metadata."
24
+ )
25
+
26
+ result = run_uv(
27
+ ["init", "--script", str(bakefile_path.name)],
28
+ check=False,
29
+ cwd=bakefile_path.parent,
30
+ echo=False,
31
+ )
32
+
33
+ is_already_pep723 = result.returncode == 2 and "is already a PEP 723 script" in result.stderr
34
+
35
+ is_valid_output = result.returncode == 0 or is_already_pep723
36
+
37
+ if not is_valid_output:
38
+ command: str = " ".join(result.args)
39
+ logger.error(f"Failed to initialize PEP 723 metadata for {bakefile_path}")
40
+ raise BakebookError(
41
+ f"Failed to initialize PEP 723 metadata.\n\n"
42
+ f"Command: `{command}`\n\n"
43
+ f"Error: {result.stderr.strip()}"
44
+ )
45
+
46
+ if is_already_pep723:
47
+ console.warning(f"{bakefile_path.name} already has PEP 723 metadata")
48
+
49
+ run_uv(
50
+ ["add", "bakefile", "--script", str(bakefile_path.name)],
51
+ cwd=bakefile_path.parent,
52
+ echo=False,
53
+ )
54
+
55
+
56
+ def read_inline(bakefile_path: Path) -> dict[str, Any] | None:
57
+ inline_regex = r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
58
+ script = bakefile_path.read_text()
59
+ name = "script"
60
+ matches = list(filter(lambda m: m.group("type") == name, re.finditer(inline_regex, script)))
61
+
62
+ if len(matches) > 1:
63
+ raise ValueError(f"Multiple {name} blocks found")
64
+ elif len(matches) == 1:
65
+ content = "".join(
66
+ line[2:] if line.startswith("# ") else line[1:]
67
+ for line in matches[0].group("content").splitlines(keepends=True)
68
+ )
69
+ return tomllib.loads(content)
70
+ else:
71
+ return None