Skip to content

Retain model import caches#3600

Merged
koxudaxi merged 2 commits into
mainfrom
agent/retain-import-caches
Jul 11, 2026
Merged

Retain model import caches#3600
koxudaxi merged 2 commits into
mainfrom
agent/retain-import-caches

Conversation

@koxudaxi

@koxudaxi koxudaxi commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Performance

    • Retain reusable import caches for built-in model generation to reduce redundant work.
    • Restrict cache clearing to cases where it could affect customized or changed types.
  • Bug Fixes

    • Improved correctness for aliasing, generics, discriminator handling, defaults, and cross-module references.
    • Ensured external cache hooks are preserved across module boundaries and rechecked when inherited enum types replace built-in wrappers.
  • Tests

    • Added integration coverage for cache retention vs invalidation, alias/generic scenarios, cross-module hook behavior, and inherited enum rechecking.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4db3d0d-7787-4f3c-9d51-06b866fae163

📥 Commits

Reviewing files that changed from the base of the PR and between 15bb061 and 53e7434.

⛔ Files ignored due to path filters (2)
  • tests/data/expected/main/builtin_import_cache_unique_items_unhashable.txt is excluded by !tests/data/**/*.txt and included by none
  • tests/data/jsonschema/unique_items_unhashable_default.json is excluded by !tests/data/**/*.json and included by none
📒 Files selected for processing (4)
  • src/datamodel_code_generator/parser/base.py
  • tests/data/expected/main/builtin_import_cache_unique_items_unhashable.py
  • tests/data/expected/main/custom_import_cache_unique_items.py
  • tests/main/test_main_general.py
✅ Files skipped from review due to trivial changes (1)
  • tests/data/expected/main/custom_import_cache_unique_items.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/main/test_main_general.py
  • src/datamodel_code_generator/parser/base.py

📝 Walkthrough

Walkthrough

The parser now distinguishes built-in and custom generation types, retaining import caches for built-in-only processing while conditionally invalidating caches after custom mutations. New fixtures and integration tests cover aliases, defaults, cross-module models, generic bases, and inherited enums.

Changes

Import cache lifecycle

