typing: give init_sd's display config keys a type, type-check the styling files - #980
Merged
Merged
Conversation
…ck the styling files styling_typecheck_cases.py is checked by basedpyright, not pytest. Each line with a `# pyright: ignore` is a mistake the types have to reject; with reportUnnecessaryTypeIgnoreComment on, an ignore that suppresses nothing is an error. Today init_sd has no type, so every one of them is accepted, and the good cases fail because merge_rule-only overrides, the inherit and duration displayers and the string displayer's highlight_* keys aren't in the Python types. styling_core.py, customizations/styling.py and styling_helpers.py join pyrightconfig.typecheck.json. test_index_styling_non_str_level_names: an int-named single-level index or columns level reaches col_path as an int, but col_path is string[] on the JS side and the MultiIndex branches already str() their level names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
📦 TestPyPI package publishedpip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.6.dev36153886336or with uv: uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.6.dev36153886336MCP server for Claude Codeclaude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.6.dev36153886336" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table📖 Docs preview🎨 Storybook preview |
…ling files InitColMeta types the keys styling reads out of an init_sd entry (displayer_args, ag_grid_specs, delete_keys, highlight_*, merge_rule, column_config_override); every other key is a summary stat typed like ColMeta's values, via PEP 728 extra_items. delete_keys is a List because a bare str is an Iterable[str]. init_sd is annotated InitSD on the widgets, CustomizableDataflow and the two server create_* functions. PartialColConfig becomes a closed TypedDict and is what column_config_overrides now takes, so a merge_rule-only override (compare, extension_utils, pandera) type-checks. Both PEP 728 types are defined under TYPE_CHECKING: typing_extensions raises on closed / extra_items before 4.13 and Pyodide 0.27 ships 4.12, so at runtime they're plain dicts. ag_grid_specs stays Dict[str, Any] under an AGGridColDef alias: it's handed to AG-Grid as a ColDef, a large third-party interface that's mostly callbacks Python can't send. The Python displayer types pick up what the JS side already has: the duration and inherit displayers and the string displayer's highlight_* keys. styling_core.py, customizations/styling.py and styling_helpers.py now type-check clean. get_left_col_configs str()s a single-level index or columns name like the MultiIndex branches already did, and builds the last index column's col_path before appending it instead of patching it afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t for an unnamed column level's header
Three ddd frames built the way real code gets int column levels:
pivot_table with a values list (('revenue', 2023), ...), unstack onto
year/quarter, and a pivot of a headerless CSV (int level names too).
The pivot_table frame leaves the values level unnamed next to a named
'year' level. get_index_level_names str()s every name once any is set,
so the index column's top header renders the text "None".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These pass already. Left col_path for int level values and int level names, data columns keeping the frame's own int-containing labels as col_path, and column_config_overrides keyed by those labels (a hidden merge_rule and a color_map_config). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_index_level_names str()'d every level name once any level was named, so pivot_table(values=[...], columns='year') put the text "None" in the index column's top header. An unnamed level now gets '', as it already did when no level is named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…med ones get_multiindex_partly_named_index_df is pd.concat of a dict of year pivots: the dict keys make an unnamed outer index level above 'region', and the columns axis is named 'year'. With a named columns axis every index column takes the col_path branch, which str()s the None level name, so the first index column's header reads "None". The same happens under MultiIndex columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d ones With a named columns axis, get_left_col_configs gives every index column a col_path and appended str(idx_name), so the unnamed outer level pd.concat makes from a dict's keys got the header "None". It's '' now, matching the column-level fix in get_index_level_names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #978.
Display config reached styling through
init_sd, typed as the stats bagColMeta. ItsSDValsvalue type accepts a str forag_grid_specs, and a str fordelete_keysbecause a str is anIterable[str]. None of the styling files were type-checked in CI.Types
InitColMeta(styling_core.py) types the keys styling reads from one column'sinit_sdentry:displayer_args,ag_grid_specs,delete_keys,highlight_phrase/highlight_regex/highlight_color,merge_ruleandcolumn_config_override, allNotRequired. Any other key is a summary stat, typed likeColMeta's values through PEP 728extra_items=SDVals, soColMetaitself is unchanged.InitSD = Dict[ColIdentifier, InitColMeta].delete_keysisList[str].SequenceorIterablewould accept a bare str.init_sd'sdisplayer_argsis shallow-merged over the computed one, so it's typedDisplayerArgsOverride, which has every displayer key and all of them optional.PartialColConfigis now a closed TypedDict (any subset of a column config) and is the value type ofOverrideColumnConfig.column_config_overridesused to require a fullBaseColumnConfig, so the{'merge_rule': 'hidden'}overrides incompare.py,extension_utils.pyand the pandera integration didn't type-check.init_sdis annotated onBuckarooWidgetBase,BuckarooInfiniteWidget,DFViewerInfinite,PolarsDFViewerInfinite,CustomizableDataflow,create_dataflowandcreate_polars_dataflow.DFWhole.tsalready had:DurationDisplayerA,InheritDisplayerA, andhighlight_*onStringDisplayerA.ag_grid_specsstaysDict[str, Any], now namedAGGridColDef. It's handed to AG-Grid as aColDef. MirroringColDefin Python would mean copying a large third-party interface, most of which is callbacks Python can't send. The str-for-a-dict mistake from the issue is already caught byDict.PEP 728 at runtime. typing_extensions before 4.13 raises on
closedandextra_items(4.12.2:TypeError: TypedDict takes either a dict or keyword arguments, but not both), and marimo's WASM export runs Pyodide 0.27.5, which ships 4.12.2. SoPartialColConfigandInitColMetaare defined underTYPE_CHECKINGand areDict[str, Any]at runtime. basedpyright 1.39.8 handles both.Type-checking the styling files
styling_core.py,customizations/styling.pyandstyling_helpers.pyare added topyrightconfig.typecheck.json. They had 39 errors on main and have 0 now.reportUnnecessaryTypeIgnoreCommentis on for the whole scope and flagged nothing in the files that were already there.tests/unit/dataflow/styling_typecheck_cases.pyholds the type-level cases. basedpyright checks it, pytest doesn't collect it. Each line with a# pyright: ignoreis a mistake the types have to reject, and an ignore that no longer suppresses anything is an error.There are three
casts. Two are instyle_columns, whereorig_col_nameandcolumn_config_overridecome out of the untyped merged sd. The third is infix_column_config, which turns aBaseColumnConfiginto aColumnConfigby swapping identity keys.Behaviour changes
get_left_col_configsnowstr()s a single-level index or columns name, as the MultiIndex branches already did. An int-named index used to reachcol_pathas an int ([3, 7]), while JS typescol_pathasstring[]. The function also builds the last index column'scol_pathbefore appending it instead of patchingccs[-1]afterwards. The output is the same, and the existing index-styling tests cover it.pivot_table(index='region', columns='year', values=['revenue', 'units'])does this: its values level is unnamed and itsyearlevel is named.get_index_level_namesstr()'d every level name once any was set, so the index column's top header read "None". It's now blank, as it already was when no level is named. I checked this in a rendered static embed, before and after.pd.concat({'actual': df1, 'budget': df2})gives an unnamed outer index level above a named one. When the columns axis is named, every index column gets acol_path, and the unnamed level's header read "None". It's blank now. I checked this in a rendered static embed as well.Int-labelled MultiIndex frames
The ddd gets three frames built the way real code gets int column levels:
get_multiindex_int_cols_df:pivot_tablewith a values list, giving('revenue', 2023), ...get_multiindex_int_levels_df:unstackonto year/quarter, giving(2023, 1), ...get_multiindex_int_names_df: a pivot of a headerless CSV, which has int level names as well as int values.get_multiindex_partly_named_index_df:pd.concatof a dict of year pivots, giving an unnamed outer row level aboveregionand a columns axis namedyear.The tests cover their left
col_paths, the data columns'col_path, andcolumn_config_overrideskeyed by int-containing tuples.A data column's
col_pathis still the frame's own label, ints included. That's whatmerge_column_configlooks overrides up by, and in the rendered page an int header such as 2023 displays fine. So thestr()above only applies to level names on the index side.Not covered here
For any row MultiIndex, the pinned summary rows show
Nonein the index cells instead of the stat names (dtype,mean, ...), whether or not the levels are named. That's a separate, pre-existing bug and isn't touched here.Widget and dataflow constructor calls still aren't argument-checked. They're traitlets
HasTraitsclasses, andHasDescriptors.__new__returnsAny, so pyright never evaluates__init__for aBuckarooWidget(...)call. Editors show the new annotations, and the constructor body is typed. A caller's badinit_sdis only flagged when it goes throughcreate_dataflow/create_polars_dataflow. ATYPE_CHECKING-only__new__ -> Selfon the base classes would fix that. It would also start checkingpinned_rows, which is annotatedPinnedRowConfigalthough callers pass a list.style_columnstill receives the merged sd asColMeta.InitColMetatypesinit_sdwhere it enters, not where styling reads it.The typecheck job is still non-blocking. The scoped files are at 0 errors, so it could be made blocking.
Runtime validation of these keys is the fast follow from the issue.
Test
test_index_styling_non_str_level_names. On CI, every Python test job failed on that test and nothing else (3.13: 1 failed, 1163 passed). The typecheck job is non-blocking and stays green, but its log showed 51 errors.pytest tests/unitgives 1275 passed and 6 skipped, ruff and paddy_format are clean, andbuckaroo.dataflow.styling_coreimports under typing_extensions 4.12.2.test_index_styling_int_cols_unnamed_level, which fails on the "None" header. On CI, every Python test job failed on that test and nothing else (3.13: 1 failed, 1168 passed).pytest tests/unitgives 1280 passed and 6 skipped, and every CI check passes.pytest tests/unitgives 1282 passed and 6 skipped, and every CI check passes.🤖 Generated with Claude Code