Skip to content

Commit 4aa10ea

Browse files
yilibinbinAaronhaohaoclaude
authored
refactor(shared): merge expression_names into expression_registry (drop over-split) (#83)
An over-split audit of the whole package (docs/OVER_SPLIT_AUDIT_2026.md) found the codebase is healthy — exactly ONE genuine over-split. shared/expression_names.py was a 12-line behavior-free re-export: reserved_expression_names() just forwarded to expression_registry, and is_reserved_expression_name() was byte-identical to the registry's own. Both already live in shared/expression_registry.py. It was not a layer boundary (same package), not an import firewall (expression_registry is itself stdlib-only and guarded lightweight), and not a compatibility contract (its only test asserted value-equality with the registry, i.e. confirmed its own redundancy). Merging removes one import hop with zero target-file growth. - Delete shared/expression_names.py. - Repoint the two production importers (shared/input_normalization.py, shared/computation_inputs.py) to shared.expression_registry. - Update test_expression_registry.py: drop the deleted module from the import-lightness guard and the redundant value-equality assertion; the registry-level assertions remain. Also archives the over-split audit report. ruff clean; 83 registry/consumer/ constants-editor tests pass; the registry lightness guard still holds. Co-authored-by: fanghao <fanghaoaa@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7f5e037 commit 4aa10ea

5 files changed

Lines changed: 62 additions & 18 deletions

File tree

docs/OVER_SPLIT_AUDIT_2026.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Over-Split Audit — DataLab
2+
3+
## 1. Executive summary
4+
5+
**The codebase is healthy. It is not over-split.** Across 15 candidate splits that survived initial triage, adversarial verification confirmed exactly **one** genuine over-split worth consolidating — a 12-line pure re-export module (`shared/expression_names.py`). Every other small module examined turned out to be a legitimate layering boundary, a single-source-of-truth for drifting constants, a localization facade, or a compatibility contract with tests pinning it in place.
6+
7+
This is the profile of a codebase that splits deliberately, not reflexively. The maintainer's principle — "split for value/maintainability, not for the sake of splitting" — is broadly being honored. The one MERGE below is a rare miss (a light wrapper stacked on an already-light module), not a systemic pattern. **Recommendation: merge the single item, leave everything else.**
8+
9+
One item in the KEEP list (`latex_escape` wrapper) is flagged as a *soft* consolidation opportunity — real but lower-value indirection inside otherwise-legitimate files. It is not a file-level over-split, so it is noted, not ranked as a MERGE.
10+
11+
---
12+
13+
## 2. Consolidate these (ranked)
14+
15+
### #1`shared/expression_names.py` → fold into `shared/expression_registry.py` · effort: **S**
16+
17+
| | |
18+
|---|---|
19+
| **Files** | `shared/expression_names.py` (delete) |
20+
| **Merge target** | `shared/expression_registry.py` (already contains both functions, lines 60–67) |
21+
| **Callers to repoint** | `shared/computation_inputs.py`, `shared/input_normalization.py` (one import line each) |
22+
| **Test to update** | `tests/test_expression_registry.py` (one import line) |
23+
24+
**What to merge.** `expression_names.py` re-exports `reserved_expression_names()` and `is_reserved_expression_name()` from `expression_registry.py`. The re-export is behavior-free: `reserved_expression_names()` just forwards, and `is_reserved_expression_name()` is byte-identical to the registry's own implementation. Both functions **already live** in `expression_registry.py`, so merging grows the target by **zero lines** — callers simply repoint imports.
25+
26+
**Why it adds value.** This is pure indirection with no compensating benefit:
27+
- **Not a layer boundary** — both files sit in `shared/` at the same layer.
28+
- **Not an import firewall** — the usual defense for a thin shim. It fails here: `expression_registry.py` is *itself* import-light (only `re` + `typing`), and the existing `test_..._import_stay_lightweight` guard already protects it against PySide6/mpmath/sympy/etc. Callers importing directly from the registry get the identical lightness guarantee. The wrapper shields against nothing.
29+
- **Not a compatibility contract** — the sole test consumer asserts the two sources return *identical* values, i.e. it exists to confirm redundancy, not to pin an external API. A full grep found no other importers, no `__init__` re-export, no dynamic/`importlib` references.
30+
31+
The one nuance flagged during verification: design docs name this file as an intended future "unit-validation facade." That is forward-looking intent, not present value — today the module has zero behavior and zero external contract. If a real validation seam is needed later, it should be a *new* module with actual logic, not a behavior-free re-export kept alive on spec.
32+
33+
Both independent verifiers returned **CONFIRMED / MERGE**. Safe, mechanical, strictly removes one import hop.
34+
35+
---
36+
37+
## 3. These splits are GOOD — do not touch
38+
39+
Each of these looked small enough to question but earned its place. Leave them.
40+
41+
| Module | Why it's legitimate |
42+
|---|---|
43+
| `app_desktop/bridge_qt.py` | **Bridge pattern.** Adapts `datalab_core` `SessionCallbacks` to Qt signals so the core never takes a PySide6 dependency. Small size = focused responsibility; dedicated test suite confirms the boundary. |
44+
| `app_desktop/result_csv_spec.py` | **Single source of truth.** Keeps CSV headers/filenames from drifting across first-render mixins vs. `_refresh_display_format`. A test asserts both paths read the same spec — removing it re-invites hardcoding drift. Data-heavy, not fragmented. |
45+
| `app_desktop/result_view_titles.py` | **Localization facade.** Maps view keys → bilingual strings, deferring to shared UI specs on miss. Small because the concept is small. |
46+
| `app_desktop/workbench_model_bindings.py` | **State-path registry.** Centralizes `STATE_ROLE_MODEL_PATHS` + path construction; inlining pushes these strings back into the already-large `panels.py`. Declarative, cohesive. |
47+
| `app_desktop/ui_schema_runtime.py` | **Mid-level binding layer.** Adds i18n lifecycle (`owner._register_text`) on top of `bind_field`; 8 view/widget builders depend on it, avoiding duplicated language-routing logic. |
48+
| `app_desktop/widget_hints.py` | **Borderline but keep.** A one-liner wrapping `setAccessibleDescription` with a `getattr` guard, shared by 4 callers across modules. Marginal value, but consolidating buys almost nothing and risks scattering the guard pattern. Not worth the churn. |
49+
| `datalab_latex/latex_tables_*` (fitting / statistics_grouped / statistics_matrix / budget) | **Files are correctly split by table domain — keep the files.** *However*, the `latex_escape()` **wrapper** inside `latex_tables_fitting.py` (lines 70–73) is a thin delegator to `shared.latex_escaping`, and three sibling modules import it from there instead of from `shared`. This is a soft, in-file cleanup (repoint 4 imports to `shared.latex_escaping`, drop the wrapper) — **not** a file-level over-split. Optional, low priority. |
50+
51+
---
52+
53+
## 4. Left as-is (harmless — not worth the churn)
54+
55+
Three trivially-small modules were examined and deliberately **left alone**: `app_desktop/current_page_stack.py` (a `QStackedWidget` subclass overriding two layout-hint methods), `app_desktop/root_latex_writer.py` (path/file-I/O wrapper around the LaTeX doc builder), and `app_desktop/shell_layout.py` (status-bar update helpers over `workbench_toolbar`). Each is small because its job is small; consolidating them would add no clarity and only generate diff noise.
56+
57+
---
58+
59+
**Bottom line:** 1 merge (S effort, mechanical), 1 optional in-file import cleanup, everything else stays. The codebase splits for value, not for the sake of it.

shared/computation_inputs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mpmath import mp
1010

1111
from shared.bilingual import _dual_msg
12-
from shared.expression_names import is_reserved_expression_name
12+
from shared.expression_registry import is_reserved_expression_name
1313
from shared.input_normalization import ConstantsState
1414
from shared.symbolic_math import normalize_symbolic_expression
1515
from shared.uncertainty import UncertainValue, parse_numeric_value, parse_uncertainty_format

shared/expression_names.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

shared/input_normalization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Mapping
88

99
from shared.bilingual import _dual_msg
10-
from shared.expression_names import is_reserved_expression_name
10+
from shared.expression_registry import is_reserved_expression_name
1111
from shared.uncertainty import parse_numeric_value, parse_uncertainty_format
1212

1313

tests/test_expression_registry.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
import sys
55

66

7-
def test_expression_registry_and_names_import_stay_lightweight() -> None:
7+
def test_expression_registry_import_stays_lightweight() -> None:
88
script = r"""
99
import sys
1010
1111
import shared.expression_registry
12-
import shared.expression_names
1312
1413
forbidden_prefixes = (
1514
"PySide6",
@@ -44,7 +43,6 @@ def test_expression_registry_names_match_all_consumers() -> None:
4443
from datalab_latex.expression_engine import list_allowed_functions as latex_allowed
4544
from fitting import model_parser
4645
from fitting import symbolic_export
47-
from shared import expression_names
4846
from shared.expression_engine import list_allowed_functions as shared_allowed
4947
from shared.expression_registry import (
5048
allowed_constant_names,
@@ -63,7 +61,6 @@ def test_expression_registry_names_match_all_consumers() -> None:
6361
assert set(shared_allowed()["constants"]) == set(constants)
6462
assert set(latex_allowed()["constants"]) == set(constants)
6563
assert shared_allowed() == latex_allowed()
66-
assert expression_names.reserved_expression_names() == expected_reserved
6764
assert model_parser.reserved_expression_names() == expected_reserved
6865
assert set(symbolic_export.SYMPY_FUNCTION_MAP) == set(functions)
6966
assert set(symbolic_export.MATHEMATICA_FUNCTION_MAP) == set(functions)

0 commit comments

Comments
 (0)