Layer / File(s) Summary
Cache retention policy
src/datamodel_code_generator/parser/base.py
Adds built-in generation-type detection, conditional cache-clearing helpers, and change tracking for aliased base-class imports.
Mutation cache handling
src/datamodel_code_generator/parser/base.py
Threads can_retain_cache through discriminator, default, field-name, type-alias, shadowed-import, and generic-base mutations.
Module processing integration
src/datamodel_code_generator/parser/base.py
Computes cache-retention state per module and applies it during post-processing and finalization.
Cache behavior fixtures and tests
tests/main/test_main_general.py, tests/data/expected/main/*
Adds integration tests and expected outputs for built-in retention, custom invalidation, cross-module models, generic bases, unhashable defaults, and inherited enums.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Parser
  participant _process_single_module
  participant DataModel
  Parser->>_process_single_module: determine cache retention for module
  _process_single_module->>DataModel: apply model post-processing
  _process_single_module->>DataModel: retain or clear import cache
Loading

Possibly related PRs

Suggested labels: breaking-change-analyzed

Suggested reviewers: gaborbernat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly matches the PR’s main goal of retaining model import caches.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/retain-import-caches

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

📚 Docs Preview: https://pr-3600.datamodel-code-generator.pages.dev

@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 25.45%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 13 regressed benchmarks
⏩ 98 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime test_perf_large_models_pydantic_v2 2.6 s 3.7 s -30.43%
WallTime test_perf_complex_refs 1.4 s 2 s -28.02%
WallTime test_perf_deep_nested 3.7 s 5.2 s -27.84%
WallTime test_perf_duplicate_names 757.7 ms 1,037.3 ms -26.96%
WallTime test_perf_all_options_enabled 3.9 s 5.3 s -26.93%
WallTime test_perf_multiple_files_input 2.5 s 3.4 s -26.19%
WallTime test_perf_kubernetes_style_pydantic_v2 1.9 s 2.6 s -25.42%
WallTime test_perf_graphql_style_pydantic_v2 576.8 ms 765.6 ms -24.67%
WallTime test_perf_openapi_large 2.2 s 2.9 s -24.42%
WallTime test_perf_stripe_style_pydantic_v2 1.5 s 1.9 s -23.57%
WallTime test_perf_aws_style_openapi_pydantic_v2 1.4 s 1.9 s -23.13%
WallTime test_perf_large_models_pydantic_v2_builtin 661.1 ms 848.5 ms -22.09%
WallTime test_perf_large_models_pydantic_v2_noformat 634.9 ms 799.3 ms -20.56%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing agent/retain-import-caches (53e7434) with main (01303c3)

Open in CodSpeed

Footnotes

  1. 98 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (01303c3) to head (53e7434).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main     #3600    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          164       164            
  Lines        35325     35456   +131     
  Branches      4078      4091    +13     
==========================================
+ Hits         35325     35456   +131     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/datamodel_code_generator/parser/base.py (2)

3843-3883: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reorder the final validate_default pass before capturing model_imports.
src/datamodel_code_generator/parser/base.py:3847-3883 — the last __set_validate_default_on_fields(...) runs after model_imports is snapshotted, so any field that gains validate_default=True here can still miss the Field(...) import in the emitted module. Move that pass earlier, or rebuild the import merge afterward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/datamodel_code_generator/parser/base.py` around lines 3843 - 3883, Move
the final __set_validate_default_on_fields pass before model_imports is
constructed in the module-finalization flow, preserving its existing
can_retain_cache argument; then snapshot model_imports and perform import
merging and cleanup afterward so newly required Field imports are included.
Remove the now-duplicated later validation loop.

2236-2260: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the retained imports cache after converting unique lists to sets. Duplicate-name handling and reuse_model/collapse_reuse_models both touch model.imports/get_dedup_key() earlier in _build_module_structure, so can_retain_cache=True can carry a stale imports tuple into __change_from_import().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/datamodel_code_generator/parser/base.py` around lines 2236 - 2260, Ensure
__replace_unique_list_to_set always invalidates the model imports cache after
changing a field type, including when can_retain_cache=True. Update the
changed/cache-clearing logic in __replace_unique_list_to_set so stale imports
and get_dedup_key() data cannot reach __change_from_import().
🧹 Nitpick comments (2)
src/datamodel_code_generator/parser/base.py (1)

3179-3198: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Asymmetric cache clearing between the retain/no-retain branches.

In the can_retain_cache=True branch, the model's cache is only cleared when original_import was actually found and removed from _additional_imports, mirroring the mutation. In the else (can_retain_cache=False) branch, _additional_imports is unconditionally reassigned (a no-op new list when nothing changes) but model.clear_imports_cache() is never called at all.

In practice this is very likely a no-op: the same can_retain_cache condition here is generally consistent with the value computed for the same models list inside _process_single_module, whose trailing if not can_retain_cache: _clear_model_imports_cache(models) (line 3831-3832) would already have cleared these caches before __apply_generic_base_class runs. Still, relying on that indirect coupling is fragile — a future refactor to either check could silently reintroduce staleness. Consider making the else branch explicit for defense-in-depth.

♻️ Suggested defensive fix
                     else:
-                        model._additional_imports = [
-                            i for i in additional_imports if i != original_import
-                        ]
+                        if original_import in additional_imports:
+                            model._additional_imports = [
+                                i for i in additional_imports if i != original_import
+                            ]
+                            model.clear_imports_cache()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/datamodel_code_generator/parser/base.py` around lines 3179 - 3198, Ensure
the can_retain_cache=False branch in the module/model processing loop explicitly
clears each model’s imports cache after updating _additional_imports, matching
the cache invalidation behavior of the can_retain_cache=True branch. Add
model.clear_imports_cache() within that else path, using the surrounding model
iteration and _can_retain_model_imports_cache logic as the change location.
tests/main/test_main_general.py (1)

97-198: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Consider adding coverage for the reuse_model/collapse_reuse_models + mutation-ordering interplay.

The new tests thoroughly cover the builtin-retention and cross-module/enum-hook scenarios, but none exercise reuse_model=True + collapse_reuse_models=True (or duplicate class names) combined with mutations that happen in __replace_unique_list_to_set / __override_required_field before .imports is first read in _process_single_module. Given DataModel.get_dedup_key() reads .imports earlier in _build_module_structure, this combination is the scenario most likely to expose the cache-retention gaps flagged in base.py. Adding a manifest-style test similar to the existing ones for this combination would help lock in correct behavior.

Also applies to: 200-257, 259-311

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/main/test_main_general.py` around lines 97 - 198, Extend
test_parser_retains_builtin_import_cache_and_invalidates_custom_cache with a
manifest-style scenario using reuse_model=True and collapse_reuse_models=True,
including duplicate class names and mutations from __replace_unique_list_to_set
and __override_required_field before _process_single_module first reads imports.
Assert the resulting generated output and cache-retention/invalidation manifest,
covering the earlier .imports access through DataModel.get_dedup_key() in
_build_module_structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/datamodel_code_generator/parser/base.py`:
- Around line 3843-3883: Move the final __set_validate_default_on_fields pass
before model_imports is constructed in the module-finalization flow, preserving
its existing can_retain_cache argument; then snapshot model_imports and perform
import merging and cleanup afterward so newly required Field imports are
included. Remove the now-duplicated later validation loop.
- Around line 2236-2260: Ensure __replace_unique_list_to_set always invalidates
the model imports cache after changing a field type, including when
can_retain_cache=True. Update the changed/cache-clearing logic in
__replace_unique_list_to_set so stale imports and get_dedup_key() data cannot
reach __change_from_import().

---

Nitpick comments:
In `@src/datamodel_code_generator/parser/base.py`:
- Around line 3179-3198: Ensure the can_retain_cache=False branch in the
module/model processing loop explicitly clears each model’s imports cache after
updating _additional_imports, matching the cache invalidation behavior of the
can_retain_cache=True branch. Add model.clear_imports_cache() within that else
path, using the surrounding model iteration and _can_retain_model_imports_cache
logic as the change location.

In `@tests/main/test_main_general.py`:
- Around line 97-198: Extend
test_parser_retains_builtin_import_cache_and_invalidates_custom_cache with a
manifest-style scenario using reuse_model=True and collapse_reuse_models=True,
including duplicate class names and mutations from __replace_unique_list_to_set
and __override_required_field before _process_single_module first reads imports.
Assert the resulting generated output and cache-retention/invalidation manifest,
covering the earlier .imports access through DataModel.get_dedup_key() in
_build_module_structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5ed4c08-5f98-4412-b75c-c31035ad7af1

📥 Commits

Reviewing files that changed from the base of the PR and between 01303c3 and 15bb061.

⛔ Files ignored due to path filters (5)
  • tests/data/expected/main/builtin_import_cache_retention.txt is excluded by !tests/data/**/*.txt and included by none
  • tests/data/expected/main/custom_import_cache_cross_module_history.txt is excluded by !tests/data/**/*.txt and included by none
  • tests/data/expected/main/custom_import_cache_cross_module_order.txt is excluded by !tests/data/**/*.txt and included by none
  • tests/data/expected/main/custom_import_cache_inherited_enum_history.txt is excluded by !tests/data/**/*.txt and included by none
  • tests/data/expected/main/custom_import_cache_inherited_enum_order.txt is excluded by !tests/data/**/*.txt and included by none
