Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions cli/python/base_cli_adapters/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from typing import Any

from base_cli import ConfigurationError
from base_cli.config import load_yaml_file as load_cli_yaml_file
from base_setup.ide_schema import parse_ide_extensions
from base_setup.ide_schema import parse_ide_settings
Expand Down Expand Up @@ -67,14 +68,17 @@ def read_user_config(
*,
supported_ides: frozenset[str] | None = SUPPORTED_IDES,
) -> UserConfig:
raw = load_user_config(home)
path = user_config_path(home)
return UserConfig(
raw=raw,
workspace=_read_user_workspace_config(path, raw.get("workspace")),
github=_read_user_github_config(path, raw.get("github")),
ide=_read_user_ide_config(path, raw.get("ide"), supported_ides=supported_ides),
)
try:
raw = load_user_config(home)
path = user_config_path(home)
return UserConfig(
raw=raw,
workspace=_read_user_workspace_config(path, raw.get("workspace")),
github=_read_user_github_config(path, raw.get("github")),
ide=_read_user_ide_config(path, raw.get("ide"), supported_ides=supported_ides),
)
except ValueError as exc:
raise ConfigurationError(str(exc)) from exc


def _read_user_workspace_config(path: Path, workspace_data: Any) -> UserWorkspaceConfig:
Expand Down
2 changes: 1 addition & 1 deletion cli/python/base_cli_adapters/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pathlib import Path

from base_cli._runtime import RuntimeLayout
from base_cli.runtime import RuntimeLayout

