typedframes 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {typedframes-0.7.0 → typedframes-0.9.0}/PKG-INFO +12 -12
- {typedframes-0.7.0 → typedframes-0.9.0}/README.md +11 -11
- {typedframes-0.7.0 → typedframes-0.9.0}/pyproject.toml +1 -1
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/Cargo.lock +1 -1
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/Cargo.toml +1 -1
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/ast_extract.rs +38 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/constants.rs +84 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/errors.rs +19 -0
- typedframes-0.9.0/rust/src/frame_ops.rs +1335 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/index.rs +418 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/lib.rs +1 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/linter.rs +3982 -558
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/notebook.rs +40 -1
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/pyapi.rs +2 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/__init__.py +1 -1
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/cli.py +144 -22
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/.cargo/config.toml +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/README.md +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/benches/parser_bench.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/config.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/contract.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/main.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/sql.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/typo.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/rust/tests/integration_test.rs +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/_rust_checker.pyi +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/base_schema.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_group.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_group_error.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_set.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/missing_dependency_error.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/mypy.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/pandas.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/pandera.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/polars.py +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/py.typed +0 -0
- {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/schema_algebra.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typedframes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -42,7 +42,7 @@ Project-URL: Repository, https://github.com/w-martin/typedframes
|
|
|
42
42
|
|
|
43
43
|
> ⚠️ **Project Status: Proof of Concept**
|
|
44
44
|
>
|
|
45
|
-
> `typedframes` (v0.
|
|
45
|
+
> `typedframes` (v0.9.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
|
|
46
46
|
> integrations work, but expect rough edges. The codebase prioritizes demonstrating the viability of static DataFrame
|
|
47
47
|
> column checking over production-grade stability.
|
|
48
48
|
>
|
|
@@ -504,7 +504,7 @@ repos:
|
|
|
504
504
|
name: typedframes check
|
|
505
505
|
entry: typedframes check . --strict
|
|
506
506
|
language: python
|
|
507
|
-
additional_dependencies: ["typedframes==0.
|
|
507
|
+
additional_dependencies: ["typedframes==0.9.0"]
|
|
508
508
|
types_or: [python, jupyter]
|
|
509
509
|
pass_filenames: false
|
|
510
510
|
```
|
|
@@ -553,7 +553,7 @@ The action installs the PyPI wheel into a throwaway virtualenv and runs the chec
|
|
|
553
553
|
| Input | Default | |
|
|
554
554
|
|-------|---------|-|
|
|
555
555
|
| `path` | `.` | File or directory to check |
|
|
556
|
-
| `version` | `latest` | PyPI version to install, e.g. `"0.
|
|
556
|
+
| `version` | `latest` | PyPI version to install, e.g. `"0.9.0"` |
|
|
557
557
|
| `strict` | `true` | Fail the step on errors — `typedframes check` exits 0 without it |
|
|
558
558
|
| `coverage-fail-under` | *(unset)* | Minimum DataFrame schema coverage, e.g. `"90"` |
|
|
559
559
|
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown, `explain` to diagnose a lower-than-expected total |
|
|
@@ -667,17 +667,17 @@ plus the requested features, so both halves get checked normally. See
|
|
|
667
667
|
Fast feedback reduces development time. The typedframes Rust binary provides near-instant column checking.
|
|
668
668
|
|
|
669
669
|
**Benchmark results** (20 runs, 3 warmup, caches cleared between runs):
|
|
670
|
-
*2026-
|
|
670
|
+
*2026-09-30 · Darwin 27.0.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.20.0*
|
|
671
671
|
|
|
672
672
|
| Tool | Version | What it does | typedframes (13 files) | great_expectations (485 files) |
|
|
673
673
|
|------|---------|--------------|------------------------|--------------------------------|
|
|
674
|
-
| typedframes | 0.
|
|
675
|
-
| ruff | 0.16.3 | Linter (no type checking) |
|
|
676
|
-
| ty | 0.0.72 | Type checker |
|
|
677
|
-
| pyrefly | 1.2.0 | Type checker |
|
|
678
|
-
| mypy | 2.3.1 | Type checker (no plugin) |
|
|
679
|
-
| mypy + typedframes | 2.3.1 | Type checker + column checker |
|
|
680
|
-
| pyright | 1.1.411 | Type checker |
|
|
674
|
+
| typedframes | 0.9.0 | DataFrame column checker | 51ms ±613µs (IQR 997µs) | 268ms ±4ms (IQR 5ms) |
|
|
675
|
+
| ruff | 0.16.3 | Linter (no type checking) | 31ms ±735µs (IQR 882µs) | 237ms ±4ms (IQR 6ms) |
|
|
676
|
+
| ty | 0.0.72 | Type checker | 74ms ±2ms (IQR 3ms) | 782ms ±12ms (IQR 17ms) |
|
|
677
|
+
| pyrefly | 1.2.0 | Type checker | 98ms ±1ms (IQR 2ms) | 276ms ±8ms (IQR 14ms) |
|
|
678
|
+
| mypy | 2.3.1 | Type checker (no plugin) | 2.73s ±37ms (IQR 43ms) | 4.30s ±72ms (IQR 106ms) |
|
|
679
|
+
| mypy + typedframes | 2.3.1 | Type checker + column checker | 2.72s ±19ms (IQR 23ms) | 4.61s ±22ms (IQR 23ms) |
|
|
680
|
+
| pyright | 1.1.411 | Type checker | 826ms ±11ms (IQR 11ms) | 3.34s ±26ms (IQR 40ms) |
|
|
681
681
|
|
|
682
682
|
*Run `uv run python benchmarks/benchmark_checkers.py` to reproduce.*
|
|
683
683
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> ⚠️ **Project Status: Proof of Concept**
|
|
10
10
|
>
|
|
11
|
-
> `typedframes` (v0.
|
|
11
|
+
> `typedframes` (v0.9.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
|
|
12
12
|
> integrations work, but expect rough edges. The codebase prioritizes demonstrating the viability of static DataFrame
|
|
13
13
|
> column checking over production-grade stability.
|
|
14
14
|
>
|
|
@@ -470,7 +470,7 @@ repos:
|
|
|
470
470
|
name: typedframes check
|
|
471
471
|
entry: typedframes check . --strict
|
|
472
472
|
language: python
|
|
473
|
-
additional_dependencies: ["typedframes==0.
|
|
473
|
+
additional_dependencies: ["typedframes==0.9.0"]
|
|
474
474
|
types_or: [python, jupyter]
|
|
475
475
|
pass_filenames: false
|
|
476
476
|
```
|
|
@@ -519,7 +519,7 @@ The action installs the PyPI wheel into a throwaway virtualenv and runs the chec
|
|
|
519
519
|
| Input | Default | |
|
|
520
520
|
|-------|---------|-|
|
|
521
521
|
| `path` | `.` | File or directory to check |
|
|
522
|
-
| `version` | `latest` | PyPI version to install, e.g. `"0.
|
|
522
|
+
| `version` | `latest` | PyPI version to install, e.g. `"0.9.0"` |
|
|
523
523
|
| `strict` | `true` | Fail the step on errors — `typedframes check` exits 0 without it |
|
|
524
524
|
| `coverage-fail-under` | *(unset)* | Minimum DataFrame schema coverage, e.g. `"90"` |
|
|
525
525
|
| `coverage-detail` | `summary` | Or `term-missing` for the per-file breakdown, `explain` to diagnose a lower-than-expected total |
|
|
@@ -633,17 +633,17 @@ plus the requested features, so both halves get checked normally. See
|
|
|
633
633
|
Fast feedback reduces development time. The typedframes Rust binary provides near-instant column checking.
|
|
634
634
|
|
|
635
635
|
**Benchmark results** (20 runs, 3 warmup, caches cleared between runs):
|
|
636
|
-
*2026-
|
|
636
|
+
*2026-09-30 · Darwin 27.0.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.20.0*
|
|
637
637
|
|
|
638
638
|
| Tool | Version | What it does | typedframes (13 files) | great_expectations (485 files) |
|
|
639
639
|
|------|---------|--------------|------------------------|--------------------------------|
|
|
640
|
-
| typedframes | 0.
|
|
641
|
-
| ruff | 0.16.3 | Linter (no type checking) |
|
|
642
|
-
| ty | 0.0.72 | Type checker |
|
|
643
|
-
| pyrefly | 1.2.0 | Type checker |
|
|
644
|
-
| mypy | 2.3.1 | Type checker (no plugin) |
|
|
645
|
-
| mypy + typedframes | 2.3.1 | Type checker + column checker |
|
|
646
|
-
| pyright | 1.1.411 | Type checker |
|
|
640
|
+
| typedframes | 0.9.0 | DataFrame column checker | 51ms ±613µs (IQR 997µs) | 268ms ±4ms (IQR 5ms) |
|
|
641
|
+
| ruff | 0.16.3 | Linter (no type checking) | 31ms ±735µs (IQR 882µs) | 237ms ±4ms (IQR 6ms) |
|
|
642
|
+
| ty | 0.0.72 | Type checker | 74ms ±2ms (IQR 3ms) | 782ms ±12ms (IQR 17ms) |
|
|
643
|
+
| pyrefly | 1.2.0 | Type checker | 98ms ±1ms (IQR 2ms) | 276ms ±8ms (IQR 14ms) |
|
|
644
|
+
| mypy | 2.3.1 | Type checker (no plugin) | 2.73s ±37ms (IQR 43ms) | 4.30s ±72ms (IQR 106ms) |
|
|
645
|
+
| mypy + typedframes | 2.3.1 | Type checker + column checker | 2.72s ±19ms (IQR 23ms) | 4.61s ±22ms (IQR 23ms) |
|
|
646
|
+
| pyright | 1.1.411 | Type checker | 826ms ±11ms (IQR 11ms) | 3.34s ±26ms (IQR 40ms) |
|
|
647
647
|
|
|
648
648
|
*Run `uv run python benchmarks/benchmark_checkers.py` to reproduce.*
|
|
649
649
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typedframes"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Static analysis for pandas and polars DataFrames. Catch column errors at lint-time, not runtime."
|
|
9
9
|
keywords = ["pandas", "polars", "type-checking", "static-analysis", "dataframe", "linter", "mypy-plugin"]
|
|
10
10
|
readme = "README.md"
|
|
@@ -16,6 +16,44 @@ pub(crate) fn is_schema_base(name: &str) -> bool {
|
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// The bare name of a decorator expression: `staticmethod` for `@staticmethod`,
|
|
20
|
+
// `wraps` for both `@wraps(f)` and `@functools.wraps(f)`. `None` for anything else
|
|
21
|
+
// (an attribute chain deeper than one level, a subscript, a call with no simple
|
|
22
|
+
// name, ...) -- callers treat that the same as an unrecognized name.
|
|
23
|
+
pub(crate) fn decorator_name(expr: &Expr) -> Option<&str> {
|
|
24
|
+
match expr {
|
|
25
|
+
Expr::Name(name) => Some(name.id.as_str()),
|
|
26
|
+
Expr::Attribute(attr) => Some(attr.attr.as_str()),
|
|
27
|
+
Expr::Call(call) => decorator_name(&call.func),
|
|
28
|
+
_ => None,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Column names for `pd.DataFrame([{...}, {...}])` -- a literal list of per-row dict
|
|
33
|
+
// literals (pandas' "records" orientation). Every element must be a dict literal with
|
|
34
|
+
// only string-literal keys; the result is the union of every row's keys, in
|
|
35
|
+
// first-seen order (matching how pandas itself orders the resulting columns) -- a row
|
|
36
|
+
// missing a key another row has doesn't invalidate the inference, it's just NaN for
|
|
37
|
+
// that row.
|
|
38
|
+
pub(crate) fn extract_records_list_columns(list: &ast::ExprList) -> Option<Vec<String>> {
|
|
39
|
+
if list.elts.is_empty() {
|
|
40
|
+
return None;
|
|
41
|
+
}
|
|
42
|
+
let mut columns = Vec::new();
|
|
43
|
+
for elt in &list.elts {
|
|
44
|
+
let Expr::Dict(dict) = elt else {
|
|
45
|
+
return None;
|
|
46
|
+
};
|
|
47
|
+
for item in &dict.items {
|
|
48
|
+
let key = item.key.as_ref().and_then(|k| extract_string_literal(k))?;
|
|
49
|
+
if !columns.iter().any(|c: &String| c == key) {
|
|
50
|
+
columns.push(key.to_string());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
Some(columns)
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
pub(crate) fn extract_string_literal(expr: &Expr) -> Option<&str> {
|
|
20
58
|
if let Expr::StringLiteral(s) = expr {
|
|
21
59
|
Some(s.value.to_str())
|
|
@@ -244,6 +244,90 @@ pub(crate) const SQL_PRODUCING_METHODS: &[&str] = &["query", "sql"];
|
|
|
244
244
|
// checker's existing "empty string = none" sentinel convention for the same fields.
|
|
245
245
|
pub(crate) const OPEN_FRAME_MARKER: &str = "__typedframes_open_frame__";
|
|
246
246
|
|
|
247
|
+
// Methods that return a frame with the same columns as their receiver, beyond
|
|
248
|
+
// `ROW_PASSTHROUGH_METHODS` (which the assignment dispatch already propagates
|
|
249
|
+
// into a new binding). Consulted only by `frame_ops::classify_rhs`, to tell a
|
|
250
|
+
// reassignment that leaves a tracked frame's schema intact (`df = df.copy()`)
|
|
251
|
+
// apart from one that may change it. Deliberately conservative: anything not
|
|
252
|
+
// listed here is treated as possibly schema-changing.
|
|
253
|
+
pub(crate) const SCHEMA_PRESERVING_METHODS: &[&str] = &[
|
|
254
|
+
"copy",
|
|
255
|
+
"drop_duplicates",
|
|
256
|
+
"astype",
|
|
257
|
+
"replace",
|
|
258
|
+
"round",
|
|
259
|
+
"abs",
|
|
260
|
+
"clip",
|
|
261
|
+
"where",
|
|
262
|
+
"mask",
|
|
263
|
+
"sort_index",
|
|
264
|
+
"interpolate",
|
|
265
|
+
"shift",
|
|
266
|
+
"convert_dtypes",
|
|
267
|
+
"infer_objects",
|
|
268
|
+
"explode",
|
|
269
|
+
"isna",
|
|
270
|
+
"isnull",
|
|
271
|
+
"notna",
|
|
272
|
+
"notnull",
|
|
273
|
+
"clone",
|
|
274
|
+
"unique",
|
|
275
|
+
"drop_nulls",
|
|
276
|
+
"fill_null",
|
|
277
|
+
"fill_nan",
|
|
278
|
+
"slice",
|
|
279
|
+
"limit",
|
|
280
|
+
"reverse",
|
|
281
|
+
"lazy",
|
|
282
|
+
"collect",
|
|
283
|
+
"to_pandas",
|
|
284
|
+
"to_polars",
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
// Methods whose result is a Series, scalar, list, dict, or a serialisation --
|
|
288
|
+
// never a frame -- so a name reassigned from one is no longer the tracked frame.
|
|
289
|
+
pub(crate) const NON_FRAME_METHODS: &[&str] = &[
|
|
290
|
+
"sum",
|
|
291
|
+
"mean",
|
|
292
|
+
"median",
|
|
293
|
+
"std",
|
|
294
|
+
"var",
|
|
295
|
+
"min",
|
|
296
|
+
"max",
|
|
297
|
+
"count",
|
|
298
|
+
"nunique",
|
|
299
|
+
"idxmax",
|
|
300
|
+
"idxmin",
|
|
301
|
+
"any",
|
|
302
|
+
"all",
|
|
303
|
+
"prod",
|
|
304
|
+
"value_counts",
|
|
305
|
+
"to_dict",
|
|
306
|
+
"to_list",
|
|
307
|
+
"tolist",
|
|
308
|
+
"to_numpy",
|
|
309
|
+
"to_csv",
|
|
310
|
+
"to_parquet",
|
|
311
|
+
"to_json",
|
|
312
|
+
"to_string",
|
|
313
|
+
"to_html",
|
|
314
|
+
"to_markdown",
|
|
315
|
+
"to_latex",
|
|
316
|
+
"to_excel",
|
|
317
|
+
"to_sql",
|
|
318
|
+
"to_records",
|
|
319
|
+
"memory_usage",
|
|
320
|
+
"iterrows",
|
|
321
|
+
"itertuples",
|
|
322
|
+
"items",
|
|
323
|
+
"pop",
|
|
324
|
+
"get",
|
|
325
|
+
"squeeze",
|
|
326
|
+
"item",
|
|
327
|
+
"keys",
|
|
328
|
+
"insert",
|
|
329
|
+
];
|
|
330
|
+
|
|
247
331
|
pub(crate) const ROW_PASSTHROUGH_METHODS: &[&str] = &[
|
|
248
332
|
"filter",
|
|
249
333
|
"query",
|
|
@@ -72,6 +72,8 @@ pub struct FileStats {
|
|
|
72
72
|
/// against `typed_sites`/`untyped_sites` by line is what tells a reader whether a
|
|
73
73
|
/// given call was counted at all, not just whether it was typed.
|
|
74
74
|
pub all_dataframe_calls: Vec<DataFrameCallSite>,
|
|
75
|
+
/// Every place a tracked frame stopped carrying its column set -- see [`LegEvent`].
|
|
76
|
+
pub leg_events: Vec<LegEvent>,
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/// One DataFrame origin the linter recognized but could not resolve columns for.
|
|
@@ -110,6 +112,23 @@ pub struct TypedSite {
|
|
|
110
112
|
pub schema: String,
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/// A statement after which a tracked DataFrame no longer has a column set the checker
|
|
116
|
+
/// can vouch for. Powers `--coverage-detail=explain`'s "tracking ended" listing, so a
|
|
117
|
+
/// run can say exactly which line lost a frame and why.
|
|
118
|
+
///
|
|
119
|
+
/// `outcome` is `"unresolved"` (the frame is still a DataFrame, but its columns are
|
|
120
|
+
/// unknown from here) or `"untracked"` (the name no longer holds that frame, or the
|
|
121
|
+
/// checker can make no claim about it). Line and column are 1-indexed, matching
|
|
122
|
+
/// [`LintError`].
|
|
123
|
+
#[derive(Debug, Serialize)]
|
|
124
|
+
pub struct LegEvent {
|
|
125
|
+
pub line: usize,
|
|
126
|
+
pub col: usize,
|
|
127
|
+
pub var: String,
|
|
128
|
+
pub outcome: String,
|
|
129
|
+
pub reason: String,
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
/// One `<load module>.<load function>(...)`-shaped call site found anywhere in a
|
|
114
133
|
/// file by [`crate::linter::LoadCallSiteCollector`]'s unconditional scan -- see
|
|
115
134
|
/// [`FileStats::all_dataframe_calls`]. Line and column are 1-indexed and point at
|