Skip to content

Commit d9b5606

Browse files
authored
Merge pull request #2 from ahrzb/chore/ci-and-lint
ci: GitHub Actions (Ubuntu, uv, ruff, pytest) + repo lint cleanup
2 parents 42b9f12 + bc8a582 commit d9b5606

25 files changed

Lines changed: 319 additions & 82 deletions

.git-blame-ignore-revs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Commits skipped by `git blame` — bulk, mechanical reformats with no logic changes.
2+
# GitHub applies this file automatically. Enable it locally with:
3+
# git config blame.ignoreRevsFile .git-blame-ignore-revs
4+
5+
# style: apply ruff format + autofix across the repo
6+
2708018aa86c7f9bb229bf6b422e3c1531564a30

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
18+
# The package is a pyo3/maturin extension, so CI needs a Rust toolchain
19+
# to build it during `uv sync`.
20+
- name: Install Rust toolchain
21+
uses: dtolnay/rust-toolchain@stable
22+
23+
- name: Cache Rust build
24+
uses: Swatinem/rust-cache@v2
25+
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@v7
28+
with:
29+
enable-cache: true
30+
31+
# Provisions Python 3.14 (per requires-python), builds the extension, and
32+
# installs locked deps.
33+
- name: Build extension + install deps
34+
run: uv sync --locked
35+
36+
# Lint + formatting go through pre-commit so CI enforces exactly what the
37+
# local hooks do (config: .pre-commit-config.yaml), rather than a second,
38+
# drift-prone copy of the ruff invocations.
39+
- name: Lint + format (pre-commit)
40+
run: uv run pre-commit run --all-files --show-diff-on-failure
41+
42+
- name: Tests
43+
run: uv run pytest -q

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Single source of truth for lint + formatting.
2+
#
3+
# CI does not run ruff directly — it runs `pre-commit run --all-files`, so what
4+
# blocks a PR is exactly what runs on your machine. `rev` is pinned to the ruff
5+
# version locked in uv.lock; bump both together or local and CI will disagree.
6+
#
7+
# One-time local setup: uv run pre-commit install
8+
repos:
9+
- repo: https://github.com/astral-sh/ruff-pre-commit
10+
rev: v0.15.21
11+
hooks:
12+
- id: ruff-check
13+
args: [--fix]
14+
- id: ruff-format

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dev = [
1919
"notebook>=7.6.0",
2020
"numpy>=2.0",
2121
"pandas>=2.0",
22+
"pre-commit>=4.6.0",
2223
"pytest>=8.3.5",
2324
"ruff>=0.11.7",
2425
"scikit-learn>=1.5",
@@ -41,6 +42,8 @@ lint.ignore = ["S101"]
4142
[tool.ruff.lint.per-file-ignores]
4243
"*test*.py" = ["S608"]
4344
"_state.py" = ["S608"]
45+
# Doc generator: emits long markdown lines verbatim; wrapping would corrupt output.
46+
"scripts/gen_datafusion_catalogue.py" = ["E501"]
4447

4548
[tool.pytest.ini_options]
4649
python_files = ["*_test.py", "test_*.py"]

scripts/gen_datafusion_catalogue.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ def main() -> None:
8888
("Window functions", "WINDOW"),
8989
("Scalar functions", "SCALAR"),
9090
]:
91-
lines += [f"## {title} ({len(groups[key])})", "", "| Function | Signature | Description |", "|---|---|---|"]
91+
lines += [
92+
f"## {title} ({len(groups[key])})",
93+
"",
94+
"| Function | Signature | Description |",
95+
"|---|---|---|",
96+
]
9297
lines += [
9398
f"| `{name}` | {clean(syn) or '—'} | {clean(desc) or '—'} |"
9499
for name, _ftype, syn, desc in groups[key]

sql_transform/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def fit(
9393
self._rewritten_sql,
9494
row_tables={"__THIS__": this_model},
9595
static_tables=self._state_tables,
96-
transformers={n: (obj, out_s) for n, (obj, in_s, out_s) in self._udf_specs.items()},
96+
transformers={
97+
n: (obj, out_s) for n, (obj, in_s, out_s) in self._udf_specs.items()
98+
},
9799
)
98100
return self
99101

@@ -105,7 +107,9 @@ def transform(self, table: pa.Table, /) -> pa.Table:
105107
the Rust engine instead."""
106108
if self._infer_fn is None:
107109
raise RuntimeError("Must call fit() before transform")
108-
return run_batch(self._rewritten_sql, table, self._state_tables, self._udf_specs)
110+
return run_batch(
111+
self._rewritten_sql, table, self._state_tables, self._udf_specs
112+
)
109113

110114
def infer(self, row: dict[str, Any] | BaseModel, /) -> BaseModel:
111115
"""Single-row inference through the Rust InferFn against the frozen

sql_transform/_codegen/plan.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ class Cast:
191191

192192
@dataclass
193193
class Case:
194-
arms: list # list[tuple[cond, result]]
195-
default: Any # result expr; Literal(None) when no ELSE
196-
has_else: bool # whether an explicit ELSE was written
194+
arms: list # list[tuple[cond, result]]
195+
default: Any # result expr; Literal(None) when no ELSE
196+
has_else: bool # whether an explicit ELSE was written
197197

198198

199199
@dataclass

sql_transform/_codegen/plan_test.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,7 @@ def test_infer_type_arithmetic_and_nullability():
339339

340340

341341
def test_infer_type_functions_and_casts():
342-
schemas = {
343-
"t": {"s": cp.FieldType(cp.STR, False), "i": cp.FieldType(cp.INT, True)}
344-
}
342+
schemas = {"t": {"s": cp.FieldType(cp.STR, False), "i": cp.FieldType(cp.INT, True)}}
345343
upper_s = cp.Func("upper", [cp.Column("t", "s")])
346344
assert cp.infer_type(upper_s, schemas).base == cp.STR
347345

sql_transform/_compose.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
@dataclass(frozen=True)
2929
class Ref:
3030
transform: object # a SQLTransform, or a fitted transformer if is_transformer
31-
frozen: bool # True for {a.transform}; False for bare {a}
32-
expr_text: str # interpolation source, for error messages
31+
frozen: bool # True for {a.transform}; False for bare {a}
32+
expr_text: str # interpolation source, for error messages
3333
is_transformer: bool = False
3434

3535

@@ -186,8 +186,10 @@ def fit_into_scope(
186186
# Remap inner's single __THIS__ column -> input_expr throughout the tree,
187187
# so its window aggregates are over input_expr (agg-over-expression).
188188
def remap(n):
189-
if isinstance(n, exp.Column) and n.name == inner_col and not (
190-
n.table and n.table.startswith("__STATE")
189+
if (
190+
isinstance(n, exp.Column)
191+
and n.name == inner_col
192+
and not (n.table and n.table.startswith("__STATE"))
191193
):
192194
return input_expr.copy()
193195
return n
@@ -211,9 +213,11 @@ def remap(n):
211213
if isinstance(frozen_expr, exp.Alias):
212214
frozen_expr = frozen_expr.this
213215
frozen_expr = frozen_expr.transform(
214-
lambda n: exp.column(n.name, table=scope)
215-
if isinstance(n, exp.Column) and n.table in rename
216-
else n
216+
lambda n: (
217+
exp.column(n.name, table=scope)
218+
if isinstance(n, exp.Column) and n.table in rename
219+
else n
220+
)
217221
)
218222
return frozen_expr, scoped_state
219223

sql_transform/_compose_test.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ def test_multi_input_inner_raises_clean_value_error():
3232
"SELECT age / AVG(age) OVER (PARTITION BY city) AS e FROM __THIS__"
3333
).fit(train)
3434
with pytest.raises(ValueError, match="one input column"):
35-
SQLTransform(
36-
t"SELECT {grouped.transform}(age) AS e2 FROM __THIS__"
37-
).fit(train)
35+
SQLTransform(t"SELECT {grouped.transform}(age) AS e2 FROM __THIS__").fit(train)
3836

3937

4038
def test_frozen_reference_on_unfit_errors():
@@ -51,7 +49,9 @@ def test_bare_reference_on_fitted_transform_is_ambiguous_error_via_cascade():
5149
train_v = pa.table({"v": [10.0, 20.0, 30.0, 40.0]})
5250
train = pa.table({"age": [10.0, 20.0, 30.0, 40.0]})
5351
b = SQLTransform("SELECT v * 2 AS d FROM __THIS__").fit(train_v)
54-
a = SQLTransform("SELECT (v - AVG(v) OVER ()) / STDDEV(v) OVER () AS s FROM __THIS__")
52+
a = SQLTransform(
53+
"SELECT (v - AVG(v) OVER ()) / STDDEV(v) OVER () AS s FROM __THIS__"
54+
)
5555
with pytest.raises(ValueError, match="already fitted"):
5656
SQLTransform(t"SELECT {a}({b}(age)) AS z FROM __THIS__").fit(train)
5757

@@ -62,7 +62,9 @@ def test_frozen_reference_on_unfit_errors_via_cascade():
6262
# path too.
6363
train = pa.table({"age": [10.0, 20.0, 30.0, 40.0]})
6464
b = SQLTransform("SELECT v * 2 AS d FROM __THIS__") # unfit
65-
a = SQLTransform("SELECT (v - AVG(v) OVER ()) / STDDEV(v) OVER () AS s FROM __THIS__")
65+
a = SQLTransform(
66+
"SELECT (v - AVG(v) OVER ()) / STDDEV(v) OVER () AS s FROM __THIS__"
67+
)
6668
with pytest.raises(ValueError, match="not fitted"):
6769
SQLTransform(t"SELECT {a}({b.transform}(age)) AS z FROM __THIS__").fit(train)
6870

@@ -94,9 +96,9 @@ def test_reference_not_applied_to_column_errors():
9496
"SELECT (age - AVG(age) OVER ()) / STDDEV(age) OVER () AS s FROM __THIS__"
9597
).fit(train)
9698
with pytest.raises(ValueError, match="single input column"):
97-
SQLTransform(
98-
t"SELECT {scaler.transform}(age + 1) AS s FROM __THIS__"
99-
).fit(train)
99+
SQLTransform(t"SELECT {scaler.transform}(age + 1) AS s FROM __THIS__").fit(
100+
train
101+
)
100102

101103

102104
def test_non_transform_interpolation_errors():

0 commit comments

Comments
 (0)