Skip to content

Commit db42874

Browse files
committed
Fix ruff placement and ValidationApp indentation
- Keep ruff in [project.optional-dependencies] dev for CI - Remove ruff from [dependency-groups] to avoid duplication - Fix ValidationApp class indentation (was incorrectly nested) - Add .claude/CLAUDE.md with linting requirements - Run ruff format on cli.py
1 parent ab44f8b commit db42874

4 files changed

Lines changed: 183 additions & 145 deletions

File tree

.claude/CLAUDE.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Sidemantic Project Guidelines
2+
3+
## CRITICAL: Before Every Commit
4+
5+
**ALWAYS run linting and formatting before committing:**
6+
7+
```bash
8+
uv run ruff check --fix .
9+
uv run ruff format .
10+
```
11+
12+
This is NON-NEGOTIABLE. If you modify Python code, you MUST lint and format before committing.
13+
14+
**Why this matters:**
15+
- CI runs ruff check and will fail if code isn't formatted
16+
- Ruff must be installed in `[project.optional-dependencies] dev` for CI
17+
- NOT in `[dependency-groups]` (that's uv-specific, CI uses optional-dependencies)
18+
19+
## Dependency Management
20+
21+
- Use `uv` for all Python package management
22+
- Ruff should be in dev dependencies (`[dependency-groups] dev`)
23+
- DO NOT add dev tools to main dependencies unless explicitly requested
24+
- Optional features use `[project.optional-dependencies]`:
25+
- `workbench` - textual, plotext (for TUI)
26+
- `serve` - riffq, pyarrow (for PostgreSQL server)
27+
28+
## Testing
29+
30+
Run tests before committing significant changes:
31+
```bash
32+
uv run pytest -v
33+
```
34+
35+
## Common Mistakes to Avoid
36+
37+
1. **Breaking ruff** - Always ensure ruff is installed in dev dependencies
38+
2. **Pyodide compatibility** - Keep heavy deps (textual, pygments) optional
39+
3. **Not linting** - Format and lint EVERY TIME before commit

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,4 @@ pythonpath = ["."]
8989
dev = [
9090
"marimo==0.16.5",
9191
"pytest>=8.4.2",
92-
"ruff>=0.13.2",
9392
]

sidemantic/cli.py

Lines changed: 144 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -918,163 +918,165 @@ def _render_chart(self) -> None:
918918
class ValidationApp(App):
919919
"""Interactive validation results viewer."""
920920

921-
CSS = """
922-
Screen {
923-
background: $surface;
924-
}
925-
926-
.section {
927-
margin: 1 2;
928-
padding: 1;
929-
border: solid $primary;
930-
}
931-
932-
.section-title {
933-
text-style: bold;
934-
margin-bottom: 1;
935-
}
936-
937-
.error {
938-
color: $error;
939-
}
921+
CSS = """
922+
Screen {
923+
background: $surface;
924+
}
940925
941-
.warning {
942-
color: $warning;
943-
}
926+
.section {
927+
margin: 1 2;
928+
padding: 1;
929+
border: solid $primary;
930+
}
944931
945-
.success {
946-
color: $success;
947-
}
932+
.section-title {
933+
text-style: bold;
934+
margin-bottom: 1;
935+
}
948936
949-
.info {
950-
color: $accent;
951-
}
952-
"""
937+
.error {
938+
color: $error;
939+
}
953940
954-
BINDINGS = [
955-
Binding("ctrl+c", "quit", "Quit"),
956-
]
941+
.warning {
942+
color: $warning;
943+
}
957944
958-
def __init__(self, directory: Path, verbose: bool = False):
959-
super().__init__()
960-
self.directory = directory
961-
self.verbose = verbose
962-
self.errors = []
963-
self.warnings = []
964-
self.info = []
945+
.success {
946+
color: $success;
947+
}
965948
966-
def compose(self) -> ComposeResult:
967-
"""Create child widgets."""
968-
yield Header()
969-
with VerticalScroll():
970-
yield Static("", id="validation-results")
971-
yield Footer()
949+
.info {
950+
color: $accent;
951+
}
952+
"""
953+
954+
BINDINGS = [
955+
Binding("ctrl+c", "quit", "Quit"),
956+
]
957+
958+
def __init__(self, directory: Path, verbose: bool = False):
959+
super().__init__()
960+
self.directory = directory
961+
self.verbose = verbose
962+
self.errors = []
963+
self.warnings = []
964+
self.info = []
965+
966+
def compose(self) -> ComposeResult:
967+
"""Create child widgets."""
968+
yield Header()
969+
with VerticalScroll():
970+
yield Static("", id="validation-results")
971+
yield Footer()
972+
973+
def on_mount(self) -> None:
974+
"""Run validation."""
975+
try:
976+
layer = SemanticLayer()
977+
load_from_directory(layer, str(self.directory))
972978

973-
def on_mount(self) -> None:
974-
"""Run validation."""
975-
try:
976-
layer = SemanticLayer()
977-
load_from_directory(layer, str(self.directory))
979+
if not layer.graph.models:
980+
self.exit(message="No models found in directory")
981+
return
978982

979-
if not layer.graph.models:
980-
self.exit(message="No models found in directory")
981-
return
983+
self.info.append(f"Loaded {len(layer.graph.models)} models")
982984

983-
self.info.append(f"Loaded {len(layer.graph.models)} models")
984-
985-
# Validate each model
986-
for model_name, model in layer.graph.models.items():
987-
# Check primary key
988-
if not model.primary_key:
989-
self.warnings.append(f"Model '{model_name}' has no primary key defined")
990-
991-
# Check for dimensions
992-
if not model.dimensions:
993-
self.warnings.append(f"Model '{model_name}' has no dimensions")
994-
995-
# Check for metrics
996-
if not model.metrics:
997-
self.warnings.append(f"Model '{model_name}' has no metrics")
998-
999-
# Validate relationships
1000-
for rel in model.relationships:
1001-
if rel.name not in layer.graph.models:
1002-
self.errors.append(f"Model '{model_name}' has relationship to '{rel.name}' which doesn't exist")
1003-
1004-
# Check for duplicate dimension names
1005-
dim_names = [d.name for d in model.dimensions]
1006-
duplicates = [name for name in set(dim_names) if dim_names.count(name) > 1]
1007-
if duplicates:
1008-
self.errors.append(f"Model '{model_name}' has duplicate dimensions: {', '.join(duplicates)}")
1009-
1010-
# Check for duplicate metric names
1011-
metric_names = [m.name for m in model.metrics]
1012-
duplicates = [name for name in set(metric_names) if metric_names.count(name) > 1]
1013-
if duplicates:
1014-
self.errors.append(f"Model '{model_name}' has duplicate metrics: {', '.join(duplicates)}")
1015-
1016-
# Check for orphaned models
1017-
if len(layer.graph.models) > 1:
1018-
orphaned = []
985+
# Validate each model
1019986
for model_name, model in layer.graph.models.items():
1020-
has_outgoing = len(model.relationships) > 0
1021-
has_incoming = any(
1022-
any(r.name == model_name for r in m.relationships)
1023-
for name, m in layer.graph.models.items()
1024-
if name != model_name
1025-
)
1026-
if not has_outgoing and not has_incoming:
1027-
orphaned.append(model_name)
1028-
1029-
if orphaned:
1030-
self.warnings.append(f"Orphaned models (no relationships): {', '.join(orphaned)}")
987+
# Check primary key
988+
if not model.primary_key:
989+
self.warnings.append(f"Model '{model_name}' has no primary key defined")
1031990

1032-
# Add summary stats
1033-
total_dims = sum(len(m.dimensions) for m in layer.graph.models.values())
1034-
total_metrics = sum(len(m.metrics) for m in layer.graph.models.values())
1035-
total_rels = sum(len(m.relationships) for m in layer.graph.models.values())
991+
# Check for dimensions
992+
if not model.dimensions:
993+
self.warnings.append(f"Model '{model_name}' has no dimensions")
1036994

1037-
self.info.append(f"Total dimensions: {total_dims}")
1038-
self.info.append(f"Total metrics: {total_metrics}")
1039-
self.info.append(f"Total relationships: {total_rels}")
995+
# Check for metrics
996+
if not model.metrics:
997+
self.warnings.append(f"Model '{model_name}' has no metrics")
1040998

1041-
# Display results
1042-
self._update_display()
999+
# Validate relationships
1000+
for rel in model.relationships:
1001+
if rel.name not in layer.graph.models:
1002+
self.errors.append(
1003+
f"Model '{model_name}' has relationship to '{rel.name}' which doesn't exist"
1004+
)
10431005

1044-
except Exception as e:
1045-
self.exit(message=f"Error during validation: {e}")
1046-
1047-
def _update_display(self) -> None:
1048-
"""Update the validation results display."""
1049-
results = self.query_one("#validation-results", Static)
1050-
content = []
1051-
1052-
content.append(f"[bold]Validation Results: {self.directory}[/bold]\n")
1053-
1054-
if self.errors:
1055-
content.append("[bold error]✗ Errors[/bold error]")
1056-
for error in self.errors:
1057-
content.append(f" [error]✗[/error] {error}")
1058-
content.append("")
1059-
1060-
if self.warnings:
1061-
content.append("[bold warning]⚠ Warnings[/bold warning]")
1062-
for warning in self.warnings:
1063-
content.append(f" [warning]⚠[/warning] {warning}")
1064-
content.append("")
1065-
1066-
if self.verbose or not (self.errors or self.warnings):
1067-
content.append("[bold info]ℹ Info[/bold info]")
1068-
for i in self.info:
1069-
content.append(f" [info]ℹ[/info] {i}")
1070-
content.append("")
1071-
1072-
if not self.errors:
1073-
content.append("\n[bold success]✓ Validation Passed[/bold success]")
1074-
else:
1075-
content.append("\n[bold error]✗ Validation Failed[/bold error]")
1006+
# Check for duplicate dimension names
1007+
dim_names = [d.name for d in model.dimensions]
1008+
duplicates = [name for name in set(dim_names) if dim_names.count(name) > 1]
1009+
if duplicates:
1010+
self.errors.append(f"Model '{model_name}' has duplicate dimensions: {', '.join(duplicates)}")
1011+
1012+
# Check for duplicate metric names
1013+
metric_names = [m.name for m in model.metrics]
1014+
duplicates = [name for name in set(metric_names) if metric_names.count(name) > 1]
1015+
if duplicates:
1016+
self.errors.append(f"Model '{model_name}' has duplicate metrics: {', '.join(duplicates)}")
1017+
1018+
# Check for orphaned models
1019+
if len(layer.graph.models) > 1:
1020+
orphaned = []
1021+
for model_name, model in layer.graph.models.items():
1022+
has_outgoing = len(model.relationships) > 0
1023+
has_incoming = any(
1024+
any(r.name == model_name for r in m.relationships)
1025+
for name, m in layer.graph.models.items()
1026+
if name != model_name
1027+
)
1028+
if not has_outgoing and not has_incoming:
1029+
orphaned.append(model_name)
1030+
1031+
if orphaned:
1032+
self.warnings.append(f"Orphaned models (no relationships): {', '.join(orphaned)}")
1033+
1034+
# Add summary stats
1035+
total_dims = sum(len(m.dimensions) for m in layer.graph.models.values())
1036+
total_metrics = sum(len(m.metrics) for m in layer.graph.models.values())
1037+
total_rels = sum(len(m.relationships) for m in layer.graph.models.values())
1038+
1039+
self.info.append(f"Total dimensions: {total_dims}")
1040+
self.info.append(f"Total metrics: {total_metrics}")
1041+
self.info.append(f"Total relationships: {total_rels}")
1042+
1043+
# Display results
1044+
self._update_display()
1045+
1046+
except Exception as e:
1047+
self.exit(message=f"Error during validation: {e}")
1048+
1049+
def _update_display(self) -> None:
1050+
"""Update the validation results display."""
1051+
results = self.query_one("#validation-results", Static)
1052+
content = []
1053+
1054+
content.append(f"[bold]Validation Results: {self.directory}[/bold]\n")
1055+
1056+
if self.errors:
1057+
content.append("[bold error]✗ Errors[/bold error]")
1058+
for error in self.errors:
1059+
content.append(f" [error]✗[/error] {error}")
1060+
content.append("")
1061+
1062+
if self.warnings:
1063+
content.append("[bold warning]⚠ Warnings[/bold warning]")
1064+
for warning in self.warnings:
1065+
content.append(f" [warning]⚠[/warning] {warning}")
1066+
content.append("")
1067+
1068+
if self.verbose or not (self.errors or self.warnings):
1069+
content.append("[bold info]ℹ Info[/bold info]")
1070+
for i in self.info:
1071+
content.append(f" [info]ℹ[/info] {i}")
1072+
content.append("")
1073+
1074+
if not self.errors:
1075+
content.append("\n[bold success]✓ Validation Passed[/bold success]")
1076+
else:
1077+
content.append("\n[bold error]✗ Validation Failed[/bold error]")
10761078

1077-
results.update("\n".join(content))
1079+
results.update("\n".join(content))
10781080

10791081

10801082
@app.command()

uv.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)