Skip to content

Commit e0a547a

Browse files
committed
Refactor: unify suews-convert under a single entry point (gh#1304)
The yaml-upgrade subcommand introduced in #1306 was a false split: all three converter paths (legacy tables, df_state, older YAML) produce the same artefact - a current-schema YAML - so the CLI only needs one entry. Per Ting: "output 永远是 current-schema YAML, 没必要分子命令." * `src/supy/cmd/table_converter.py`: drop the `yaml-upgrade` subcommand; the root `suews-convert -i <in> -o <out> [-f <from>]` now auto-detects by file extension and dispatches to `upgrade_yaml` for `*.yml`/`*.yaml` inputs in addition to the existing table / df_state paths. * `-f/--from` loses the hard `click.Choice(list_ver_from)` so it can carry a table release, a supy release tag, or a schema version; per-input validation now lives in the command body (clear error for unknown table releases on nml input). * `src/supy/util/converter/__init__.py::detect_input_type` learns the `yaml` category so the dispatcher stays ignorant of filename conventions. * `src/supy/util/converter/yaml_upgrade.py`: drop the unused `assume_yes` parameter (YAGNI) and retune the log / error messages to reference `-f/--from` instead of the old `-f/--from-ver` flag name. * `src/supy/data_model/core/config.py`: update the schema-drift hint to point at `suews-convert -i <old.yml> -o <new.yml>` (bare command when a signature is present, `+ -f <release-tag>` when not) - matches the new CLI shape and avoids directing users at a command that no longer exists. Tests updated to exercise the unified path: `test_yaml_upgrade.py:: TestSuewsConvertYamlPath` invokes the root command with a YAML input (with and without `-f`); `test_from_yaml_errors.py` asserts the updated hint text. 25 tests in `test/data_model/test_yaml_upgrade.py` + `test_release_compat.py` + `test_from_yaml_errors.py` pass; full `test/data_model/` suite stays at 423 passed / 4 skipped. CLI-surface tests in `test/core/test_cli_conversion.py` + `test_cli_run.py` still green (21 passed). #1306 hadn't been in any release, so no external users are touching the removed subcommand - the consolidation is pure simplification. Refs #1301, #1304.
1 parent 166c584 commit e0a547a

File tree

7 files changed

+150
-155
lines changed

7 files changed

+150
-155
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ EXAMPLES:
5656

5757
### 19 Apr 2026
5858

59+
- [change][experimental] Consolidate `suews-convert` under a single unified entry point (#1304)
60+
- Dropped the `yaml-upgrade` subcommand introduced in #1306; the top-level `suews-convert -i <in> -o <out> [-f <from>]` now auto-detects the input type from the file extension and dispatches to the table/`df_state`/YAML-upgrade path accordingly. Output is always a current-schema YAML, regardless of the source format
61+
- `-f/--from` loses the `click.Choice(list_ver_from)` constraint so the same flag can carry a table release (`2024a`), a supy release tag (`2026.1.28`) or a schema version (`2025.12`); per-input validation moved into the command body
62+
- Loader drift hint in `SUEWSConfig.from_yaml` updated to `suews-convert -i <old.yml> -o <new.yml> [-f <release-tag>]`
5963
- [feature][experimental] Register `2026.1.28 -> 2025.12` YAML upgrade handler (#1304)
6064
- New handler `_migrate_pre_setpoint_split_to_2025_12` in `src/supy/util/converter/yaml_upgrade.py` migrates profile-shaped `HeatingSetpointTemperature` / `CoolingSetpointTemperature` fields (pre-#1261) into the post-split `*Profile` + scalar pair, and sets `model.physics.setpointmethod = 2` (SCHEDULED) to preserve the user's profile intent
6165
- Added vendored release fixture `test/fixtures/release_configs/2026.1.28.yml` (captured from tag `2026.1.28`) and a new `TestReleaseCompat.test_pre_drift_release_upgrades_and_parses` guard that asserts every pre-drift fixture upgrades cleanly and then parses under the current validator

src/supy/cmd/table_converter.py

Lines changed: 67 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,39 @@
1515
CURRENT_VERSION = None
1616

1717

18-
@click.group(
19-
invoke_without_command=True,
18+
@click.command(
2019
context_settings=dict(show_default=True),
2120
help=(
22-
"Convert between SUEWS input formats.\n\n"
23-
"Subcommands:\n"
24-
" yaml-upgrade Upgrade an existing YAML to the current schema.\n\n"
25-
"Default (no subcommand) converts legacy tables or df_state to YAML, "
26-
"preserving backward-compatibility with earlier releases. Run "
27-
"`suews-convert COMMAND --help` for subcommand-specific help."
21+
"Convert any supported SUEWS input into a current-schema YAML.\n\n"
22+
"Input type is auto-detected from the file:\n"
23+
" RunControl.nml / *.nml legacy SUEWS table set\n"
24+
" *.csv / *.pkl df_state snapshot\n"
25+
" *.yml / *.yaml older-release YAML (cross-release upgrade)\n\n"
26+
"Pass -f/--from to disambiguate the source version when auto-detection "
27+
"is ambiguous (table releases for .nml inputs; release tag or schema "
28+
"version for .yml inputs)."
2829
),
2930
)
3031
@click.option(
3132
"-f",
3233
"--from",
3334
"fromVer",
34-
help="Version to convert from (auto-detect if not specified)",
35-
type=click.Choice(list_ver_from),
35+
help=(
36+
"Source version. For .nml inputs pick a table release (e.g. 2024a); "
37+
"for .yml inputs pass a supy release tag (e.g. 2026.1.28) or a "
38+
"schema version. Auto-detected when omitted."
39+
),
3640
required=False,
3741
default=None,
3842
)
3943
@click.option(
4044
"-i",
4145
"--input",
4246
"input_file",
43-
help="Input file: RunControl.nml for tables, or df_state.csv/.pkl",
47+
help=(
48+
"Input file: RunControl.nml for tables, *.csv/*.pkl for df_state, "
49+
"or *.yml/*.yaml for an older YAML config."
50+
),
4451
type=click.Path(exists=True, dir_okay=False, file_okay=True),
4552
required=False,
4653
)
@@ -56,7 +63,10 @@
5663
"-d",
5764
"--debug-dir",
5865
"debug_dir",
59-
help="Optional directory to keep intermediate conversion files for debugging. If not provided, temporary directories are removed automatically.",
66+
help=(
67+
"Optional directory to keep intermediate conversion files for "
68+
"debugging table/df_state runs. Ignored for YAML upgrades."
69+
),
6070
type=click.Path(),
6171
required=False,
6272
default=None,
@@ -66,7 +76,10 @@
6676
"no_validate_profiles",
6777
is_flag=True,
6878
default=False,
69-
help="Disable automatic profile validation and creation of missing profiles",
79+
help=(
80+
"Disable automatic profile validation and creation of missing profiles "
81+
"(table/df_state paths only)."
82+
),
7083
)
7184
@click.pass_context
7285
def convert_table_cmd(
@@ -77,28 +90,25 @@ def convert_table_cmd(
7790
debug_dir: str = None,
7891
no_validate_profiles: bool = False,
7992
):
80-
"""Convert SUEWS inputs to YAML configuration, or upgrade an existing YAML.
93+
"""Convert any supported SUEWS input to a current-schema YAML.
8194
82-
Default (no subcommand): converts legacy tables or df_state format to YAML.
83-
Input must be a specific file:
84-
- RunControl.nml: Converts table-based SUEWS input
85-
- *.csv or *.pkl: Converts df_state format
95+
The command auto-detects the input format from the file extension and
96+
dispatches to the matching converter. All three paths produce a YAML that
97+
parses under the current ``SUEWSConfig`` validator.
8698
8799
Examples:
88-
# Convert tables to YAML (legacy path)
100+
# Legacy tables -> YAML
89101
suews-convert -i path/to/RunControl.nml -o config.yml
90102
91-
# Convert old df_state CSV to YAML
103+
# df_state snapshot -> YAML
92104
suews-convert -i df_state.csv -o config.yml
93105
94-
# Upgrade an existing YAML to the current schema
95-
suews-convert yaml-upgrade -i old.yml -o new.yml
96-
"""
97-
if ctx.invoked_subcommand is not None:
98-
# A subcommand (e.g. `yaml-upgrade`) handles its own logic; drop
99-
# through so Click can dispatch to it.
100-
return
106+
# Older-release YAML -> current-schema YAML (auto-detect source)
107+
suews-convert -i old.yml -o new.yml
101108
109+
# Older YAML without a schema_version field -> explicit source tag
110+
suews-convert -i old.yml -o new.yml -f 2026.1.28
111+
"""
102112
if input_file is None or output_file is None:
103113
click.echo(ctx.get_help())
104114
sys.exit(0 if (input_file is None and output_file is None) else 2)
@@ -126,7 +136,26 @@ def convert_table_cmd(
126136
f"Warning: Output file should have .yml or .yaml extension", err=True
127137
)
128138

129-
# Handle based on input type
139+
# Dispatch on input type
140+
if input_type == "yaml":
141+
from ..util.converter.yaml_upgrade import YamlUpgradeError, upgrade_yaml
142+
143+
try:
144+
upgrade_yaml(
145+
input_path=input_path,
146+
output_path=output_path,
147+
from_ver=fromVer,
148+
)
149+
except YamlUpgradeError as e:
150+
click.secho(f"[ERROR] {e}", fg="red", err=True)
151+
sys.exit(1)
152+
except Exception as e: # noqa: BLE001 - surface unexpected failures verbatim
153+
click.secho(f"[ERROR] YAML upgrade failed: {e}", fg="red", err=True)
154+
sys.exit(1)
155+
156+
click.secho(f"\n[OK] Successfully created: {output_path}", fg="green")
157+
return
158+
130159
if input_type == "nml":
131160
# Table conversion
132161
click.echo(f"Converting SUEWS tables to YAML")
@@ -144,18 +173,24 @@ def convert_table_cmd(
144173
"Could not detect version. Use -f to specify.", fg="red", err=True
145174
)
146175
sys.exit(1)
176+
elif fromVer not in list_ver_from:
177+
click.secho(
178+
f"Unsupported table release: {fromVer}. "
179+
f"Supported: {', '.join(list_ver_from)}",
180+
fg="red",
181+
err=True,
182+
)
183+
sys.exit(1)
147184

148185
elif input_type == "df_state":
149186
# df_state conversion
150187
click.echo(f"Converting df_state to YAML")
151188
click.echo(f" Input: {input_path}")
152189

153190
if fromVer:
154-
click.echo(
155-
" Note: Version specification ignored for df_state", fg="yellow"
156-
)
191+
click.echo(" Note: Version specification ignored for df_state")
157192

158-
# Perform conversion
193+
# Perform table / df_state conversion
159194
try:
160195
convert_to_yaml(
161196
input_file=str(input_path),
@@ -171,75 +206,5 @@ def convert_table_cmd(
171206
sys.exit(1)
172207

173208

174-
@convert_table_cmd.command("yaml-upgrade")
175-
@click.option(
176-
"-i",
177-
"--input",
178-
"input_file",
179-
help="Existing YAML configuration to upgrade.",
180-
type=click.Path(exists=True, dir_okay=False, file_okay=True),
181-
required=True,
182-
)
183-
@click.option(
184-
"-o",
185-
"--output",
186-
"output_file",
187-
help="Destination for the upgraded YAML.",
188-
type=click.Path(dir_okay=False),
189-
required=True,
190-
)
191-
@click.option(
192-
"-f",
193-
"--from-ver",
194-
"from_ver",
195-
help=(
196-
"Source release tag or schema version (e.g. '2026.4.3'). "
197-
"Omit to auto-detect from the YAML's schema_version field."
198-
),
199-
required=False,
200-
default=None,
201-
)
202-
@click.option(
203-
"-y",
204-
"--assume-yes",
205-
"assume_yes",
206-
is_flag=True,
207-
default=False,
208-
help="Skip confirmation prompts (for CI use).",
209-
)
210-
def yaml_upgrade_cmd(
211-
input_file: str,
212-
output_file: str,
213-
from_ver: str,
214-
assume_yes: bool,
215-
):
216-
"""Upgrade a YAML config from an earlier release to the current schema.
217-
218-
Examples:
219-
# Auto-detect source schema from the YAML's schema_version field
220-
suews-convert yaml-upgrade -i old.yml -o new.yml
221-
222-
# Or specify the release tag explicitly when the file has no signature
223-
suews-convert yaml-upgrade -i old.yml -o new.yml -f 2026.4.3
224-
"""
225-
from ..util.converter.yaml_upgrade import YamlUpgradeError, upgrade_yaml
226-
227-
try:
228-
upgrade_yaml(
229-
input_path=input_file,
230-
output_path=output_file,
231-
from_ver=from_ver,
232-
assume_yes=assume_yes,
233-
)
234-
except YamlUpgradeError as e:
235-
click.secho(f"[ERROR] {e}", fg="red", err=True)
236-
sys.exit(1)
237-
except Exception as e: # noqa: BLE001 - surface unexpected failures verbatim
238-
click.secho(f"[ERROR] yaml-upgrade failed: {e}", fg="red", err=True)
239-
sys.exit(1)
240-
241-
click.secho(f"[OK] Upgraded YAML written to {output_file}", fg="green")
242-
243-
244209
if __name__ == "__main__":
245210
convert_table_cmd()

src/supy/data_model/core/config.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3667,8 +3667,8 @@ def _transform_validation_error(
36673667
36683668
`had_signature` carries whether the source YAML actually shipped a
36693669
`schema_version` field. It is forwarded to `_drift_hint` so unsigned
3670-
YAMLs get a hint that asks for `-f/--from-ver <release-tag>` rather
3671-
than a bare `yaml-upgrade` command that the CLI would reject.
3670+
YAMLs get a hint that asks for `-f/--from <release-tag>` rather
3671+
than a bare `suews-convert` invocation that the CLI would reject.
36723672
"""
36733673

36743674
# Extract GRIDID mapping from sites
@@ -3765,17 +3765,14 @@ def _drift_hint(
37653765
except Exception: # noqa: BLE001 - detection is best-effort
37663766
detected = "unspecified"
37673767
detected_line = f" Detected schema version: {detected}\n"
3768-
upgrade_cmd = (
3769-
"suews-convert yaml-upgrade -i <old.yml> -o <new.yml>"
3770-
)
3768+
upgrade_cmd = "suews-convert -i <old.yml> -o <new.yml>"
37713769
else:
37723770
detected_line = (
37733771
" No schema_version field in YAML "
37743772
"(predates schema versioning).\n"
37753773
)
37763774
upgrade_cmd = (
3777-
"suews-convert yaml-upgrade -i <old.yml> -o <new.yml> "
3778-
"-f <release-tag>"
3775+
"suews-convert -i <old.yml> -o <new.yml> -f <release-tag>"
37793776
)
37803777

37813778
return (

src/supy/util/converter/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def detect_input_type(input_file: Union[str, Path]) -> str:
2727
Returns:
2828
'nml' for RunControl.nml (table conversion)
2929
'df_state' for CSV/pickle files
30+
'yaml' for an existing YAML configuration (cross-release upgrade)
3031
3132
Raises:
3233
ValueError: If input is not a file or has unknown extension
@@ -40,18 +41,22 @@ def detect_input_type(input_file: Union[str, Path]) -> str:
4041
raise ValueError(
4142
f"Input must be a file, not a directory. Got: {input_path}\n"
4243
f"For table conversion, specify: path/to/RunControl.nml\n"
43-
f"For df_state conversion, specify: path/to/df_state.csv or .pkl"
44+
f"For df_state conversion, specify: path/to/df_state.csv or .pkl\n"
45+
f"For YAML upgrade, specify: path/to/config.yml"
4446
)
4547

4648
# Check file type
4749
if input_path.name == "RunControl.nml" or input_path.suffix == ".nml":
4850
return "nml"
4951
elif input_path.suffix in [".csv", ".pkl", ".pickle"]:
5052
return "df_state"
53+
elif input_path.suffix in [".yml", ".yaml"]:
54+
return "yaml"
5155
else:
5256
raise ValueError(
5357
f"Unknown input file type: {input_path.suffix}\n"
54-
f"Supported: RunControl.nml for tables, .csv/.pkl for df_state"
58+
f"Supported: RunControl.nml for tables, .csv/.pkl for df_state, "
59+
f".yml/.yaml for cross-release YAML upgrade"
5560
)
5661

5762

src/supy/util/converter/yaml_upgrade.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ def upgrade_yaml(
206206
input_path: str | Path,
207207
output_path: str | Path,
208208
from_ver: str | None = None,
209-
assume_yes: bool = False,
210209
) -> None:
211210
"""Upgrade a YAML configuration written for an earlier release.
212211
@@ -219,9 +218,6 @@ def upgrade_yaml(
219218
from_ver : str, optional
220219
Source schema version or release tag. When omitted, the source version
221220
is auto-detected from the YAML's `schema_version` / `version` field.
222-
assume_yes : bool, default False
223-
When False and the upgrade path is a no-op, emit a brief status and
224-
still write the output. Reserved for future confirmation prompts.
225221
"""
226222
input_path = Path(input_path)
227223
output_path = Path(output_path)
@@ -235,7 +231,7 @@ def upgrade_yaml(
235231
if signature is None:
236232
raise YamlUpgradeError(
237233
"No schema_version field found. This YAML predates the "
238-
f"v{CURRENT_SCHEMA_VERSION} schema. Re-run with -f/--from-ver "
234+
f"v{CURRENT_SCHEMA_VERSION} schema. Re-run with -f/--from "
239235
"<tag> to specify the source version explicitly."
240236
)
241237
source_schema = _resolve_package_to_schema(signature)
@@ -244,12 +240,12 @@ def upgrade_yaml(
244240
source_schema = _resolve_package_to_schema(from_ver)
245241
if signature is not None and _resolve_package_to_schema(signature) != source_schema:
246242
_log(
247-
f"[yaml-upgrade] WARNING: user-supplied --from-ver={from_ver} "
243+
f"[yaml-upgrade] WARNING: user-supplied --from={from_ver} "
248244
f"(schema {source_schema}) disagrees with file signature "
249245
f"{signature}. Respecting user override."
250246
)
251247
else:
252-
_log(f"[yaml-upgrade] Using user-supplied --from-ver={from_ver}")
248+
_log(f"[yaml-upgrade] Using user-supplied --from={from_ver}")
253249

254250
target_schema = CURRENT_SCHEMA_VERSION
255251
_log(

0 commit comments

Comments
 (0)