diff --git a/architecture/glossary.md b/architecture/glossary.md index 053945e..22385a3 100644 --- a/architecture/glossary.md +++ b/architecture/glossary.md @@ -37,3 +37,16 @@ module-private so the store interface (`validate`, `flags`, `create_lines`, `teardown_line`, `referenced_variables`) hides the kinds from callers — the same single-source shape as the service-key registry. _Avoid_: store list, kinds table. + +**Token**: +The result of rendering one Compose value into a `podman run`/`pod create` +argument — either a literal `str` (already shell-safe) or an `Expand` (a +value carrying `${VAR}` references that expand at script-run time, not at +generation time). `Token = str | Expand` in `keys.py`. +_Avoid_: arg, flag value. + +**Expand**: +A token whose Compose variable references must expand when the generated +script runs, not when compose2pod generates it. In code, `Expand` in +`keys.py`. +_Avoid_: variable, placeholder. diff --git a/architecture/supported-subset.md b/architecture/supported-subset.md index 1c1f640..c7cd520 100644 --- a/architecture/supported-subset.md +++ b/architecture/supported-subset.md @@ -207,12 +207,12 @@ compose2pod hoists them onto `podman pod create` instead - **Value shapes:** `dns` / `dns_search` / `dns_opt` accept a string or a list of strings; `sysctls` accepts a mapping (`key: value`) or a list of `"key=value"` strings, each value a string or number. A `${VAR}` inside a - value is wrapped in `_Expand` like other interpolated fields, so it stays + value is wrapped in `Expand` like other interpolated fields, so it stays live at run time and counts toward `referenced_variables` — the generated script's own shell expands it when it runs, not compose2pod at generation time. `--add-host` entries render differently by source: an alias/hostname entry stays a plain unquoted token (pre-existing behavior, unchanged by - this move), while an `extra_hosts` entry goes through `_Expand` (quoted, + this move), while an `extra_hosts` entry goes through `Expand` (quoted, `${VAR}`-live) — same as before it was per-service. - **Pod-wide divergence:** unlike every other service key, these apply to every container in the pod once emitted — including services that never @@ -461,18 +461,18 @@ interpolated string leaf into a double-quoted POSIX-shell fragment with the variable references left live, so the generated script's own shell expands them against its runtime environment when the script runs. -The interpolated set is exactly what `_Expand(...)` wraps in +The interpolated set is exactly what `Expand(...)` wraps in `compose2pod/emit.py` and `compose2pod/keys.py` — there is no separate list to maintain by hand, so treat the **service-key registry** (`SERVICE_KEYS` in `compose2pod/keys.py`; see `architecture/glossary.md`) -and the `_Expand(...)` call sites in `emit.py` as the source of truth if this +and the `Expand(...)` call sites in `emit.py` as the source of truth if this enumeration ever appears to drift: - **Structural fields:** `image` (only when the service has no `build` override — otherwise the CI image is used, not the compose value), `command`, `entrypoint`, `environment`, `env_file`, `volumes`, `tmpfs`, and the healthcheck `test` command. -- **Service-key registry fields** whose spec wraps its value in `_Expand` — +- **Service-key registry fields** whose spec wraps its value in `Expand` — e.g. `user`, `working_dir`, `platform`, `group_add`, `cap_add`, `cap_drop`, `security_opt`, `devices`, `labels`, `annotations`, `ulimits`, and every numeric resource-limit key (`mem_limit`, `cpus`, `pids_limit`, ...). The diff --git a/compose2pod/emit.py b/compose2pod/emit.py index 5697715..837d44e 100644 --- a/compose2pod/emit.py +++ b/compose2pod/emit.py @@ -10,7 +10,7 @@ from compose2pod.exceptions import UnsupportedComposeError from compose2pod.graph import depends_on, hostnames, startup_order from compose2pod.healthcheck import health_cmd, interval_seconds -from compose2pod.keys import SERVICE_KEYS, Token, _Expand, _key_value_pairs +from compose2pod.keys import SERVICE_KEYS, Expand, Token, key_value_pairs from compose2pod.pod import pod_create_flags from compose2pod.resources import deploy_resource_flags from compose2pod.shell import to_shell, variable_names @@ -24,7 +24,7 @@ def image_for(svc: dict[str, Any], ci_image: str) -> Token: """Services with a build section run the freshly built CI image.""" if "build" in svc: return ci_image - return _Expand(value=svc["image"]) + return Expand(value=svc["image"]) def command_tokens(svc: dict[str, Any]) -> list[Token]: @@ -33,8 +33,8 @@ def command_tokens(svc: dict[str, Any]) -> list[Token]: if command is None: return [] if isinstance(command, str): - return ["/bin/sh", "-c", _Expand(value=command)] - return [_Expand(value=str(token)) for token in command] + return ["/bin/sh", "-c", Expand(value=command)] + return [Expand(value=str(token)) for token in command] def entrypoint_tokens(svc: dict[str, Any]) -> list[Token]: @@ -43,15 +43,15 @@ def entrypoint_tokens(svc: dict[str, Any]) -> list[Token]: if entrypoint is None: return [] if isinstance(entrypoint, str): - return ["/bin/sh", "-c", _Expand(value=entrypoint)] - return [_Expand(value=str(token)) for token in entrypoint] + return ["/bin/sh", "-c", Expand(value=entrypoint)] + return [Expand(value=str(token)) for token in entrypoint] def _add_health_flags(flags: list[Token], healthcheck: dict[str, Any]) -> None: """Add healthcheck flags to the flags list.""" cmd = health_cmd(healthcheck.get("test")) if cmd is not None: - flags += ["--health-cmd", _Expand(value=cmd)] + flags += ["--health-cmd", Expand(value=cmd)] if "timeout" in healthcheck: flags += ["--health-timeout", str(healthcheck["timeout"])] if "start_period" in healthcheck: @@ -63,13 +63,13 @@ def _add_health_flags(flags: list[Token], healthcheck: dict[str, Any]) -> None: def _add_env_flags(flags: list[Token], svc: dict[str, Any], project_dir: str) -> None: """Add -e and --env-file flags to the flags list.""" # A null environment value means "pass KEY through from the host" (bare `-e KEY`). - for pair in _key_value_pairs(svc.get("environment") or {}): - flags += ["-e", _Expand(value=str(pair))] + for pair in key_value_pairs(svc.get("environment") or {}): + flags += ["-e", Expand(value=str(pair))] env_files = svc.get("env_file") or [] if isinstance(env_files, str): env_files = [env_files] for env_file in env_files: - flags += ["--env-file", _Expand(value=str(Path(project_dir, env_file)))] + flags += ["--env-file", Expand(value=str(Path(project_dir, env_file)))] def _add_volume_flags(flags: list[Token], svc: dict[str, Any], project_dir: str) -> None: @@ -77,7 +77,7 @@ def _add_volume_flags(flags: list[Token], svc: dict[str, Any], project_dir: str) for volume in svc.get("volumes") or []: if ":" not in volume: # Anonymous volume: a bare container path, no host source to translate. - flags += ["-v", _Expand(value=volume)] + flags += ["-v", Expand(value=volume)] continue source, destination = volume.split(":", 1) if source.startswith("."): @@ -85,12 +85,12 @@ def _add_volume_flags(flags: list[Token], svc: dict[str, Any], project_dir: str) source = str(Path(project_dir, source)) # Absolute bind mount (starts with "/") and named volume (bare # identifier) are both kept as-is — neither is a path to translate. - flags += ["-v", _Expand(value=f"{source}:{destination}")] + flags += ["-v", Expand(value=f"{source}:{destination}")] tmpfs = svc.get("tmpfs") or [] if isinstance(tmpfs, str): tmpfs = [tmpfs] for mount in tmpfs: - flags += ["--tmpfs", _Expand(value=mount)] + flags += ["--tmpfs", Expand(value=mount)] def run_flags(name: str, svc: dict[str, Any], pod: str, project_dir: str) -> list[Token]: @@ -168,13 +168,13 @@ class PlannedScript: def _render(tokens: list[Token]) -> str: - return " ".join(to_shell(token.value) if isinstance(token, _Expand) else shlex.quote(token) for token in tokens) + return " ".join(to_shell(token.value) if isinstance(token, Expand) else shlex.quote(token) for token in tokens) def _collect_vars(tokens: list[Token], names: set[str]) -> None: - """Add the run-time variables any `_Expand` tokens expand to `names`.""" + """Add the run-time variables any `Expand` tokens expand to `names`.""" for token in tokens: - if isinstance(token, _Expand): + if isinstance(token, Expand): names.update(variable_names(token.value)) @@ -220,7 +220,7 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript: Every token source is visited a single time and feeds both outputs: each service's `_run_tokens` and the `pod_create_flags` render into lines *and* - have their `_Expand` variables collected, so the script and the variable + have their `Expand` variables collected, so the script and the variable list cannot disagree about what the script expands at run time. """ services = compose["services"] diff --git a/compose2pod/extends.py b/compose2pod/extends.py index 6ce3886..3f4817a 100644 --- a/compose2pod/extends.py +++ b/compose2pod/extends.py @@ -3,7 +3,7 @@ from typing import Any from compose2pod.exceptions import UnsupportedComposeError -from compose2pod.keys import SERVICE_KEYS, _pairs_to_mapping +from compose2pod.keys import SERVICE_KEYS, pairs_to_mapping # Merge policy for keys with a SERVICE_KEYS KeySpec comes from spec.merge (see @@ -38,7 +38,7 @@ def _as_mapping(key: str, name: str, value: Any) -> dict[str, Any]: # noqa: ANN return value if isinstance(value, list): if key == "environment": - return _pairs_to_mapping(name, key, value) + return pairs_to_mapping(name, key, value) if key == "depends_on": return {dep: {} for dep in value} msg = f"service {name!r}: cannot merge {key!r} across incompatible forms" diff --git a/compose2pod/keys.py b/compose2pod/keys.py index 9fd537a..167624e 100644 --- a/compose2pod/keys.py +++ b/compose2pod/keys.py @@ -16,16 +16,16 @@ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True) -class _Expand: +class Expand: """A token whose Compose variable references expand at script-run time.""" value: str -Token = str | _Expand +Token = str | Expand -def _key_value_pairs(value: list[Any] | dict[str, Any]) -> list[Any]: +def key_value_pairs(value: list[Any] | dict[str, Any]) -> list[Any]: """Compose list/map key-value section as 'KEY=value' / 'KEY' entries. A null map value yields a bare 'KEY'. Meaning is caller-defined: '-e KEY' @@ -57,12 +57,12 @@ def _validate_bool(name: str, key: str, value: Any) -> None: # noqa: ANN401 - C raise UnsupportedComposeError(msg) -def _is_number(value: Any) -> bool: # noqa: ANN401 - Compose values are untyped YAML/JSON +def is_number(value: Any) -> bool: # noqa: ANN401 - Compose values are untyped YAML/JSON return not isinstance(value, bool) and isinstance(value, int | float | str) def _validate_number(name: str, key: str, value: Any) -> None: # noqa: ANN401 - Compose values are untyped YAML/JSON - if not _is_number(value): + if not is_number(value): msg = f"service {name!r}: '{key}' must be a number or string" raise UnsupportedComposeError(msg) @@ -73,7 +73,7 @@ def _validate_list(name: str, key: str, value: Any) -> None: # noqa: ANN401 - C raise UnsupportedComposeError(msg) -def _validate_map(name: str, key: str, value: Any) -> None: # noqa: ANN401 - Compose values are untyped YAML/JSON +def validate_map(name: str, key: str, value: Any) -> None: # noqa: ANN401 - Compose values are untyped YAML/JSON if not isinstance(value, list | dict): msg = f"service {name!r}: '{key}' must be a list or mapping" raise UnsupportedComposeError(msg) @@ -94,8 +94,8 @@ def _concat_list(name: str, key: str, base: Any, local: Any) -> list[Any]: # no return _as_list(name, key, base) + _as_list(name, key, local) -def _pairs_to_mapping(name: str, key: str, value: Any) -> dict[str, Any]: # noqa: ANN401 - Compose values are untyped YAML/JSON - """Normalize list-or-dict key-value form to a mapping; inverse of _key_value_pairs.""" +def pairs_to_mapping(name: str, key: str, value: Any) -> dict[str, Any]: # noqa: ANN401 - Compose values are untyped YAML/JSON + """Normalize list-or-dict key-value form to a mapping; inverse of key_value_pairs.""" if isinstance(value, dict): return value if isinstance(value, list): @@ -110,12 +110,12 @@ def _pairs_to_mapping(name: str, key: str, value: Any) -> dict[str, Any]: # noq def _merge_map(name: str, key: str, base: Any, local: Any) -> dict[str, Any]: # noqa: ANN401 - Compose values are untyped YAML/JSON """Merge policy for map-shaped keys: key-by-key merge, local wins.""" - return {**_pairs_to_mapping(name, key, base), **_pairs_to_mapping(name, key, local)} + return {**pairs_to_mapping(name, key, base), **pairs_to_mapping(name, key, local)} def _scalar(flag: str) -> KeySpec: def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untyped YAML/JSON - return [flag, _Expand(value=str(value))] + return [flag, Expand(value=str(value))] return KeySpec(validate=_validate_scalar, emit=emit) @@ -129,7 +129,7 @@ def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untype def _number_scalar(flag: str) -> KeySpec: def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untyped YAML/JSON - return [flag, _Expand(value=str(value))] + return [flag, Expand(value=str(value))] return KeySpec(validate=_validate_number, emit=emit) @@ -138,7 +138,7 @@ def _list(flag: str) -> KeySpec: def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untyped YAML/JSON tokens: list[Token] = [] for item in value: - tokens += [flag, _Expand(value=str(item))] + tokens += [flag, Expand(value=str(item))] return tokens return KeySpec(validate=_validate_list, emit=emit, merge=_concat_list) @@ -147,14 +147,14 @@ def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untype def _map(flag: str) -> KeySpec: def emit(value: Any) -> list[Token]: # noqa: ANN401 - Compose values are untyped YAML/JSON tokens: list[Token] = [] - for pair in _key_value_pairs(value): - tokens += [flag, _Expand(value=str(pair))] + for pair in key_value_pairs(value): + tokens += [flag, Expand(value=str(pair))] return tokens - return KeySpec(validate=_validate_map, emit=emit, merge=_merge_map) + return KeySpec(validate=validate_map, emit=emit, merge=_merge_map) -def _extra_host_pairs(value: list[Any] | dict[str, Any]) -> list[Any]: +def extra_host_pairs(value: list[Any] | dict[str, Any]) -> list[Any]: """Compose extra_hosts as 'host:ip' entries; map values keep their colons (IPv6-safe).""" if isinstance(value, list): return value @@ -204,7 +204,7 @@ def _emit_ulimits(value: Any) -> list[Token]: # noqa: ANN401 - Compose values a return [] tokens: list[Token] = [] for arg in _ulimit_args(value): - tokens += ["--ulimit", _Expand(value=arg)] + tokens += ["--ulimit", Expand(value=arg)] return tokens diff --git a/compose2pod/pod.py b/compose2pod/pod.py index 4a203b6..1c6b336 100644 --- a/compose2pod/pod.py +++ b/compose2pod/pod.py @@ -3,7 +3,7 @@ from typing import Any from compose2pod.exceptions import UnsupportedComposeError -from compose2pod.keys import Token, _Expand, _extra_host_pairs, _validate_map +from compose2pod.keys import Expand, Token, extra_host_pairs, validate_map _DNS_KEYS = {"dns": "--dns", "dns_search": "--dns-search", "dns_opt": "--dns-option"} @@ -48,7 +48,7 @@ def validate_pod_options(name: str, svc: dict[str, Any]) -> None: if "sysctls" in svc: _sysctl_pairs(name, svc["sysctls"]) if "extra_hosts" in svc: - _validate_map(name, "extra_hosts", svc["extra_hosts"]) + validate_map(name, "extra_hosts", svc["extra_hosts"]) def uses_pod_options(services: dict[str, Any]) -> bool: @@ -66,7 +66,7 @@ def _dns_flags(services: dict[str, Any], order: list[str]) -> list[Token]: for value in _as_str_list(name, key, svc[key]): seen[value] = None for value in seen: - tokens += [flag, _Expand(value=value)] + tokens += [flag, Expand(value=value)] return tokens @@ -83,7 +83,7 @@ def _sysctl_flags(services: dict[str, Any], order: list[str]) -> list[Token]: merged[key] = val tokens: list[Token] = [] for key, val in merged.items(): - tokens += ["--sysctl", _Expand(value=f"{key}={val}")] + tokens += ["--sysctl", Expand(value=f"{key}={val}")] return tokens @@ -93,7 +93,7 @@ def _add_host_flags(services: dict[str, Any], order: list[str], hosts: list[str] A host name landing on two different addresses -- across services' extra_hosts, or against an alias's fixed 127.0.0.1 -- is refused rather than guessed at, matching the sysctls conflict rule below. Alias entries render as plain tokens (unquoted, as - before this move); extra_hosts entries render via `_Expand` (as before, quoted/interpolated) + before this move); extra_hosts entries render via `Expand` (as before, quoted/interpolated) -- relocating the flags changes nothing else observable about either source. """ merged: dict[str, str] = {} @@ -104,7 +104,7 @@ def _add_host_flags(services: dict[str, Any], order: list[str], hosts: list[str] svc = services[name] if "extra_hosts" not in svc: continue - for entry in _extra_host_pairs(svc["extra_hosts"]): + for entry in extra_host_pairs(svc["extra_hosts"]): host, _sep, addr = str(entry).partition(":") if merged.get(host, addr) != addr: msg = f"service {name!r}: conflicting host {host!r} ({merged[host]!r} vs {addr!r})" @@ -113,7 +113,7 @@ def _add_host_flags(services: dict[str, Any], order: list[str], hosts: list[str] from_extra_hosts.add(host) tokens: list[Token] = [] for host, addr in merged.items(): - value = _Expand(value=f"{host}:{addr}") if host in from_extra_hosts else f"{host}:{addr}" + value = Expand(value=f"{host}:{addr}") if host in from_extra_hosts else f"{host}:{addr}" tokens += ["--add-host", value] return tokens diff --git a/compose2pod/resources.py b/compose2pod/resources.py index 50e17a1..8864c80 100644 --- a/compose2pod/resources.py +++ b/compose2pod/resources.py @@ -3,7 +3,7 @@ from typing import Any from compose2pod.exceptions import UnsupportedComposeError -from compose2pod.keys import Token, _Expand, _is_number +from compose2pod.keys import Expand, Token, is_number # deploy.resources.limits. -> (podman flag, conflicting legacy key) @@ -11,7 +11,7 @@ def _check_number(name: str, field: str, value: Any) -> None: # noqa: ANN401 - Compose values are untyped - if not _is_number(value): + if not is_number(value): msg = f"service {name!r}: {field} must be a number or string" raise UnsupportedComposeError(msg) @@ -91,7 +91,7 @@ def deploy_resource_flags(svc: dict[str, Any]) -> list[Token]: tokens: list[Token] = [] for field, (flag, _legacy) in _LIMITS.items(): if field in limits: - tokens += [flag, _Expand(value=str(limits[field]))] + tokens += [flag, Expand(value=str(limits[field]))] if "memory" in reservations: - tokens += ["--memory-reservation", _Expand(value=str(reservations["memory"]))] + tokens += ["--memory-reservation", Expand(value=str(reservations["memory"]))] return tokens diff --git a/planning/changes/2026-07-13.07-public-keys-primitives.md b/planning/changes/2026-07-13.07-public-keys-primitives.md new file mode 100644 index 0000000..1e0ace2 --- /dev/null +++ b/planning/changes/2026-07-13.07-public-keys-primitives.md @@ -0,0 +1,135 @@ +--- +summary: Drop the leading underscore on the keys.py primitives (Expand, is_number, validate_map, extra_host_pairs, key_value_pairs, pairs_to_mapping) that five other modules and four test files already import across the module boundary, and add Token/Expand to the glossary. +--- + +# Design: give keys.py's cross-module primitives a real interface + +## Summary + +`compose2pod/keys.py` exports `Token` (already public) but also six +underscore-prefixed names — `_Expand`, `_is_number`, `_validate_map`, +`_extra_host_pairs`, `_key_value_pairs`, `_pairs_to_mapping` — that five other +production modules (`pod.py`, `resources.py`, `emit.py`, `stores.py`, +`extends.py`) and four test files (`test_pod.py`, `test_resources.py`, +`test_emit.py`, `test_keys.py`) already import directly. The leading +underscore names a seam nobody respects. This change drops the underscore on +those six names — no behavior change, no new module, no public *library* API +change (none of these names reach `compose2pod/__init__.py`'s `__all__`) — +and adds `Token`/`Expand` to `architecture/glossary.md` now that they're +openly shared vocabulary. + +## Motivation + +Every module that needs to build a `Token` reaches past `keys.py`'s nominal +public surface (`SERVICE_KEYS`, `STRUCTURAL_KEYS`, `KeySpec`, `Token`) into +its "private" half. `architecture/supported-subset.md` (lines 210, 215, 464, +468, 475) already names `_Expand` directly in prose, treating it as shared +vocabulary rather than an internal detail — the underscore is fiction at this +point. The coupling itself is real and permanent (5 production modules +depend on it, confirmed via `git grep`) — this isn't a pass-through to +delete, just a seam that's mislabeled. Today, renaming something inside +`keys.py` that happens to be one of these six names risks silently breaking +five distant callers, with no interface boundary flagging the risk. + +Everything else in `keys.py` — `_validate_scalar`/`_validate_bool`/ +`_validate_number`/`_validate_list`/`_as_list`/`_concat_list`/`_merge_map`/ +`_scalar`/`_bool`/`_number_scalar`/`_list`/`_map`/`_validate_pull_policy`/ +`_emit_pull_policy`/`_validate_ulimits`/`_ulimit_args`/`_emit_ulimits` — has +no external caller (confirmed via `git grep`) and stays private; only the six +names with a real external caller move. + +## Design + +**Rename in place, no new module.** All six names stay in `keys.py` +(currently 264 lines) — the shared vocabulary lives right next to +`SERVICE_KEYS`/`KeySpec`, which already builds on it internally (e.g. `_map()` +calls `_key_value_pairs`). Splitting into a separate primitives module was +considered and rejected: the primitives aren't used independently of the +registry construction, so a second file would separate genuinely coupled code +without a compensating gain. + +**Renames** (mechanical, no signature or behavior changes): + +| Old | New | +|---|---| +| `_Expand` | `Expand` | +| `_is_number` | `is_number` | +| `_validate_map` | `validate_map` | +| `_extra_host_pairs` | `extra_host_pairs` | +| `_key_value_pairs` | `key_value_pairs` | +| `_pairs_to_mapping` | `pairs_to_mapping` | + +`Token = str | Expand` (the type alias itself is unchanged, already public). + +**Every call site updates**, both the `from compose2pod.keys import ...` +lines and in-body references: + +- `compose2pod/keys.py` — internal uses of the six names (e.g. `_map()`'s + `key_value_pairs(value)`, `_merge_map`'s `pairs_to_mapping(...)`, + `_scalar`'s `Expand(value=...)`), plus the `_pairs_to_mapping` docstring's + "inverse of `_key_value_pairs`" cross-reference. +- `compose2pod/pod.py` — import line, `validate_map(...)`, + `extra_host_pairs(...)`, `Expand(...)` call sites, and the comment at + line 96 mentioning `_Expand`. +- `compose2pod/resources.py` — import line, `is_number(...)`, `Expand(...)`. +- `compose2pod/emit.py` — import line, `key_value_pairs(...)`, all + `Expand(...)` call sites, the `isinstance(token, Expand)` checks, and the + docstring/comment at lines 175/223 mentioning `_Expand`. +- `compose2pod/stores.py` — no change needed (only imports the already-public + `Token`). +- `compose2pod/extends.py` — import line, `pairs_to_mapping(...)`. +- `tests/test_pod.py`, `tests/test_resources.py`, `tests/test_emit.py` — + import lines and every `_Expand(value=...)` construction in test bodies. +- `tests/test_keys.py` — import line (`validate_map`), and the + `TestMergeCallables` docstring's mention of `_pairs_to_mapping`. +- `architecture/supported-subset.md` — the five `_Expand` prose references + (lines 210, 215, 464, 468, 475) drop the underscore to match the renamed + identifier. + +**`architecture/glossary.md`** gains two entries, since `Token`/`Expand` are +now openly shared vocabulary across five modules rather than an +implementation detail of `keys.py`: + +```markdown +**Token**: +The result of rendering one Compose value into a `podman run`/`pod create` +argument — either a literal `str` (already shell-safe) or an `Expand` (a +value carrying `${VAR}` references that expand at script-run time, not at +generation time). `Token = str | Expand` in `keys.py`. +_Avoid_: arg, flag value. + +**Expand**: +A token whose Compose variable references must expand when the generated +script runs, not when compose2pod generates it. In code, `Expand` in +`keys.py`. +_Avoid_: variable, placeholder. +``` + +## Non-goals + +- Splitting these primitives into a new module — considered, rejected (see + Design); nothing about their usage pattern calls for a second file. +- Renaming or restructuring anything not reached by an external caller today + (the sixteen still-private helpers listed in Motivation) — no evidence they + need a public interface, and touching them would be unrelated churn. +- Adding `__all__` to `keys.py` — no other internal module in this codebase + uses `__all__` (only the top-level package `__init__.py` does, for its + actual public library surface); the leading-underscore convention already + used everywhere else (e.g. `stores.py`'s `_STORE_KINDS`) is sufficient once + these six names are corrected. + +## Testing + +Behavior-preserving rename: every existing test keeps its assertions +unchanged, only import lines and literal `_Expand(...)` construction sites +change to `Expand(...)`. `just test-ci` at 100% (no new code paths — a rename +can't add or remove coverage), `just lint-ci` clean, `just check-planning`. + +## Risk + +- **Missed call site** (low x med): a leftover `_Expand`/`_is_number`/etc. + reference would fail immediately at import time or via `ty check` (unknown + name), not silently — `git grep` before and after the rename confirms zero + remaining references to the old names outside this design doc itself. +- **`architecture/supported-subset.md` prose drifts from code** (low x low): + five prose mentions are enumerated above by line number so none are missed. diff --git a/tests/test_emit.py b/tests/test_emit.py index 21435f7..9677dea 100644 --- a/tests/test_emit.py +++ b/tests/test_emit.py @@ -8,7 +8,7 @@ from compose2pod.emit import ( _SCRIPT_HEADER, EmitOptions, - _Expand, + Expand, command_tokens, emit_script, entrypoint_tokens, @@ -27,8 +27,8 @@ class TestRunFlags: def test_db_flags(self, chats_compose: dict) -> None: flags = run_flags("db", chats_compose["services"]["db"], "test-pod", "/builds/chats") assert flags[:4] == ["--pod", "test-pod", "--name", "test-pod-db"] - assert flags[4:6] == ["-e", _Expand(value="POSTGRES_PASSWORD=password")] - assert flags[6:8] == ["--health-cmd", _Expand(value="pg_isready -U database -d database")] + assert flags[4:6] == ["-e", Expand(value="POSTGRES_PASSWORD=password")] + assert flags[6:8] == ["--health-cmd", Expand(value="pg_isready -U database -d database")] assert flags[8:10] == ["--health-timeout", "5s"] assert flags[10:12] == ["--health-retries", "15"] # fix #2 @@ -41,53 +41,53 @@ def test_start_period_is_passed_through(self) -> None: def test_env_map_form(self) -> None: svc = {"image": "x", "environment": {"A": "1", "B": "two words"}} flags = run_flags("app", svc, "p", "/builds/x") - assert flags[4:8] == ["-e", _Expand(value="A=1"), "-e", _Expand(value="B=two words")] + assert flags[4:8] == ["-e", Expand(value="A=1"), "-e", Expand(value="B=two words")] def test_env_map_null_value_is_host_passthrough(self) -> None: # A null mapping value means "pass KEY through from the host", like `- KEY`. svc = {"image": "x", "environment": {"PASSTHRU": None, "SET": "v"}} flags = run_flags("app", svc, "p", "/builds/x") - assert flags[4:8] == ["-e", _Expand(value="PASSTHRU"), "-e", _Expand(value="SET=v")] + assert flags[4:8] == ["-e", Expand(value="PASSTHRU"), "-e", Expand(value="SET=v")] def test_env_file_and_volume_resolved_against_project_dir(self) -> None: svc = {"image": "x", "env_file": "tests.env", "volumes": [".:/srv/www/"]} flags = run_flags("app", svc, "p", "/builds/chats") - assert flags[4:6] == ["--env-file", _Expand(value="/builds/chats/tests.env")] - assert flags[6:8] == ["-v", _Expand(value="/builds/chats:/srv/www/")] + assert flags[4:6] == ["--env-file", Expand(value="/builds/chats/tests.env")] + assert flags[6:8] == ["-v", Expand(value="/builds/chats:/srv/www/")] def test_env_file_list_form(self) -> None: svc = {"image": "x", "env_file": ["a.env", "b.env"]} flags = run_flags("app", svc, "p", "/builds/x") assert flags[4:8] == [ "--env-file", - _Expand(value="/builds/x/a.env"), + Expand(value="/builds/x/a.env"), "--env-file", - _Expand(value="/builds/x/b.env"), + Expand(value="/builds/x/b.env"), ] def test_tmpfs_string_form(self) -> None: # S108 flags "/tmp" as an insecure hardcoded temp path; this is a # pass-through string being tested, not a file write. flags = run_flags("app", {"image": "x", "tmpfs": "/tmp:mode=1777"}, "p", "/builds/x") # noqa: S108 - assert flags[4:6] == ["--tmpfs", _Expand(value="/tmp:mode=1777")] # noqa: S108 + assert flags[4:6] == ["--tmpfs", Expand(value="/tmp:mode=1777")] # noqa: S108 def test_tmpfs_list_form(self) -> None: svc = {"image": "x", "tmpfs": ["/tmp:mode=1777", "/run"]} # noqa: S108 flags = run_flags("app", svc, "p", "/builds/x") - assert flags[4:8] == ["--tmpfs", _Expand(value="/tmp:mode=1777"), "--tmpfs", _Expand(value="/run")] # noqa: S108 + assert flags[4:8] == ["--tmpfs", Expand(value="/tmp:mode=1777"), "--tmpfs", Expand(value="/run")] # noqa: S108 def test_absolute_volume_source_is_kept_as_is(self) -> None: flags = run_flags("app", {"image": "x", "volumes": ["/data/app:/srv/www/"]}, "p", "/builds/x") - assert flags[4:6] == ["-v", _Expand(value="/data/app:/srv/www/")] + assert flags[4:6] == ["-v", Expand(value="/data/app:/srv/www/")] def test_anonymous_volume_emitted_as_single_path(self) -> None: flags = run_flags("app", {"image": "x", "volumes": ["/var/cache/models"]}, "p", "/builds/x") - assert flags[4:6] == ["-v", _Expand(value="/var/cache/models")] + assert flags[4:6] == ["-v", Expand(value="/var/cache/models")] def test_named_volume_emitted_without_project_dir_translation(self) -> None: svc = {"image": "x", "volumes": ["pgdata:/var/lib/postgresql/data"]} flags = run_flags("db", svc, "p", "/builds/x") - assert flags[4:6] == ["-v", _Expand(value="pgdata:/var/lib/postgresql/data")] + assert flags[4:6] == ["-v", Expand(value="pgdata:/var/lib/postgresql/data")] def test_secret_flag_emitted(self) -> None: flags = run_flags("app", {"image": "x", "secrets": ["db"]}, "test-pod", "/b") @@ -95,38 +95,38 @@ def test_secret_flag_emitted(self) -> None: def test_healthcheck_without_timeout_omits_health_timeout_flag(self) -> None: flags = run_flags("app", {"image": "x", "healthcheck": {"test": "true"}}, "p", "/builds/x") - assert flags[4:6] == ["--health-cmd", _Expand(value="true")] + assert flags[4:6] == ["--health-cmd", Expand(value="true")] assert "--health-timeout" not in flags def test_user_flag(self) -> None: flags = run_flags("app", {"image": "x", "user": "1000:1000"}, "p", "/b") - assert flags[4:6] == ["--user", _Expand(value="1000:1000")] + assert flags[4:6] == ["--user", Expand(value="1000:1000")] def test_working_dir_flag(self) -> None: flags = run_flags("app", {"image": "x", "working_dir": "/srv/app"}, "p", "/b") - assert flags[4:6] == ["--workdir", _Expand(value="/srv/app")] + assert flags[4:6] == ["--workdir", Expand(value="/srv/app")] def test_user_and_working_dir_order(self) -> None: svc = {"image": "x", "user": "root", "working_dir": "/app"} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--user", _Expand(value="root"), "--workdir", _Expand(value="/app")] + assert flags[4:8] == ["--user", Expand(value="root"), "--workdir", Expand(value="/app")] def test_mem_limit_flag(self) -> None: flags = run_flags("app", {"image": "x", "mem_limit": "512m"}, "p", "/b") - assert flags[4:6] == ["--memory", _Expand(value="512m")] + assert flags[4:6] == ["--memory", Expand(value="512m")] def test_cpus_numeric_value_flag(self) -> None: flags = run_flags("app", {"image": "x", "cpus": 0.5}, "p", "/b") - assert flags[4:6] == ["--cpus", _Expand(value="0.5")] + assert flags[4:6] == ["--cpus", Expand(value="0.5")] def test_pids_limit_int_flag(self) -> None: flags = run_flags("app", {"image": "x", "pids_limit": 100}, "p", "/b") - assert flags[4:6] == ["--pids-limit", _Expand(value="100")] + assert flags[4:6] == ["--pids-limit", Expand(value="100")] def test_cpuset_and_shm_size_flags(self) -> None: svc = {"image": "x", "cpuset": "0-3", "shm_size": "64m"} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--cpuset-cpus", _Expand(value="0-3"), "--shm-size", _Expand(value="64m")] + assert flags[4:8] == ["--cpuset-cpus", Expand(value="0-3"), "--shm-size", Expand(value="64m")] def test_oom_kill_disable_bool_flag(self) -> None: assert run_flags("app", {"image": "x", "oom_kill_disable": True}, "p", "/b")[4:5] == ["--oom-kill-disable"] @@ -134,52 +134,52 @@ def test_oom_kill_disable_bool_flag(self) -> None: def test_group_add_flag(self) -> None: flags = run_flags("app", {"image": "x", "group_add": ["docker", 1000]}, "p", "/b") - assert flags[4:8] == ["--group-add", _Expand(value="docker"), "--group-add", _Expand(value="1000")] + assert flags[4:8] == ["--group-add", Expand(value="docker"), "--group-add", Expand(value="1000")] def test_cap_add_flag(self) -> None: flags = run_flags("app", {"image": "x", "cap_add": ["NET_ADMIN", "SYS_TIME"]}, "p", "/b") - assert flags[4:8] == ["--cap-add", _Expand(value="NET_ADMIN"), "--cap-add", _Expand(value="SYS_TIME")] + assert flags[4:8] == ["--cap-add", Expand(value="NET_ADMIN"), "--cap-add", Expand(value="SYS_TIME")] def test_cap_drop_and_security_opt_flags(self) -> None: svc = {"image": "x", "cap_drop": ["ALL"], "security_opt": ["label=disable"]} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--cap-drop", _Expand(value="ALL"), "--security-opt", _Expand(value="label=disable")] + assert flags[4:8] == ["--cap-drop", Expand(value="ALL"), "--security-opt", Expand(value="label=disable")] def test_labels_map_form(self) -> None: svc = {"image": "x", "labels": {"team": "api", "tier": "backend"}} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--label", _Expand(value="team=api"), "--label", _Expand(value="tier=backend")] + assert flags[4:8] == ["--label", Expand(value="team=api"), "--label", Expand(value="tier=backend")] def test_labels_list_form(self) -> None: svc = {"image": "x", "labels": ["team=api", "standalone"]} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--label", _Expand(value="team=api"), "--label", _Expand(value="standalone")] + assert flags[4:8] == ["--label", Expand(value="team=api"), "--label", Expand(value="standalone")] def test_labels_null_value_is_empty_label(self) -> None: # A null map value is an empty label here, NOT the host-passthrough that # `environment`'s null means -- same emitted shape, distinct meaning. flags = run_flags("app", {"image": "x", "labels": {"empty": None}}, "p", "/b") - assert flags[4:6] == ["--label", _Expand(value="empty")] + assert flags[4:6] == ["--label", Expand(value="empty")] def test_platform_flag(self) -> None: flags = run_flags("app", {"image": "x", "platform": "linux/amd64"}, "p", "/b") - assert flags[4:6] == ["--platform", _Expand(value="linux/amd64")] + assert flags[4:6] == ["--platform", Expand(value="linux/amd64")] def test_devices_flag(self) -> None: flags = run_flags("app", {"image": "x", "devices": ["/dev/fuse", "/dev/net/tun"]}, "p", "/b") - assert flags[4:8] == ["--device", _Expand(value="/dev/fuse"), "--device", _Expand(value="/dev/net/tun")] + assert flags[4:8] == ["--device", Expand(value="/dev/fuse"), "--device", Expand(value="/dev/net/tun")] def test_annotations_map_form(self) -> None: flags = run_flags("app", {"image": "x", "annotations": {"com.example/team": "api"}}, "p", "/b") - assert flags[4:6] == ["--annotation", _Expand(value="com.example/team=api")] + assert flags[4:6] == ["--annotation", Expand(value="com.example/team=api")] def test_annotations_null_value_is_bare_key(self) -> None: flags = run_flags("app", {"image": "x", "annotations": {"marker": None}}, "p", "/b") - assert flags[4:6] == ["--annotation", _Expand(value="marker")] + assert flags[4:6] == ["--annotation", Expand(value="marker")] def test_labels_still_emit_after_map_flags_refactor(self) -> None: flags = run_flags("app", {"image": "x", "labels": {"team": "api"}}, "p", "/b") - assert flags[4:6] == ["--label", _Expand(value="team=api")] + assert flags[4:6] == ["--label", Expand(value="team=api")] def test_read_only_flag(self) -> None: flags = run_flags("app", {"image": "x", "read_only": True}, "p", "/b") @@ -207,16 +207,16 @@ def test_pull_policy_passthrough_values(self) -> None: def test_ulimits_mapping_form(self) -> None: svc = {"image": "x", "ulimits": {"nofile": {"soft": 20000, "hard": 40000}}} flags = run_flags("app", svc, "p", "/b") - assert flags[4:6] == ["--ulimit", _Expand(value="nofile=20000:40000")] + assert flags[4:6] == ["--ulimit", Expand(value="nofile=20000:40000")] def test_ulimits_scalar_form(self) -> None: flags = run_flags("app", {"image": "x", "ulimits": {"nproc": 65535}}, "p", "/b") - assert flags[4:6] == ["--ulimit", _Expand(value="nproc=65535")] + assert flags[4:6] == ["--ulimit", Expand(value="nproc=65535")] def test_ulimits_mixed_forms(self) -> None: svc = {"image": "x", "ulimits": {"nproc": 65535, "nofile": {"soft": 1024, "hard": 2048}}} flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--ulimit", _Expand(value="nproc=65535"), "--ulimit", _Expand(value="nofile=1024:2048")] + assert flags[4:8] == ["--ulimit", Expand(value="nproc=65535"), "--ulimit", Expand(value="nofile=1024:2048")] def test_null_pull_policy_and_ulimits_emit_nothing(self) -> None: flags = run_flags("app", {"image": "x", "pull_policy": None, "ulimits": None}, "p", "/b") @@ -237,16 +237,16 @@ def test_registry_emission_order_across_shape_groups(self) -> None: flags = run_flags("app", svc, "p", "/b") assert flags[4:] == [ "--user", - _Expand(value="root"), + Expand(value="root"), "--init", "--cap-add", - _Expand(value="NET_ADMIN"), + Expand(value="NET_ADMIN"), "--label", - _Expand(value="team=api"), + Expand(value="team=api"), "--pull", "always", "--ulimit", - _Expand(value="nofile=1024"), + Expand(value="nofile=1024"), ] def test_deploy_resource_flags_emitted(self) -> None: @@ -255,7 +255,7 @@ def test_deploy_resource_flags_emitted(self) -> None: "deploy": {"resources": {"limits": {"memory": "256m"}, "reservations": {"memory": "128m"}}}, } flags = run_flags("app", svc, "p", "/b") - assert flags[4:8] == ["--memory", _Expand(value="256m"), "--memory-reservation", _Expand(value="128m")] + assert flags[4:8] == ["--memory", Expand(value="256m"), "--memory-reservation", Expand(value="128m")] class TestImageAndCommand: @@ -263,17 +263,17 @@ def test_build_service_uses_ci_image(self, chats_compose: dict) -> None: assert image_for(chats_compose["services"]["application"], "reg/ci:abc") == "reg/ci:abc" def test_plain_service_keeps_image(self, chats_compose: dict) -> None: - assert image_for(chats_compose["services"]["db"], "reg/ci:abc") == _Expand(value="postgres:13.5-alpine") + assert image_for(chats_compose["services"]["db"], "reg/ci:abc") == Expand(value="postgres:13.5-alpine") def test_command_list_passes_through(self, chats_compose: dict) -> None: assert command_tokens(chats_compose["services"]["migrations"]) == [ - _Expand(value="alembic"), - _Expand(value="upgrade"), - _Expand(value="head"), + Expand(value="alembic"), + Expand(value="upgrade"), + Expand(value="head"), ] def test_command_string_becomes_shell(self) -> None: - assert command_tokens({"command": "echo hi"}) == ["/bin/sh", "-c", _Expand(value="echo hi")] + assert command_tokens({"command": "echo hi"}) == ["/bin/sh", "-c", Expand(value="echo hi")] def test_missing_command_is_empty(self) -> None: assert command_tokens({"image": "x"}) == [] @@ -281,10 +281,10 @@ def test_missing_command_is_empty(self) -> None: class TestEntrypoint: def test_list_form_passes_through(self) -> None: - assert entrypoint_tokens({"entrypoint": ["sleep", "600"]}) == [_Expand(value="sleep"), _Expand(value="600")] + assert entrypoint_tokens({"entrypoint": ["sleep", "600"]}) == [Expand(value="sleep"), Expand(value="600")] def test_string_form_becomes_shell(self) -> None: - assert entrypoint_tokens({"entrypoint": "serve now"}) == ["/bin/sh", "-c", _Expand(value="serve now")] + assert entrypoint_tokens({"entrypoint": "serve now"}) == ["/bin/sh", "-c", Expand(value="serve now")] def test_missing_entrypoint_is_empty(self) -> None: assert entrypoint_tokens({"image": "x"}) == [] diff --git a/tests/test_keys.py b/tests/test_keys.py index f0f69b0..c399b5c 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -7,8 +7,8 @@ _concat_list, _merge_map, _validate_list, - _validate_map, _validate_ulimits, + validate_map, ) from compose2pod.parsing import SUPPORTED_SERVICE_KEYS @@ -80,7 +80,7 @@ def test_merge_present_iff_list_or_map_shaped(key: str) -> None: end up with no merge callable. """ spec = SERVICE_KEYS[key] - is_list_or_map_shaped = spec.validate in (_validate_list, _validate_map, _validate_ulimits) + is_list_or_map_shaped = spec.validate in (_validate_list, validate_map, _validate_ulimits) assert (spec.merge is not None) == is_list_or_map_shaped @@ -89,7 +89,7 @@ class TestMergeCallables: extends.py (Task 2) will call these through SERVICE_KEYS[key].merge, but that wiring doesn't exist yet — these tests exercise every branch of - _concat_list/_as_list/_merge_map/_pairs_to_mapping on their own so Task 1 + _concat_list/_as_list/_merge_map/pairs_to_mapping on their own so Task 1 is fully covered without depending on Task 2. """ diff --git a/tests/test_pod.py b/tests/test_pod.py index 9908e37..dea7c21 100644 --- a/tests/test_pod.py +++ b/tests/test_pod.py @@ -1,7 +1,7 @@ import pytest from compose2pod.exceptions import UnsupportedComposeError -from compose2pod.keys import _Expand +from compose2pod.keys import Expand from compose2pod.pod import pod_create_flags, uses_pod_options, validate_pod_options @@ -59,32 +59,32 @@ def test_dns_union_across_closure_dedup_first_seen(self) -> None: services = {"a": {"dns": ["1.1.1.1", "8.8.8.8"]}, "b": {"dns": "8.8.8.8"}} assert pod_create_flags(services, ["a", "b"], []) == [ "--dns", - _Expand(value="1.1.1.1"), + Expand(value="1.1.1.1"), "--dns", - _Expand(value="8.8.8.8"), + Expand(value="8.8.8.8"), ] def test_dns_search_and_opt_flags(self) -> None: services = {"a": {"dns_search": "corp.internal", "dns_opt": ["ndots:2"]}} assert pod_create_flags(services, ["a"], []) == [ "--dns-search", - _Expand(value="corp.internal"), + Expand(value="corp.internal"), "--dns-option", - _Expand(value="ndots:2"), + Expand(value="ndots:2"), ] def test_sysctls_mapping_and_list_merge(self) -> None: services = {"a": {"sysctls": {"net.core.somaxconn": 1024}}, "b": {"sysctls": ["net.ipv4.tcp_syncookies=1"]}} assert pod_create_flags(services, ["a", "b"], []) == [ "--sysctl", - _Expand(value="net.core.somaxconn=1024"), + Expand(value="net.core.somaxconn=1024"), "--sysctl", - _Expand(value="net.ipv4.tcp_syncookies=1"), + Expand(value="net.ipv4.tcp_syncookies=1"), ] def test_sysctls_same_key_same_value_merges(self) -> None: services = {"a": {"sysctls": {"net.x": "1"}}, "b": {"sysctls": {"net.x": 1}}} - assert pod_create_flags(services, ["a", "b"], []) == ["--sysctl", _Expand(value="net.x=1")] + assert pod_create_flags(services, ["a", "b"], []) == ["--sysctl", Expand(value="net.x=1")] def test_sysctls_same_key_conflict_refused(self) -> None: services = {"a": {"sysctls": {"net.x": "1"}}, "b": {"sysctls": {"net.x": "2"}}} @@ -109,17 +109,17 @@ def test_alias_only_hosts_produce_add_host(self) -> None: def test_extra_hosts_list_form(self) -> None: services = {"a": {"extra_hosts": ["db:10.0.0.5"]}} - assert pod_create_flags(services, ["a"], []) == ["--add-host", _Expand(value="db:10.0.0.5")] + assert pod_create_flags(services, ["a"], []) == ["--add-host", Expand(value="db:10.0.0.5")] def test_extra_hosts_mapping_form(self) -> None: services = {"a": {"extra_hosts": {"db": "10.0.0.5"}}} - assert pod_create_flags(services, ["a"], []) == ["--add-host", _Expand(value="db:10.0.0.5")] + assert pod_create_flags(services, ["a"], []) == ["--add-host", Expand(value="db:10.0.0.5")] def test_extra_hosts_ipv6_value_keeps_colons(self) -> None: services = {"a": {"extra_hosts": {"myhost": "2001:db8::1"}}} assert pod_create_flags(services, ["a"], []) == [ "--add-host", - _Expand(value="myhost:2001:db8::1"), + Expand(value="myhost:2001:db8::1"), ] def test_alias_and_extra_hosts_on_different_hosts_both_appear(self) -> None: @@ -128,12 +128,12 @@ def test_alias_and_extra_hosts_on_different_hosts_both_appear(self) -> None: "--add-host", "web:127.0.0.1", "--add-host", - _Expand(value="db:10.0.0.5"), + Expand(value="db:10.0.0.5"), ] def test_same_host_same_address_across_sources_dedups_silently(self) -> None: services = {"a": {"extra_hosts": {"web": "127.0.0.1"}}} - assert pod_create_flags(services, ["a"], ["web"]) == ["--add-host", _Expand(value="web:127.0.0.1")] + assert pod_create_flags(services, ["a"], ["web"]) == ["--add-host", Expand(value="web:127.0.0.1")] def test_conflicting_extra_hosts_across_services_refused(self) -> None: services = {"a": {"extra_hosts": {"db": "10.0.0.5"}}, "b": {"extra_hosts": {"db": "10.0.0.6"}}} diff --git a/tests/test_resources.py b/tests/test_resources.py index de3c0da..8e6ca46 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -3,7 +3,7 @@ import pytest from compose2pod.exceptions import UnsupportedComposeError -from compose2pod.keys import _Expand +from compose2pod.keys import Expand from compose2pod.resources import deploy_resource_flags, validate_deploy @@ -104,13 +104,13 @@ def test_limits_emitted(self) -> None: deploy = {"resources": {"limits": {"cpus": "0.5", "memory": "256m", "pids": 100}}} assert deploy_resource_flags(_svc(deploy)) == [ "--cpus", - _Expand(value="0.5"), + Expand(value="0.5"), "--memory", - _Expand(value="256m"), + Expand(value="256m"), "--pids-limit", - _Expand(value="100"), + Expand(value="100"), ] def test_reservation_memory_emitted(self) -> None: deploy = {"resources": {"reservations": {"memory": "128m"}}} - assert deploy_resource_flags(_svc(deploy)) == ["--memory-reservation", _Expand(value="128m")] + assert deploy_resource_flags(_svc(deploy)) == ["--memory-reservation", Expand(value="128m")]