from .paths import runtime_owner_root
from .paths import runtime_run_directory_name
Expand Down
18 changes: 18 additions & 0 deletions cli/python/base_github_projects/project_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def parse_project_options(
state.allow_cross_repo = True
index += 1
continue
if apply_inline_option(state, arg, allow_fields=allow_fields):
index += 1
continue
consumed = apply_spaced_option(state, remaining, index, allow_fields=allow_fields)
if consumed:
index += consumed
Expand Down Expand Up @@ -183,6 +186,21 @@ def apply_spaced_option(state: OptionState, remaining: list[str], index: int, *,
return OPTION_NOT_CONSUMED


def apply_inline_option(state: OptionState, argument: str, *, allow_fields: bool) -> bool:
"""Apply Click-compatible ``--option=value`` syntax to delegated options."""
if not argument.startswith("--") or "=" not in argument:
return False

option, value = argument.split("=", 1)
if option in PROJECT_VALUE_OPTIONS:
apply_project_option(state, option, value)
return True
if allow_fields and option in ISSUE_FIELD_OPTIONS:
state.field_values[option[2:]] = value
return True
return False


def option_value(remaining: list[str], index: int) -> str:
option = remaining[index]
if index + 1 >= len(remaining):
Expand Down
18 changes: 6 additions & 12 deletions cli/python/base_github_projects/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,13 @@ def test_delegated_usage_uses_basectl_gh_project_prefix(
assert "base_github_projects" not in captured.err


def test_main_rejects_equals_form_project_options(
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("BASE_CACHE_DIR", str(tmp_path / ".cache" / "base"))

status = engine.main(["project", "configure", "--project=Base Roadmap", "--dry-run"])
def test_parse_project_configure_accepts_equals_form_options() -> None:
args = engine.parse_args(
("project", "configure", "--project=Base Roadmap", "--dry-run"),
)

captured = capsys.readouterr()
assert status == 2
assert "Option '--project' uses unsupported equals syntax." in captured.err
assert args.project_title == "Base Roadmap"
assert args.dry_run is True


def test_parse_project_configure_arguments() -> None:
Expand Down
4 changes: 3 additions & 1 deletion cli/python/base_history/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def test_text_output_lists_recent_history_and_missing_log_marker(self) -> None:
)

status, stdout, stderr = invoke([], cache_root)
records = engine.recent_history(cache_root)

self.assertEqual(status, 0)
self.assertEqual(stderr, "")
Expand All @@ -117,7 +118,8 @@ def test_text_output_lists_recent_history_and_missing_log_marker(self) -> None:
self.assertIn("PROJECT", stdout)
self.assertIn("check", stdout)
self.assertIn("error", stdout)
self.assertIn("missing", stdout)
self.assertEqual(len(records), 1)
self.assertTrue(engine.display_log_path(records[0]).endswith(" (missing)"))

def test_text_table_expands_columns_for_long_command_and_project(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
Expand Down
9 changes: 6 additions & 3 deletions cli/python/base_pr_policy/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ def test_explicit_manifest_populates_history_project_metadata(tmp_path) -> None:
assert record["manifest"] == str(manifest_path.resolve())


def test_main_rejects_equals_form_options(capsys) -> None:
def test_main_accepts_equals_form_options(capsys, monkeypatch, tmp_path) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("BASE_CACHE_DIR", str(tmp_path / ".cache" / "base"))

status = engine.main(["body", "--issue=403"])

captured = capsys.readouterr()
assert status == 2
assert "Option '--issue' uses unsupported equals syntax." in captured.err
assert status == 0
assert "Fixes #403" in captured.out


def test_render_pr_body_uses_default_label_and_path_sections() -> None:
Expand Down
2 changes: 1 addition & 1 deletion cli/python/base_projects/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ def test_main_reports_config_errors_without_traceback(self) -> None:
user_config="workspace: [not-a-mapping]\n",
)

self.assertEqual(status, 1)
self.assertEqual(status, 2)
self.assertIn("workspace must be a mapping", stderr)
self.assertNotIn("Traceback", stderr)

Expand Down
2 changes: 1 addition & 1 deletion cli/python/base_trust/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_require_explicit_manifest_populates_history_project_metadata(self) -> N
env={"BASE_HOME": str(root / "base")},
)

self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exit_code, 1)
self.assertIn("Manifest-declared commands are not allowed", result.stderr)
self.assertEqual(len(captured), 1)
record = build_finished_record(*captured[0])
Expand Down
20 changes: 12 additions & 8 deletions docs/base-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ if __name__ == "__main__":

The command function receives `ctx` as its first argument. Infrastructure is
created immediately before command execution and cleaned up afterward.
`base_cli.run_app()` applies Base's command syntax guard before Click parses
arguments: long options with values must use space-separated syntax, such as
`--name Ada`; equals-form values such as `--name=Ada` are rejected.
`base_cli.run_app()` preserves Click's native option syntax, including both
space-separated values such as `--name Ada` and equals-form values such as
`--name=Ada`.

## Package Layout

Expand Down Expand Up @@ -141,7 +141,6 @@ registration.
```python
ctx.cli_name # str
ctx.run_id # str
ctx.base_home # Path | None
ctx.application_home # Path | None; neutral application-home alias
ctx.project_name # selected project name, or None
ctx.project_root # Path | None
Expand All @@ -157,14 +156,20 @@ ctx.log_dir # run_root/logs
ctx.cache_dir # owner_root/cache/components/<cli-name>
ctx.temp_dir # run_root/tmp/<cli-name>/<run-id>
ctx.log_file # run_root/logs/primary.log, or None when disabled
ctx.config # dict
ctx.config # consumer-owned configuration payload
ctx.framework_config # validated framework lifecycle settings, or None
ctx.config_provenance # configuration source mapping, when provided
ctx.user_config # typed user config from ~/.base.d/config.yaml
ctx.application_context # optional consumer application state
ctx.services # optional consumer services
ctx.history_display_command # consumer policy for persisted command labels
ctx.environment # str
ctx.debug # bool
ctx.dry_run # bool
ctx.keep_temp # bool
ctx.quiet # bool
ctx.json_output # bool
ctx.rich # bool; optional Rich integration enabled
ctx.log # logging.Logger
```

Expand Down Expand Up @@ -368,9 +373,8 @@ Direct `base_cli.App` command packages get:
| `--version` | show the CLI version when configured |
| `--help` | Click help |

Long option values must use the space-separated form, for example
`--environment prod`. Base rejects `--option=value` before Click parses
arguments.
Long option values accept either Click form, for example `--environment prod`
or `--environment=prod`.

These are direct Python package options. Public `basectl` launchers expose
`-v` for command-level debug logs and command-specific flags from
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pylint==3.3.9
click==8.4.1
base-cli==0.2.0
base-cli==0.4.0
PyYAML==6.0.3
pytest==9.0.3
pytest-cov==7.1.0
Expand Down
8 changes: 7 additions & 1 deletion tests/test_base_cli_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

REPO_ROOT = Path(__file__).resolve().parents[1]
BASE_CLI_DOC = REPO_ROOT / "docs" / "base-cli.md"
INTERNAL_CONTEXT_FIELDS = {"cleanup_hooks"}
INTERNAL_CONTEXT_FIELDS = {
"cleanup_hooks",
"_run_metadata_path",
"_owns_temp_dir",
"_owned_temp_identity",
"_owned_temp_descriptor",
}


def context_section() -> str:
Expand Down
Loading