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.
Files changed (38) hide show
  1. {typedframes-0.7.0 → typedframes-0.9.0}/PKG-INFO +12 -12
  2. {typedframes-0.7.0 → typedframes-0.9.0}/README.md +11 -11
  3. {typedframes-0.7.0 → typedframes-0.9.0}/pyproject.toml +1 -1
  4. {typedframes-0.7.0 → typedframes-0.9.0}/rust/Cargo.lock +1 -1
  5. {typedframes-0.7.0 → typedframes-0.9.0}/rust/Cargo.toml +1 -1
  6. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/ast_extract.rs +38 -0
  7. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/constants.rs +84 -0
  8. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/errors.rs +19 -0
  9. typedframes-0.9.0/rust/src/frame_ops.rs +1335 -0
  10. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/index.rs +418 -0
  11. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/lib.rs +1 -0
  12. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/linter.rs +3982 -558
  13. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/notebook.rs +40 -1
  14. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/pyapi.rs +2 -0
  15. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/__init__.py +1 -1
  16. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/cli.py +144 -22
  17. {typedframes-0.7.0 → typedframes-0.9.0}/rust/.cargo/config.toml +0 -0
  18. {typedframes-0.7.0 → typedframes-0.9.0}/rust/README.md +0 -0
  19. {typedframes-0.7.0 → typedframes-0.9.0}/rust/benches/parser_bench.rs +0 -0
  20. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/config.rs +0 -0
  21. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/contract.rs +0 -0
  22. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/main.rs +0 -0
  23. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/sql.rs +0 -0
  24. {typedframes-0.7.0 → typedframes-0.9.0}/rust/src/typo.rs +0 -0
  25. {typedframes-0.7.0 → typedframes-0.9.0}/rust/tests/integration_test.rs +0 -0
  26. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/_rust_checker.pyi +0 -0
  27. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/base_schema.py +0 -0
  28. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column.py +0 -0
  29. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_group.py +0 -0
  30. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_group_error.py +0 -0
  31. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/column_set.py +0 -0
  32. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/missing_dependency_error.py +0 -0
  33. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/mypy.py +0 -0
  34. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/pandas.py +0 -0
  35. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/pandera.py +0 -0
  36. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/polars.py +0 -0
  37. {typedframes-0.7.0 → typedframes-0.9.0}/src/typedframes/py.typed +0 -0
  38. {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.7.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.5.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
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.5.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.5.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-08-18 · Darwin 25.6.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.20.0*
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.4.1 | DataFrame column checker | 51ms ±918µs (IQR 1ms) | 219ms ±2ms (IQR 4ms) |
675
- | ruff | 0.16.3 | Linter (no type checking) | 30ms ±764µs (IQR 922µs) | 233ms ±3ms (IQR 4ms) |
676
- | ty | 0.0.72 | Type checker | 73ms ±1ms (IQR 2ms) | 810ms ±10ms (IQR 13ms) |
677
- | pyrefly | 1.2.0 | Type checker | 104ms ±2ms (IQR 2ms) | 276ms ±9ms (IQR 14ms) |
678
- | mypy | 2.3.1 | Type checker (no plugin) | 3.07s ±17ms (IQR 25ms) | 4.56s ±29ms (IQR 57ms) |
679
- | mypy + typedframes | 2.3.1 | Type checker + column checker | 3.07s ±13ms (IQR 17ms) | 4.84s ±20ms (IQR 23ms) |
680
- | pyright | 1.1.411 | Type checker | 781ms ±5ms (IQR 6ms) | 3.46s ±25ms (IQR 41ms) |
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.5.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
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.5.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.5.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-08-18 · Darwin 25.6.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.20.0*
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.4.1 | DataFrame column checker | 51ms ±918µs (IQR 1ms) | 219ms ±2ms (IQR 4ms) |
641
- | ruff | 0.16.3 | Linter (no type checking) | 30ms ±764µs (IQR 922µs) | 233ms ±3ms (IQR 4ms) |
642
- | ty | 0.0.72 | Type checker | 73ms ±1ms (IQR 2ms) | 810ms ±10ms (IQR 13ms) |
643
- | pyrefly | 1.2.0 | Type checker | 104ms ±2ms (IQR 2ms) | 276ms ±9ms (IQR 14ms) |
644
- | mypy | 2.3.1 | Type checker (no plugin) | 3.07s ±17ms (IQR 25ms) | 4.56s ±29ms (IQR 57ms) |
645
- | mypy + typedframes | 2.3.1 | Type checker + column checker | 3.07s ±13ms (IQR 17ms) | 4.84s ±20ms (IQR 23ms) |
646
- | pyright | 1.1.411 | Type checker | 781ms ±5ms (IQR 6ms) | 3.46s ±25ms (IQR 41ms) |
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.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"
@@ -1347,7 +1347,7 @@ checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
1347
1347
 
1348
1348
  [[package]]
1349
1349
  name = "typedframes_checker"
1350
- version = "0.7.0"
1350
+ version = "0.9.0"
1351
1351
  dependencies = [
1352
1352
  "anyhow",
1353
1353
  "criterion",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "typedframes_checker"
3
- version = "0.7.0"
3
+ version = "0.9.0"
4
4
  edition = "2021"
5
5
  description = "A high-performance Rust-based checker for typedframes DataFrame column schemas"
6
6
  license = "MIT"
@@ -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