📒 Files selected for processing (21)
  • src/datamodel_code_generator/parser/base.py
  • tests/data/expected/main/builtin_import_cache_retention.py
  • tests/data/expected/main/custom_import_cache_alias_invalidation.py
  • tests/data/expected/main/custom_import_cache_cross_module/__init__.py
  • tests/data/expected/main/custom_import_cache_cross_module/inner_folder/__init__.py
  • tests/data/expected/main/custom_import_cache_cross_module/inner_folder/artificial_folder/__init__.py
  • tests/data/expected/main/custom_import_cache_cross_module/inner_folder/artificial_folder/type_1.py
  • tests/data/expected/main/custom_import_cache_cross_module/inner_folder/schema.py
  • tests/data/expected/main/custom_import_cache_cross_module/inner_folder/type_2.py
  • tests/data/expected/main/custom_import_cache_cross_module/subfolder/__init__.py
  • tests/data/expected/main/custom_import_cache_cross_module/subfolder/type_5.py
  • tests/data/expected/main/custom_import_cache_cross_module/type_4.py
  • tests/data/expected/main/custom_import_cache_generic_base.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/__init__.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/nested_state1.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/nested_state2.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/result1.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/result2.py
  • tests/data/expected/main/custom_import_cache_inherited_enum/state.py
  • tests/data/expected/main/custom_import_cache_invalidation.py
  • tests/main/test_main_general.py

@koxudaxi

Copy link
Copy Markdown
Owner Author

Addressed the review against the current code in 53e74345:

  • Unique-list cache invalidation: the normal replacement path already clears the owner cache through GenerationStore.replace_field_type(). I found and fixed the real gap: a nested list can be mutated in place before an unhashable default raises TypeError, which skipped GenerationStore. The added external schema/golden E2E combines duplicate inline models, reuse_model, collapse_reuse_models, a warmed imports cache, and the unique-list mutation. It asserts Item:invalidated, while unchanged Left and Model remain retained. Generated output matches origin/main.
  • Final validate_default ordering: left unchanged after verification. origin/main uses the same order; the first pass sets the required flag before the imports snapshot, and the built-in final pass is idempotent. Moving the pass changes observable timing for external custom fields/hooks.
  • Generic-base fallback: left unchanged after verification. The non-retain path already performs the legacy blanket clear before generic-base processing, and GenerationStore.set_base_classes() clears on the following mutation. An extra clear changes the externally observable custom hook history and generated golden output.
  • Mutation-ordering coverage: the new compound E2E covers the reported unique/reuse/collapse gap. Existing required-field override tests cover the GenerationStore insert/remove invalidation paths. get_dedup_key() invalidation beyond imports is pre-existing on main and is not changed by this PR.

Validation: 5518 passed, 16 skipped; 1,827 generated files compiled and 79 executed; Ruff, ty, changed-file hooks, GenerationStore guard, and diff checks pass.

@koxudaxi koxudaxi merged commit 1828eff into main Jul 11, 2026
61 of 62 checks passed
@koxudaxi koxudaxi deleted the agent/retain-import-caches branch July 11, 2026 04:51
@github-actions

Copy link
Copy Markdown
Contributor

Breaking Change Analysis

Result: No breaking changes detected

Reasoning: PR #3600 is an internal performance optimization that retains model import caches when possible. All modified functions are private (name-mangled __ methods and module-private _ helpers in parser/base.py) — none are part of the public Python API or CLI. No command-line options or public API parameters were added, removed, or changed. Generated code output is unchanged; the new expected-output fixtures show identical model code, with only test-injected cache_clear_history instrumentation lines differing. No default behavior changes, no template changes, no Python version drops, and no error-handling changes. Newly added internal can_retain_cache keyword defaults preserve existing behavior. Therefore there are no user-facing breaking changes.


This analysis was performed by Claude Code Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant