From f72d1d4e0e12181f3f0ae27ff73f62fc4aad6736 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Mon, 6 Jul 2026 11:37:51 -0700 Subject: [PATCH] refactor: split manifest section readers --- cli/python/base_setup/manifest.py | 298 +----------------- cli/python/base_setup/manifest_build.py | 117 +++++++ .../base_setup/manifest_reader_common.py | 19 ++ cli/python/base_setup/manifest_release.py | 173 ++++++++++ .../tests/test_manifest_structure.py | 22 ++ 5 files changed, 341 insertions(+), 288 deletions(-) create mode 100644 cli/python/base_setup/manifest_build.py create mode 100644 cli/python/base_setup/manifest_reader_common.py create mode 100644 cli/python/base_setup/manifest_release.py create mode 100644 cli/python/base_setup/tests/test_manifest_structure.py diff --git a/cli/python/base_setup/manifest.py b/cli/python/base_setup/manifest.py index ac38578..aa9f3ab 100644 --- a/cli/python/base_setup/manifest.py +++ b/cli/python/base_setup/manifest.py @@ -10,34 +10,33 @@ from base_setup.github_manifest import GithubConfig from base_setup.github_manifest import GithubManifestError from base_setup.github_manifest import read_github_config +from base_setup.manifest_build import read_build_config from base_setup.manifest_loader import ManifestError from base_setup.manifest_loader import read_manifest_mapping from base_setup.manifest_loader import yaml # pylint: disable=unused-import from base_setup.manifest_model import ActivateConfig from base_setup.manifest_model import ArtifactRequest from base_setup.manifest_model import BaseManifest -from base_setup.manifest_model import BuildConfig -from base_setup.manifest_model import BuildTargetConfig +from base_setup.manifest_model import BuildConfig # pylint: disable=unused-import +from base_setup.manifest_model import BuildTargetConfig # pylint: disable=unused-import from base_setup.manifest_model import CommandConfig from base_setup.manifest_model import DemoConfig from base_setup.manifest_model import HealthConfig from base_setup.manifest_model import IdeConfig from base_setup.manifest_model import PortHealthConfig from base_setup.manifest_model import PythonConfig -from base_setup.manifest_model import ReleaseConfig -from base_setup.manifest_model import ReleaseGithubConfig -from base_setup.manifest_model import ReleaseHomebrewConfig +from base_setup.manifest_model import ReleaseConfig # pylint: disable=unused-import +from base_setup.manifest_model import ReleaseGithubConfig # pylint: disable=unused-import +from base_setup.manifest_model import ReleaseHomebrewConfig # pylint: disable=unused-import from base_setup.manifest_model import TestConfig +from base_setup.manifest_reader_common import read_optional_runner as _read_optional_runner +from base_setup.manifest_release import read_release_config from base_setup.manifest_schema import COMMAND_NAME_RE from base_setup.manifest_schema import CURRENT_MANIFEST_SCHEMA_VERSION from base_setup.manifest_schema import ENVIRONMENT_VARIABLE_NAME_RE -from base_setup.manifest_schema import GITHUB_REPOSITORY_RE -from base_setup.manifest_schema import HOMEBREW_PACKAGE_RE from base_setup.manifest_schema import PORT_HEALTH_STATES -from base_setup.manifest_schema import SUPPORTED_COMMAND_RUNNERS from base_setup.manifest_schema import SUPPORTED_PYTHON_MANAGERS from base_setup.manifest_schema import has_control_line_break -from base_setup.release_title import release_title_template_error def read_manifest(path: Path) -> BaseManifest: @@ -54,8 +53,8 @@ def read_manifest(path: Path) -> BaseManifest: python = _read_python(path, data.get("python")) github = _read_github(path, data.get("github")) demo = _read_demo(path, data.get("demo")) - build = _read_build(path, data.get("build")) - release = _read_release(path, data.get("release")) + build = read_build_config(path, data.get("build")) + release = read_release_config(path, data.get("release")) artifacts = _read_artifacts(path, data.get("artifacts", [])) return BaseManifest( @@ -203,271 +202,6 @@ def _read_demo(path: Path, demo_data: Any) -> DemoConfig | None: return DemoConfig(script=script, description=description, runner=runner) -def _read_build(path: Path, build_data: Any) -> BuildConfig | None: - if build_data is None: - return None - if not isinstance(build_data, dict): - raise ManifestError(f"{path}: build must be a mapping when provided.") - - allowed_keys = {"default", "targets"} - unknown_keys = sorted(set(build_data) - allowed_keys) - if unknown_keys: - raise ManifestError(f"{path}: build has unsupported keys: {', '.join(unknown_keys)}.") - - targets = _read_build_targets(path, build_data.get("targets", {})) - default = _read_build_default(path, build_data.get("default", []), targets) - return BuildConfig(default=default, targets=targets) - - -def _read_release(path: Path, release_data: Any) -> ReleaseConfig | None: - if release_data is None: - return None - if not isinstance(release_data, dict): - raise ManifestError(f"{path}: release must be a mapping when provided.") - - allowed_keys = {"version_file", "changelog", "tag_prefix", "github", "homebrew", "runner"} - unknown_keys = sorted(set(release_data) - allowed_keys) - if unknown_keys: - raise ManifestError(f"{path}: release has unsupported keys: {', '.join(unknown_keys)}.") - - version_file = _read_release_relative_path( - path, - "release.version_file", - release_data.get("version_file", "VERSION"), - ) - changelog = _read_release_relative_path( - path, - "release.changelog", - release_data.get("changelog", "CHANGELOG.md"), - ) - tag_prefix = _read_release_string(path, "release.tag_prefix", release_data.get("tag_prefix", "v")) - github = _read_release_github(path, release_data.get("github")) - homebrew = _read_release_homebrew(path, release_data.get("homebrew")) - - return ReleaseConfig( - version_file=version_file, - changelog=changelog, - tag_prefix=tag_prefix, - github=github, - homebrew=homebrew, - runner=_read_optional_runner(path, "release.runner", release_data.get("runner")), - ) - - -def _read_release_github(path: Path, github_data: Any) -> ReleaseGithubConfig: - if not isinstance(github_data, dict): - raise ManifestError(f"{path}: release.github must be a mapping.") - - allowed_keys = {"repository", "release_title"} - unknown_keys = sorted(set(github_data) - allowed_keys) - if unknown_keys: - raise ManifestError(f"{path}: release.github has unsupported keys: {', '.join(unknown_keys)}.") - - repository = _read_release_repository(path, "release.github.repository", github_data.get("repository")) - release_title = _read_release_title( - path, - "release.github.release_title", - github_data.get("release_title", "{repository} v{version}"), - ) - return ReleaseGithubConfig(repository=repository, release_title=release_title) - - -def _read_release_homebrew(path: Path, homebrew_data: Any) -> ReleaseHomebrewConfig | None: - if homebrew_data is None: - return None - if not isinstance(homebrew_data, dict): - raise ManifestError(f"{path}: release.homebrew must be a mapping when provided.") - - allowed_keys = {"required", "tap_repository", "formula_path", "package"} - unknown_keys = sorted(set(homebrew_data) - allowed_keys) - if unknown_keys: - raise ManifestError(f"{path}: release.homebrew has unsupported keys: {', '.join(unknown_keys)}.") - - required = homebrew_data.get("required", True) - if not isinstance(required, bool): - raise ManifestError(f"{path}: release.homebrew.required must be a boolean when provided.") - - tap_repository = _read_optional_release_repository( - path, - "release.homebrew.tap_repository", - homebrew_data.get("tap_repository"), - ) - formula_path = _read_optional_release_relative_path( - path, - "release.homebrew.formula_path", - homebrew_data.get("formula_path"), - ) - package = _read_optional_homebrew_package( - path, - "release.homebrew.package", - homebrew_data.get("package"), - ) - - if required: - missing = [ - field_name - for field_name, field_value in ( - ("tap_repository", tap_repository), - ("formula_path", formula_path), - ("package", package), - ) - if field_value is None - ] - if missing: - raise ManifestError( - f"{path}: release.homebrew required fields are missing: {', '.join(missing)}." - ) - - return ReleaseHomebrewConfig( - required=required, - tap_repository=tap_repository, - formula_path=formula_path, - package=package, - ) - - -def _read_release_repository(path: Path, field_name: str, value: Any) -> str: - repository = _read_release_string(path, field_name, value) - if not GITHUB_REPOSITORY_RE.fullmatch(repository): - raise ManifestError(f"{path}: {field_name} must use owner/name format.") - return repository - - -def _read_optional_release_repository(path: Path, field_name: str, value: Any) -> str | None: - if value is None: - return None - return _read_release_repository(path, field_name, value) - - -def _read_optional_homebrew_package(path: Path, field_name: str, value: Any) -> str | None: - if value is None: - return None - package = _read_release_string(path, field_name, value) - if not HOMEBREW_PACKAGE_RE.fullmatch(package): - raise ManifestError(f"{path}: {field_name} must use owner/tap/formula format.") - return package - - -def _read_release_relative_path(path: Path, field_name: str, value: Any) -> str: - release_path = _read_release_string(path, field_name, value) - parsed_path = Path(release_path) - if parsed_path.is_absolute() or ".." in parsed_path.parts: - raise ManifestError(f"{path}: {field_name} must be a relative path inside the project.") - return release_path - - -def _read_optional_release_relative_path(path: Path, field_name: str, value: Any) -> str | None: - if value is None: - return None - return _read_release_relative_path(path, field_name, value) - - -def _read_release_string(path: Path, field_name: str, value: Any) -> str: - if not isinstance(value, str) or not value.strip(): - raise ManifestError(f"{path}: {field_name} must be a non-empty string.") - value = value.strip() - if has_control_line_break(value): - raise ManifestError(f"{path}: {field_name} must not contain control line breaks.") - return value - - -def _read_release_title(path: Path, field_name: str, value: Any) -> str: - title = _read_release_string(path, field_name, value) - if error := release_title_template_error(title): - raise ManifestError(f"{path}: {field_name} {error}") - return title - - -def _read_build_default( - path: Path, - default_data: Any, - targets: dict[str, BuildTargetConfig], -) -> tuple[str, ...]: - if default_data is None: - return () - if not isinstance(default_data, list): - raise ManifestError(f"{path}: build.default must be a list when provided.") - - default: list[str] = [] - seen: set[str] = set() - for index, target_data in enumerate(default_data, start=1): - if not isinstance(target_data, str) or not target_data.strip(): - raise ManifestError(f"{path}: build.default[{index}] must be a non-empty string.") - target_name = target_data.strip() - if not COMMAND_NAME_RE.fullmatch(target_name): - raise ManifestError(f"{path}: build.default[{index}] must be a valid target name.") - if target_name in seen: - raise ManifestError(f"{path}: build.default[{index}] duplicates '{target_name}'.") - if target_name not in targets: - raise ManifestError(f"{path}: build.default[{index}] references unknown target '{target_name}'.") - seen.add(target_name) - default.append(target_name) - return tuple(default) - - -def _read_build_targets(path: Path, targets_data: Any) -> dict[str, BuildTargetConfig]: - if targets_data is None: - return {} - if not isinstance(targets_data, dict): - raise ManifestError(f"{path}: build.targets must be a mapping when provided.") - - targets: dict[str, BuildTargetConfig] = {} - for target_name_data, target_data in targets_data.items(): - if not isinstance(target_name_data, str) or not target_name_data.strip(): - raise ManifestError(f"{path}: build.targets keys must be non-empty strings.") - target_name = target_name_data.strip() - if not COMMAND_NAME_RE.fullmatch(target_name): - raise ManifestError(f"{path}: build.targets.{target_name} must be a valid target name.") - if target_name in targets: - raise ManifestError(f"{path}: build.targets duplicates '{target_name}'.") - targets[target_name] = _read_build_target(path, target_name, target_data) - return targets - - -def _read_build_target(path: Path, target_name: str, target_data: Any) -> BuildTargetConfig: - if not isinstance(target_data, dict): - raise ManifestError(f"{path}: build.targets.{target_name} must be a mapping.") - - allowed_keys = {"command", "working_dir", "description", "runner"} - unknown_keys = sorted(set(target_data) - allowed_keys) - if unknown_keys: - raise ManifestError( - f"{path}: build.targets.{target_name} has unsupported keys: {', '.join(unknown_keys)}." - ) - - command = target_data.get("command") - if not isinstance(command, str) or not command.strip(): - raise ManifestError(f"{path}: build.targets.{target_name}.command must be a non-empty string.") - command = command.strip() - if has_control_line_break(command): - raise ManifestError(f"{path}: build.targets.{target_name}.command must not contain control line breaks.") - - working_dir = target_data.get("working_dir", ".") - if not isinstance(working_dir, str) or not working_dir.strip(): - raise ManifestError(f"{path}: build.targets.{target_name}.working_dir must be a non-empty string.") - working_dir = working_dir.strip() - if has_control_line_break(working_dir): - raise ManifestError( - f"{path}: build.targets.{target_name}.working_dir must not contain control line breaks." - ) - - description = target_data.get("description") - if description is not None: - if not isinstance(description, str) or not description.strip(): - raise ManifestError( - f"{path}: build.targets.{target_name}.description must be a non-empty string when provided." - ) - description = description.strip() - if has_control_line_break(description): - raise ManifestError( - f"{path}: build.targets.{target_name}.description must not contain control line breaks." - ) - - runner = _read_optional_runner(path, f"build.targets.{target_name}.runner", target_data.get("runner")) - - return BuildTargetConfig(command=command, working_dir=working_dir, description=description, runner=runner) - - def _read_health(path: Path, health_data: Any) -> HealthConfig: if health_data is None: return HealthConfig() @@ -582,18 +316,6 @@ def _read_github(path: Path, github_data: Any) -> GithubConfig: raise ManifestError(str(exc)) from exc -def _read_optional_runner(path: Path, field_name: str, runner_data: Any) -> str | None: - if runner_data is None: - return None - if not isinstance(runner_data, str) or not runner_data.strip(): - raise ManifestError(f"{path}: {field_name} must be a non-empty string when provided.") - runner = runner_data.strip() - if runner not in SUPPORTED_COMMAND_RUNNERS: - supported = ", ".join(sorted(SUPPORTED_COMMAND_RUNNERS)) - raise ManifestError(f"{path}: {field_name} must be one of: {supported}.") - return runner - - def _read_activate_sources(path: Path, source_data: Any) -> tuple[str, ...]: if source_data is None: return () diff --git a/cli/python/base_setup/manifest_build.py b/cli/python/base_setup/manifest_build.py new file mode 100644 index 0000000..dc14cd6 --- /dev/null +++ b/cli/python/base_setup/manifest_build.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from base_setup.manifest_loader import ManifestError +from base_setup.manifest_model import BuildConfig +from base_setup.manifest_model import BuildTargetConfig +from base_setup.manifest_reader_common import read_optional_runner +from base_setup.manifest_schema import COMMAND_NAME_RE +from base_setup.manifest_schema import has_control_line_break + + +def read_build_config(path: Path, build_data: Any) -> BuildConfig | None: + if build_data is None: + return None + if not isinstance(build_data, dict): + raise ManifestError(f"{path}: build must be a mapping when provided.") + + allowed_keys = {"default", "targets"} + unknown_keys = sorted(set(build_data) - allowed_keys) + if unknown_keys: + raise ManifestError(f"{path}: build has unsupported keys: {', '.join(unknown_keys)}.") + + targets = _read_build_targets(path, build_data.get("targets", {})) + default = _read_build_default(path, build_data.get("default", []), targets) + return BuildConfig(default=default, targets=targets) + + +def _read_build_default( + path: Path, + default_data: Any, + targets: dict[str, BuildTargetConfig], +) -> tuple[str, ...]: + if default_data is None: + return () + if not isinstance(default_data, list): + raise ManifestError(f"{path}: build.default must be a list when provided.") + + default: list[str] = [] + seen: set[str] = set() + for index, target_data in enumerate(default_data, start=1): + if not isinstance(target_data, str) or not target_data.strip(): + raise ManifestError(f"{path}: build.default[{index}] must be a non-empty string.") + target_name = target_data.strip() + if not COMMAND_NAME_RE.fullmatch(target_name): + raise ManifestError(f"{path}: build.default[{index}] must be a valid target name.") + if target_name in seen: + raise ManifestError(f"{path}: build.default[{index}] duplicates '{target_name}'.") + if target_name not in targets: + raise ManifestError(f"{path}: build.default[{index}] references unknown target '{target_name}'.") + seen.add(target_name) + default.append(target_name) + return tuple(default) + + +def _read_build_targets(path: Path, targets_data: Any) -> dict[str, BuildTargetConfig]: + if targets_data is None: + return {} + if not isinstance(targets_data, dict): + raise ManifestError(f"{path}: build.targets must be a mapping when provided.") + + targets: dict[str, BuildTargetConfig] = {} + for target_name_data, target_data in targets_data.items(): + if not isinstance(target_name_data, str) or not target_name_data.strip(): + raise ManifestError(f"{path}: build.targets keys must be non-empty strings.") + target_name = target_name_data.strip() + if not COMMAND_NAME_RE.fullmatch(target_name): + raise ManifestError(f"{path}: build.targets.{target_name} must be a valid target name.") + if target_name in targets: + raise ManifestError(f"{path}: build.targets duplicates '{target_name}'.") + targets[target_name] = _read_build_target(path, target_name, target_data) + return targets + + +def _read_build_target(path: Path, target_name: str, target_data: Any) -> BuildTargetConfig: + if not isinstance(target_data, dict): + raise ManifestError(f"{path}: build.targets.{target_name} must be a mapping.") + + allowed_keys = {"command", "working_dir", "description", "runner"} + unknown_keys = sorted(set(target_data) - allowed_keys) + if unknown_keys: + raise ManifestError( + f"{path}: build.targets.{target_name} has unsupported keys: {', '.join(unknown_keys)}." + ) + + command = target_data.get("command") + if not isinstance(command, str) or not command.strip(): + raise ManifestError(f"{path}: build.targets.{target_name}.command must be a non-empty string.") + command = command.strip() + if has_control_line_break(command): + raise ManifestError(f"{path}: build.targets.{target_name}.command must not contain control line breaks.") + + working_dir = target_data.get("working_dir", ".") + if not isinstance(working_dir, str) or not working_dir.strip(): + raise ManifestError(f"{path}: build.targets.{target_name}.working_dir must be a non-empty string.") + working_dir = working_dir.strip() + if has_control_line_break(working_dir): + raise ManifestError( + f"{path}: build.targets.{target_name}.working_dir must not contain control line breaks." + ) + + description = target_data.get("description") + if description is not None: + if not isinstance(description, str) or not description.strip(): + raise ManifestError( + f"{path}: build.targets.{target_name}.description must be a non-empty string when provided." + ) + description = description.strip() + if has_control_line_break(description): + raise ManifestError( + f"{path}: build.targets.{target_name}.description must not contain control line breaks." + ) + + runner = read_optional_runner(path, f"build.targets.{target_name}.runner", target_data.get("runner")) + + return BuildTargetConfig(command=command, working_dir=working_dir, description=description, runner=runner) diff --git a/cli/python/base_setup/manifest_reader_common.py b/cli/python/base_setup/manifest_reader_common.py new file mode 100644 index 0000000..109b72b --- /dev/null +++ b/cli/python/base_setup/manifest_reader_common.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from base_setup.manifest_loader import ManifestError +from base_setup.manifest_schema import SUPPORTED_COMMAND_RUNNERS + + +def read_optional_runner(path: Path, field_name: str, runner_data: Any) -> str | None: + if runner_data is None: + return None + if not isinstance(runner_data, str) or not runner_data.strip(): + raise ManifestError(f"{path}: {field_name} must be a non-empty string when provided.") + runner = runner_data.strip() + if runner not in SUPPORTED_COMMAND_RUNNERS: + supported = ", ".join(sorted(SUPPORTED_COMMAND_RUNNERS)) + raise ManifestError(f"{path}: {field_name} must be one of: {supported}.") + return runner diff --git a/cli/python/base_setup/manifest_release.py b/cli/python/base_setup/manifest_release.py new file mode 100644 index 0000000..0ac3143 --- /dev/null +++ b/cli/python/base_setup/manifest_release.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from base_setup.manifest_loader import ManifestError +from base_setup.manifest_model import ReleaseConfig +from base_setup.manifest_model import ReleaseGithubConfig +from base_setup.manifest_model import ReleaseHomebrewConfig +from base_setup.manifest_reader_common import read_optional_runner +from base_setup.manifest_schema import GITHUB_REPOSITORY_RE +from base_setup.manifest_schema import HOMEBREW_PACKAGE_RE +from base_setup.manifest_schema import has_control_line_break +from base_setup.release_title import release_title_template_error + + +def read_release_config(path: Path, release_data: Any) -> ReleaseConfig | None: + if release_data is None: + return None + if not isinstance(release_data, dict): + raise ManifestError(f"{path}: release must be a mapping when provided.") + + allowed_keys = {"version_file", "changelog", "tag_prefix", "github", "homebrew", "runner"} + unknown_keys = sorted(set(release_data) - allowed_keys) + if unknown_keys: + raise ManifestError(f"{path}: release has unsupported keys: {', '.join(unknown_keys)}.") + + version_file = _read_release_relative_path( + path, + "release.version_file", + release_data.get("version_file", "VERSION"), + ) + changelog = _read_release_relative_path( + path, + "release.changelog", + release_data.get("changelog", "CHANGELOG.md"), + ) + tag_prefix = _read_release_string(path, "release.tag_prefix", release_data.get("tag_prefix", "v")) + github = _read_release_github(path, release_data.get("github")) + homebrew = _read_release_homebrew(path, release_data.get("homebrew")) + + return ReleaseConfig( + version_file=version_file, + changelog=changelog, + tag_prefix=tag_prefix, + github=github, + homebrew=homebrew, + runner=read_optional_runner(path, "release.runner", release_data.get("runner")), + ) + + +def _read_release_github(path: Path, github_data: Any) -> ReleaseGithubConfig: + if not isinstance(github_data, dict): + raise ManifestError(f"{path}: release.github must be a mapping.") + + allowed_keys = {"repository", "release_title"} + unknown_keys = sorted(set(github_data) - allowed_keys) + if unknown_keys: + raise ManifestError(f"{path}: release.github has unsupported keys: {', '.join(unknown_keys)}.") + + repository = _read_release_repository(path, "release.github.repository", github_data.get("repository")) + release_title = _read_release_title( + path, + "release.github.release_title", + github_data.get("release_title", "{repository} v{version}"), + ) + return ReleaseGithubConfig(repository=repository, release_title=release_title) + + +def _read_release_homebrew(path: Path, homebrew_data: Any) -> ReleaseHomebrewConfig | None: + if homebrew_data is None: + return None + if not isinstance(homebrew_data, dict): + raise ManifestError(f"{path}: release.homebrew must be a mapping when provided.") + + allowed_keys = {"required", "tap_repository", "formula_path", "package"} + unknown_keys = sorted(set(homebrew_data) - allowed_keys) + if unknown_keys: + raise ManifestError(f"{path}: release.homebrew has unsupported keys: {', '.join(unknown_keys)}.") + + required = homebrew_data.get("required", True) + if not isinstance(required, bool): + raise ManifestError(f"{path}: release.homebrew.required must be a boolean when provided.") + + tap_repository = _read_optional_release_repository( + path, + "release.homebrew.tap_repository", + homebrew_data.get("tap_repository"), + ) + formula_path = _read_optional_release_relative_path( + path, + "release.homebrew.formula_path", + homebrew_data.get("formula_path"), + ) + package = _read_optional_homebrew_package( + path, + "release.homebrew.package", + homebrew_data.get("package"), + ) + + if required: + missing = [ + field_name + for field_name, field_value in ( + ("tap_repository", tap_repository), + ("formula_path", formula_path), + ("package", package), + ) + if field_value is None + ] + if missing: + raise ManifestError( + f"{path}: release.homebrew required fields are missing: {', '.join(missing)}." + ) + + return ReleaseHomebrewConfig( + required=required, + tap_repository=tap_repository, + formula_path=formula_path, + package=package, + ) + + +def _read_release_repository(path: Path, field_name: str, value: Any) -> str: + repository = _read_release_string(path, field_name, value) + if not GITHUB_REPOSITORY_RE.fullmatch(repository): + raise ManifestError(f"{path}: {field_name} must use owner/name format.") + return repository + + +def _read_optional_release_repository(path: Path, field_name: str, value: Any) -> str | None: + if value is None: + return None + return _read_release_repository(path, field_name, value) + + +def _read_optional_homebrew_package(path: Path, field_name: str, value: Any) -> str | None: + if value is None: + return None + package = _read_release_string(path, field_name, value) + if not HOMEBREW_PACKAGE_RE.fullmatch(package): + raise ManifestError(f"{path}: {field_name} must use owner/tap/formula format.") + return package + + +def _read_release_relative_path(path: Path, field_name: str, value: Any) -> str: + release_path = _read_release_string(path, field_name, value) + parsed_path = Path(release_path) + if parsed_path.is_absolute() or ".." in parsed_path.parts: + raise ManifestError(f"{path}: {field_name} must be a relative path inside the project.") + return release_path + + +def _read_optional_release_relative_path(path: Path, field_name: str, value: Any) -> str | None: + if value is None: + return None + return _read_release_relative_path(path, field_name, value) + + +def _read_release_string(path: Path, field_name: str, value: Any) -> str: + if not isinstance(value, str) or not value.strip(): + raise ManifestError(f"{path}: {field_name} must be a non-empty string.") + value = value.strip() + if has_control_line_break(value): + raise ManifestError(f"{path}: {field_name} must not contain control line breaks.") + return value + + +def _read_release_title(path: Path, field_name: str, value: Any) -> str: + title = _read_release_string(path, field_name, value) + if error := release_title_template_error(title): + raise ManifestError(f"{path}: {field_name} {error}") + return title diff --git a/cli/python/base_setup/tests/test_manifest_structure.py b/cli/python/base_setup/tests/test_manifest_structure.py new file mode 100644 index 0000000..e7ffb32 --- /dev/null +++ b/cli/python/base_setup/tests/test_manifest_structure.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from pathlib import Path +import unittest + +from base_setup import manifest +from base_setup import manifest_build +from base_setup import manifest_release + + +class ManifestReaderStructureTests(unittest.TestCase): + def test_build_and_release_readers_live_outside_manifest_facade(self) -> None: + manifest_source = Path(manifest.__file__).read_text(encoding="utf-8") + build_source = Path(manifest_build.__file__).read_text(encoding="utf-8") + release_source = Path(manifest_release.__file__).read_text(encoding="utf-8") + + self.assertIn("def read_build_config", build_source) + self.assertIn("def read_release_config", release_source) + self.assertNotIn("def _read_build(", manifest_source) + self.assertNotIn("def _read_build_target", manifest_source) + self.assertNotIn("def _read_release(", manifest_source) + self.assertNotIn("def _read_release_github", manifest_source)