Skip to content

[v1.0] Resolve the inert CLI identity parameter in BatteriesIncludedConfigLoader #313

Description

@codeforester

Goal

Remove the misleading required cli_name argument from the public configuration-loader API or give it a documented, testable identity role before the v1 facade is frozen.

Background

BatteriesIncludedConfigLoader publicly requires cli_name and stores it as self.cli_name:

class BatteriesIncludedConfigLoader:
"""Load conventional user, project, environment, and explicit layers."""
def __init__(
self,
cli_name: str,
*,
user_config_dir: Path,
user_config_name: str = "config.yaml",
project_config_name: str = ".base-cli.yaml",
environment_dir_name: str = "environments",
) -> None:
if _SAFE_FILENAME.fullmatch(user_config_name) is None:
raise ValueError("user_config_name must be a simple filename")
if _SAFE_FILENAME.fullmatch(project_config_name) is None:
raise ValueError("project_config_name must be a simple filename")
if _SAFE_NAME.fullmatch(environment_dir_name) is None:
raise ValueError("environment_dir_name must be a simple directory name")
self.cli_name = cli_name
self.user_config_dir = user_config_dir.expanduser()
self.user_config_name = user_config_name
self.project_config_name = project_config_name
self.environment_dir_name = environment_dir_name

No loader method reads that attribute. Configuration paths, merge precedence, validation, and returned provenance are identical for any two cli_name values when the explicit path arguments are the same:

@property
def user_config_path(self) -> Path:
return self.user_config_dir / self.user_config_name
def project_config_path(self, project_root: Path | None) -> Path | None:
if project_root is None:
return None
return project_root / self.project_config_name
def _environment_paths(
self,
project_root: Path | None,
environment: str,
) -> tuple[Path, Path | None]:
user_path = self.user_config_dir / self.environment_dir_name / f"{environment}.yaml"
project_path = (
project_root / self.environment_dir_name / f"{environment}.yaml" if project_root is not None else None
)
return user_path, project_path
def load(
self,
project_root: Path | None,
explicit_path: Path | None,
*,
environment: str | None = None,
) -> ConfigSnapshot:
user_values = load_yaml_file(self.user_config_path)
project_path = self.project_config_path(project_root)
project_values = load_yaml_file(project_path) if project_path is not None else {}
explicit_values = load_yaml_file(explicit_path, required=True) if explicit_path is not None else {}
selected_environment = environment
if selected_environment is None:
candidate = explicit_values.get("environment")
if candidate is None:
candidate = project_values.get("environment", user_values.get("environment"))
selected_environment = candidate if candidate is not None else DEFAULT_ENVIRONMENT
selected_environment = _validate_environment_name(selected_environment)
user_environment_path, project_environment_path = self._environment_paths(
project_root,
selected_environment,
)
user_environment = load_yaml_file(user_environment_path)
project_environment = load_yaml_file(project_environment_path) if project_environment_path is not None else {}
merged: dict[str, Any] = {}
provenance: dict[str, str] = {}
_merge_mapping(merged, provenance, {"environment": DEFAULT_ENVIRONMENT}, "default")
for source, values in (
("user", user_values),
("project", project_values),
(f"user:environment:{selected_environment}", user_environment),
(f"project:environment:{selected_environment}", project_environment),
("explicit", explicit_values),
):
_merge_mapping(merged, provenance, values, source)
framework_values = {key: merged[key] for key in _FRAMEWORK_KEYS if key in merged}
framework = _validate_framework_config(framework_values)
consumer_config = {key: value for key, value in merged.items() if key not in _FRAMEWORK_KEYS}
return ConfigSnapshot(
config=consumer_config,
framework=framework,
provenance=MappingProxyType(dict(provenance)),
)

The generated API reference advertises the parameter as required without explaining an effect. This is an API/UX inconsistency: consumers must invent a value, may assume it namespaces configuration, and cannot tell whether changing it is safe. It should be resolved before #242 freezes the public facade and alongside #267's identity work.

Scope

Choose and document one coherent contract:

  1. remove the parameter with the pre-1.0 migration/deprecation treatment; or
  2. make it control a specific validated identity behavior without duplicating caller-provided path policy.

Audit other public constructors for stored-but-inert compatibility parameters.

Acceptance criteria

  • Every required public constructor parameter has a documented observable purpose.
  • Two loaders with different identities either have intentionally different behavior covered by tests, or no identity parameter exists.
  • The change is represented in API stability/migration notes.
  • Type hints, generated API reference, examples, and profile factory calls agree.
  • [v1.0] Segment and freeze the public API facade #242's facade inventory records the final decision.

Validation

Add public-signature and behavior tests for the chosen contract and run strict consumer typing.

Project fields

  • Status: Backlog
  • Priority: P2
  • Area: Python
  • Initiative: v1.0 Readiness
  • Size: S

Ownership

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething is not working

Type

No type

Projects

  • Status
    Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions