Skip to content

Commit ff5a72f

Browse files
committed
fix(t2-4): address review feedback — move warning to load_config, fix edge cases
- Warning moved from load_project() to load_config() and switched to Rich console helper, so it renders in-band and does not appear on fresh projects or on tycoon init --upgrade (neither goes through load_config) - load_project() now raises ValueError for schema_version > SCHEMA_VERSION, mirroring migrate_project's three-way split - save_project() stamps schema_version on every write so a load+save round-trip (sources add/remove) silently upgrades the stamp - scaffold_blank_project() and scaffold_from_template() write schema_version on new projects, eliminating the false-alarm on init - migrate_project() checks isinstance(existing, bool) before int to catch schema_version: true - tycoon init --upgrade wraps migrate_project in try/except ValueError for clean error output instead of a typer traceback
1 parent 9bc2d8c commit ff5a72f

7 files changed

Lines changed: 101 additions & 37 deletions

File tree

src/tycoon/commands/init.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,11 @@ def init_cmd(
519519
if not (target / "tycoon.yml").exists():
520520
error("No tycoon.yml found in the current directory. Run 'tycoon init' to create one.")
521521
raise typer.Exit(1)
522-
changed = migrate_project(target)
522+
try:
523+
changed = migrate_project(target)
524+
except ValueError as exc:
525+
error(str(exc))
526+
raise typer.Exit(1) from exc
523527
if changed:
524528
success("tycoon.yml migrated to the current schema version.")
525529
else:

src/tycoon/config.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
from pathlib import Path
1010

11-
from tycoon.project import PROJECT_FILENAME, TycoonProject, load_project
11+
from tycoon.project import PROJECT_FILENAME, SCHEMA_VERSION, TycoonProject, load_project
12+
from tycoon.utils.console import warn as _warn_console
1213

1314
# v0.1 defaults (used when no tycoon.yml exists)
1415
_DEFAULT_RAW_DB = "data/raw.duckdb"
@@ -124,8 +125,18 @@ def load_config() -> TycoonConfig:
124125
"""Return a TycoonConfig rooted at the nearest project directory.
125126
126127
Prefer this over importing _find_project_root across module boundaries.
128+
Emits a console warning when tycoon.yml is at an older schema version.
127129
"""
128-
return TycoonConfig(project_root=_find_project_root())
130+
cfg = TycoonConfig(project_root=_find_project_root())
131+
if cfg.project is not None:
132+
sv = cfg.project.schema_version
133+
if sv is None or sv < SCHEMA_VERSION:
134+
current = sv if sv is not None else "none"
135+
_warn_console(
136+
f"tycoon.yml is at schema version {current}, current is {SCHEMA_VERSION}. "
137+
"Run 'tycoon init --upgrade' to migrate."
138+
)
139+
return cfg
129140

130141

131142
# Singleton (used by modules not yet migrated to load_config)

src/tycoon/project.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,6 @@ def _check_path_field(cls, v: str | None) -> str | None:
398398

399399
def load_project(project_root: Path) -> TycoonProject | None:
400400
"""Load and validate tycoon.yml from the given root. Returns None if not found."""
401-
import warnings
402-
403401
path = project_root / PROJECT_FILENAME
404402
if not path.exists():
405403
return None
@@ -408,12 +406,10 @@ def load_project(project_root: Path) -> TycoonProject | None:
408406
return TycoonProject()
409407
raw = _interpolate_allowed_fields(raw)
410408
project = TycoonProject.model_validate(raw)
411-
if project.schema_version is None or project.schema_version < SCHEMA_VERSION:
412-
current = project.schema_version if project.schema_version is not None else "none"
413-
warnings.warn(
414-
f"tycoon.yml is at schema version {current}, current is {SCHEMA_VERSION}. "
415-
"Run 'tycoon init --upgrade' to migrate.",
416-
stacklevel=2,
409+
if project.schema_version is not None and project.schema_version > SCHEMA_VERSION:
410+
raise ValueError(
411+
f"tycoon.yml schema_version {project.schema_version} is newer than this tycoon supports "
412+
f"({SCHEMA_VERSION}). Upgrade tycoon-cli to use this project."
417413
)
418414
return project
419415

@@ -431,6 +427,7 @@ def save_project(project: TycoonProject, project_root: Path) -> None:
431427
"""
432428
path = project_root / PROJECT_FILENAME
433429
data = project.model_dump(by_alias=True, exclude_none=True, mode="json")
430+
data["schema_version"] = SCHEMA_VERSION
434431
if path.exists():
435432
existing = yaml.safe_load(path.read_text())
436433
if isinstance(existing, dict):
@@ -466,7 +463,7 @@ def migrate_project(project_root: Path) -> bool:
466463
return False
467464

468465
existing = raw.get("schema_version")
469-
if existing is not None and not isinstance(existing, int):
466+
if existing is not None and (isinstance(existing, bool) or not isinstance(existing, int)):
470467
raise ValueError(f"tycoon.yml schema_version must be an integer, got {type(existing).__name__}: {existing!r}")
471468
if existing is not None and existing > SCHEMA_VERSION:
472469
raise ValueError(

src/tycoon/scaffolding/templates.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import typer
1515
import yaml
1616

17-
from tycoon.project import StackConfig
17+
from tycoon.project import SCHEMA_VERSION, StackConfig
1818
from tycoon.utils.console import info, success, warn
1919

2020

@@ -159,6 +159,7 @@ def scaffold_blank_project(
159159
project_data: dict = {
160160
"name": name,
161161
"version": "0.1.0",
162+
"schema_version": SCHEMA_VERSION,
162163
"database": {
163164
"raw": raw_db_path,
164165
"warehouse": warehouse_db_path,
@@ -537,6 +538,9 @@ def scaffold_from_template(
537538
warn("tycoon.yml already exists, skipping")
538539
else:
539540
shutil.copy2(src_yml, dst_yml)
541+
_raw = yaml.safe_load(dst_yml.read_text()) or {}
542+
_raw["schema_version"] = SCHEMA_VERSION
543+
dst_yml.write_text(yaml.dump(_raw, default_flow_style=False, sort_keys=False))
540544
success(f"Created tycoon.yml from template '{template_name}'")
541545

542546
# Copy any subdirectories from the template (e.g. dbt_project/, rill/)

tests/test_config.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from pathlib import Path
66

7-
from tycoon.config import TycoonConfig
7+
from tycoon.config import TycoonConfig, load_config
8+
from tycoon.project import SCHEMA_VERSION
89

910

1011
class TestTycoonConfig:
@@ -36,3 +37,52 @@ def test_paths_relative_to_project_root(self, tmp_config):
3637
assert tmp_config.data_dir == tmp_config.root / "data"
3738
assert tmp_config.dbt_project_dir == tmp_config.root / "dbt_project"
3839
assert tmp_config.rill_dir == tmp_config.root / "rill"
40+
41+
42+
class TestLoadConfigSchemaWarning:
43+
"""T2-4: load_config warns via console when tycoon.yml schema_version is stale."""
44+
45+
def test_warns_when_schema_version_absent(self, tmp_path, monkeypatch):
46+
(tmp_path / "tycoon.yml").write_text("name: old-project\n")
47+
monkeypatch.chdir(tmp_path)
48+
49+
calls = []
50+
monkeypatch.setattr("tycoon.config._warn_console", lambda msg: calls.append(msg))
51+
52+
load_config()
53+
54+
assert len(calls) == 1
55+
assert "tycoon init --upgrade" in calls[0]
56+
57+
def test_warns_when_schema_version_old(self, tmp_path, monkeypatch):
58+
(tmp_path / "tycoon.yml").write_text(f"name: old\nschema_version: {SCHEMA_VERSION - 1}\n")
59+
monkeypatch.chdir(tmp_path)
60+
61+
calls = []
62+
monkeypatch.setattr("tycoon.config._warn_console", lambda msg: calls.append(msg))
63+
64+
load_config()
65+
66+
assert len(calls) == 1
67+
68+
def test_no_warning_when_current(self, tmp_path, monkeypatch):
69+
(tmp_path / "tycoon.yml").write_text(f"name: current\nschema_version: {SCHEMA_VERSION}\n")
70+
monkeypatch.chdir(tmp_path)
71+
72+
calls = []
73+
monkeypatch.setattr("tycoon.config._warn_console", lambda msg: calls.append(msg))
74+
75+
load_config()
76+
77+
assert calls == []
78+
79+
def test_no_warning_without_tycoon_yml(self, tmp_path, monkeypatch):
80+
(tmp_path / "pyproject.toml").write_text('[project]\nname = "x"\n')
81+
monkeypatch.chdir(tmp_path)
82+
83+
calls = []
84+
monkeypatch.setattr("tycoon.config._warn_console", lambda msg: calls.append(msg))
85+
86+
load_config()
87+
88+
assert calls == []

tests/test_init.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,12 @@ def test_upgrade_already_current_reports_up_to_date(self, cli_runner, tmp_path,
327327

328328
assert result.exit_code == 0
329329
assert "up to date" in result.stdout
330+
331+
def test_upgrade_future_schema_version_exits_nonzero_cleanly(self, cli_runner, tmp_path, monkeypatch):
332+
monkeypatch.chdir(tmp_path)
333+
(tmp_path / "tycoon.yml").write_text(f"name: future\nschema_version: {SCHEMA_VERSION + 1}\n")
334+
335+
result = cli_runner.invoke(app, ["init", "--upgrade"])
336+
337+
assert result.exit_code != 0
338+
assert "newer than this tycoon supports" in result.output

tests/test_project.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -195,36 +195,25 @@ def test_metadata_field_parses(self, tmp_path):
195195
assert p.metadata.path == ".tycoon/custom_meta.duckdb"
196196

197197

198-
class TestSchemaVersionWarning:
199-
"""T2-4: load_project emits a warning when schema_version is absent or old."""
198+
class TestSchemaVersionEnforcement:
199+
"""T2-4: load_project raises on future schema_version; save_project stamps it."""
200200

201-
def test_warns_when_schema_version_absent(self, tmp_path):
201+
def test_future_schema_version_raises_in_load(self, tmp_path):
202202
import pytest
203203

204-
(tmp_path / "tycoon.yml").write_text("name: old-project\n")
205-
206-
with pytest.warns(UserWarning, match="Run 'tycoon init --upgrade'"):
207-
load_project(tmp_path)
204+
(tmp_path / "tycoon.yml").write_text(f"name: future\nschema_version: {SCHEMA_VERSION + 1}\n")
208205

209-
def test_warns_when_schema_version_old(self, tmp_path):
210-
import pytest
211-
212-
(tmp_path / "tycoon.yml").write_text(f"name: old-project\nschema_version: {SCHEMA_VERSION - 1}\n")
213-
214-
with pytest.warns(UserWarning, match="Run 'tycoon init --upgrade'"):
206+
with pytest.raises(ValueError, match="newer than this tycoon supports"):
215207
load_project(tmp_path)
216208

217-
def test_no_warning_when_current(self, tmp_path):
218-
import warnings
219-
220-
(tmp_path / "tycoon.yml").write_text(f"name: current\nschema_version: {SCHEMA_VERSION}\n")
221-
222-
with warnings.catch_warnings():
223-
warnings.filterwarnings("error", category=UserWarning)
224-
p = load_project(tmp_path)
225-
209+
def test_save_project_stamps_schema_version(self, tmp_path):
210+
(tmp_path / "tycoon.yml").write_text("name: old-project\n")
211+
p = load_project(tmp_path)
226212
assert p is not None
227-
assert p.schema_version == SCHEMA_VERSION
213+
save_project(p, tmp_path)
214+
reloaded = load_project(tmp_path)
215+
assert reloaded is not None
216+
assert reloaded.schema_version == SCHEMA_VERSION
228217

229218

230219
class TestMigrateProject:

0 commit comments

Comments
 (0)