Skip to content

Add ReadOnly support for TypedDict with --use-frozen-field#2813

Merged
koxudaxi merged 4 commits intomainfrom
feature/typed-dict-readonly-support
Dec 26, 2025
Merged

Add ReadOnly support for TypedDict with --use-frozen-field#2813
koxudaxi merged 4 commits intomainfrom
feature/typed-dict-readonly-support

Conversation

@koxudaxi
Copy link
Copy Markdown
Owner

@koxudaxi koxudaxi commented Dec 26, 2025

Fixes: #2808

Summary by CodeRabbit

  • New Features

    • Added ReadOnly support for TypedDict fields in Python 3.13+
    • Included typing_extensions backport for ReadOnly on Python 3.11-3.12
  • Tests

    • Added test coverage for frozen field TypedDict scenarios across Python 3.10, 3.11, and 3.13

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 26, 2025

📝 Walkthrough

Walkthrough

This PR adds support for Python 3.13's typing.ReadOnly feature to work with the --use-frozen-field flag for TypedDict models. It introduces version-aware field model selection, ReadOnly type hint wrapping, import handling, and corresponding test expectations for Python 3.10–3.13 target versions.

Changes

Cohort / File(s) Summary
Version detection & type constants
src/datamodel_code_generator/format.py, src/datamodel_code_generator/types.py
Added _is_py_313_or_later cached property and has_typed_dict_read_only property to PythonVersion. Introduced READ_ONLY and READ_ONLY_PREFIX constants in types module.
Model infrastructure
src/datamodel_code_generator/model/__init__.py, src/datamodel_code_generator/model/imports.py
Refactored field model resolution in get_data_model_types to use Python version capabilities. Added IMPORT_READ_ONLY and IMPORT_READ_ONLY_BACKPORT import constants.
TypedDict field modeling
src/datamodel_code_generator/model/typed_dict.py
Extended DataModelField with DEFAULT_READ_ONLY_IMPORTS, _read_only property, and ReadOnly wrapping logic in type_hint. Introduced DataModelFieldReadOnlyBackport for Python 3.11–3.12 backport support. Updated DataModelFieldBackport to include read-only imports.
Test expectations
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py, ...use_frozen_field_typed_dict_py3{10,11}.py
Added expected TypedDict output fixtures showing ReadOnly and NotRequired wrappers for different Python versions (3.13, 3.11, 3.10).
Test coverage
tests/main/jsonschema/test_main_jsonschema.py
Added parameterized test test_main_use_frozen_field_typed_dict validating frozen-ReadOnly behavior across target Python versions with conditional skip for 3.13 when BLACK_PY313_SKIP is active.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

breaking-change-analyzed

Poem

🐰 Frozen fields now hop with joy,
ReadOnly makes TypedDict's deploy!
Python 3.13 brings the way,
With backports bright for older day—
Typing's ReadOnly, locked and sound! 🔐

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being added: ReadOnly support for TypedDict when using the --use-frozen-field option.
Linked Issues check ✅ Passed The PR implements the exact feature requested in issue #2808: adding ReadOnly support for TypedDict with the --use-frozen-field option, covering Python 3.13+ with backport support.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing ReadOnly support for TypedDict. No extraneous modifications were detected beyond the scope of the linked issue.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/typed-dict-readonly-support

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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 26, 2025

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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Dec 26, 2025

CodSpeed Performance Report

Merging #2813 will not alter performance

Comparing feature/typed-dict-readonly-support (ed1e4a0) with main (64f7a8b)

Summary

✅ 73 untouched
🆕 3 new
⏩ 10 skipped1

Benchmarks breakdown

Benchmark BASE HEAD Efficiency
🆕 test_main_use_frozen_field_typed_dict[3.10-use_frozen_field_typed_dict_py310.py] N/A 37 ms N/A
🆕 test_main_use_frozen_field_typed_dict[3.11-use_frozen_field_typed_dict_py311.py] N/A 36.9 ms N/A
🆕 test_main_use_frozen_field_typed_dict[3.13-use_frozen_field_typed_dict.py] N/A 35.3 ms N/A

Footnotes

  1. 10 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/main/jsonschema/test_main_jsonschema.py (1)

5686-5725: TypedDict frozen-field test is well‑structured; consider explicit 3.12 coverage

The parametrized test_main_use_frozen_field_typed_dict cleanly exercises the three code paths (native typing.ReadOnly, backported typing_extensions.ReadOnly, and the 3.10 NotRequired+ReadOnly combination), and the docstring accurately reflects the intended behavior. The use of BLACK_PY313_SKIP on the 3.13 case is a good guard against formatter mismatches.

One possible enhancement: since Python 3.11–3.12 share the backport path, adding a "3.12" row (reusing the use_frozen_field_typed_dict_py311.py expected file if output is identical) would give direct coverage for that intermediate target as well. Not required for correctness, but it would make the version matrix more explicit.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64f7a8b and ed1e4a0.

📒 Files selected for processing (9)
  • src/datamodel_code_generator/format.py
  • src/datamodel_code_generator/model/__init__.py
  • src/datamodel_code_generator/model/imports.py
  • src/datamodel_code_generator/model/typed_dict.py
  • src/datamodel_code_generator/types.py
  • tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py
  • tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py310.py
  • tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py311.py
  • tests/main/jsonschema/test_main_jsonschema.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-25T09:22:57.664Z
Learnt from: koxudaxi
Repo: koxudaxi/datamodel-code-generator PR: 2799
File: src/datamodel_code_generator/util.py:49-66
Timestamp: 2025-12-25T09:22:57.664Z
Learning: In datamodel-code-generator, the is_pydantic_v2() and is_pydantic_v2_11() functions in src/datamodel_code_generator/util.py intentionally use global variable caching (_is_v2, _is_v2_11) on top of lru_cache for performance optimization. This dual-layer caching eliminates function call overhead and cache lookup overhead for frequently-called version checks. The PLW0603 linter warnings should be suppressed with # noqa: PLW0603 as this is a deliberate design choice.

Applied to files:

  • src/datamodel_code_generator/format.py
🧬 Code graph analysis (6)
src/datamodel_code_generator/model/imports.py (1)
src/datamodel_code_generator/imports.py (2)
  • Import (20-38)
  • from_full_path (35-38)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py311.py (4)
src/datamodel_code_generator/model/typed_dict.py (1)
  • TypedDict (49-114)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py (1)
  • User (10-14)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py310.py (1)
  • User (12-16)
src/datamodel_code_generator/model/base.py (1)
  • name (826-828)
src/datamodel_code_generator/model/__init__.py (2)
src/datamodel_code_generator/format.py (2)
  • has_typed_dict_read_only (113-115)
  • has_typed_dict_non_required (108-110)
src/datamodel_code_generator/model/typed_dict.py (4)
  • DataModelField (117-170)
  • DataModelFieldReadOnlyBackport (173-179)
  • DataModelFieldBackport (182-189)
  • TypedDict (49-114)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py (4)
src/datamodel_code_generator/model/typed_dict.py (1)
  • TypedDict (49-114)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py310.py (1)
  • User (12-16)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py311.py (1)
  • User (12-16)
src/datamodel_code_generator/model/base.py (1)
  • name (826-828)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py310.py (3)
src/datamodel_code_generator/model/typed_dict.py (1)
  • TypedDict (49-114)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py (1)
  • User (10-14)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py311.py (1)
  • User (12-16)
src/datamodel_code_generator/model/typed_dict.py (2)
src/datamodel_code_generator/types.py (1)
  • type_hint (567-656)
src/datamodel_code_generator/model/base.py (2)
  • type_hint (276-294)
  • fall_back_to_nullable (440-442)
🔇 Additional comments (10)
tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py311.py (1)

1-16: LGTM! Test fixture correctly represents Python 3.11 target version.

The import strategy is correct: NotRequired from typing (available natively in Python 3.11+) and ReadOnly from typing_extensions (backport for pre-3.13 versions). The TypedDict structure properly demonstrates ReadOnly field wrapping.

tests/data/expected/main/jsonschema/use_frozen_field_typed_dict_py310.py (1)

1-16: LGTM! Test fixture correctly represents Python 3.10 target version.

The imports are accurate for Python 3.10: TypedDict from typing (available since 3.8) and both NotRequired and ReadOnly from typing_extensions (as backports). The structure is consistent with the other version-specific fixtures.

src/datamodel_code_generator/model/__init__.py (1)

127-136: LGTM! Version-aware field model selection is correct.

The three-way conditional properly handles the capability matrix:

  • Python 3.13+: native ReadOnly and NotRequired
  • Python 3.11-3.12: native NotRequired, backport ReadOnly
  • Python 3.10: backport both

The refactored approach with an explicit local variable improves readability compared to inline conditionals.

src/datamodel_code_generator/model/imports.py (1)

17-18: LGTM! New import constants follow established patterns.

The IMPORT_READ_ONLY and IMPORT_READ_ONLY_BACKPORT constants mirror the structure of IMPORT_NOT_REQUIRED / IMPORT_NOT_REQUIRED_BACKPORT above them, maintaining consistency in the codebase.

src/datamodel_code_generator/format.py (2)

89-91: LGTM! Version check follows established pattern.

The _is_py_313_or_later implementation is consistent with other version checks in the class and correctly uses cached_property for performance optimization.


112-115: LGTM! Property correctly exposes ReadOnly capability.

The has_typed_dict_read_only property appropriately delegates to _is_py_313_or_later and the docstring correctly references PEP 705 (TypedDict ReadOnly).

src/datamodel_code_generator/types.py (1)

89-90: LGTM! Constants follow established pattern.

The READ_ONLY and READ_ONLY_PREFIX constants mirror the NOT_REQUIRED / NOT_REQUIRED_PREFIX pattern above, maintaining consistency. These constants support the construction of ReadOnly[...] type hint wrappers.

tests/data/expected/main/jsonschema/use_frozen_field_typed_dict.py (1)

1-14: LGTM! Test fixture correctly represents Python 3.13+ target version.

The imports are accurate for Python 3.13+: all three items (NotRequired, ReadOnly, TypedDict) now come from typing as Python 3.13 introduces native ReadOnly support (PEP 705). This fixture completes the version matrix along with the py310 and py311 variants.

tests/main/jsonschema/test_main_jsonschema.py (1)

28-40: Importing BLACK_PY313_SKIP looks consistent with existing skip markers

Using a shared skip marker for Python 3.13/Black-specific behavior aligns with the other BLACK-related fixtures (e.g., LEGACY_BLACK_SKIP, MSGSPEC_LEGACY_BLACK_SKIP) and keeps version/formatter gating in one place. No changes needed here.

src/datamodel_code_generator/model/typed_dict.py (1)

13-21: ReadOnly support for TypedDict fields is wired correctly across Python versions

The changes in DataModelField and its backport variants look consistent and correct:

  • The _read_only flag is scoped to TypedDict parents and gated on both use_frozen_field and read_only, so existing schemas without readOnly or without the CLI flag remain unaffected.
  • The type hint wrapping order (ReadOnly[...] inner, then NotRequired[...] outer) is sensible: required read-only fields become ReadOnly[T], and non‑required read-only fields become NotRequired[ReadOnly[T]].
  • fall_back_to_nullable still prevents Optional[...] generation for non‑required TypedDict fields, so optionality is expressed via NotRequired rather than Optional, even in the read‑only case.
  • Imports are only added when needed:
    • Base DataModelField (3.13+) uses IMPORT_NOT_REQUIRED and IMPORT_READ_ONLY from typing.
    • DataModelFieldReadOnlyBackport (3.11–3.12) reuses typing.NotRequired but switches DEFAULT_READ_ONLY_IMPORTS to IMPORT_READ_ONLY_BACKPORT (typing_extensions.ReadOnly).
    • DataModelFieldBackport (3.10) correctly backports both NotRequired and ReadOnly from typing_extensions.

Overall, this cleanly connects the Python-version–aware field classes to the new ReadOnly capability without disturbing existing TypedDict behavior.

Also applies to: 118-171, 173-189

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.51%. Comparing base (25053ac) to head (ed1e4a0).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2813   +/-   ##
=======================================
  Coverage   99.51%   99.51%           
=======================================
  Files          89       89           
  Lines       13855    13886   +31     
  Branches     1634     1637    +3     
=======================================
+ Hits        13788    13819   +31     
  Misses         36       36           
  Partials       31       31           
Flag Coverage Δ
unittests 99.51% <100.00%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

@koxudaxi koxudaxi merged commit 983216a into main Dec 26, 2025
85 of 90 checks passed
@koxudaxi koxudaxi deleted the feature/typed-dict-readonly-support branch December 26, 2025 18:19
@github-actions
Copy link
Copy Markdown
Contributor

Breaking Change Analysis

Result: No breaking changes detected

Reasoning: This PR adds ReadOnly support for TypedDict when using --use-frozen-field, extending an existing feature to a new output model type. Previously, --use-frozen-field had no effect on TypedDict output - fields were generated without any ReadOnly wrapper. Now, when both --use-frozen-field and --output-model-type typing.TypedDict are used, readOnly schema fields are wrapped with ReadOnly[]. This is purely additive new functionality that requires explicit opt-in via the existing --use-frozen-field flag. No existing behavior is changed, no templates were modified, and no CLI options were altered. Users who don't use --use-frozen-field with TypedDict see no change.


This analysis was performed by Claude Code Action

@hgl
Copy link
Copy Markdown

hgl commented Dec 31, 2025

Thank you so much for implementing it so swiftly.

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.

Make --use-frozen-field also support TypedDict

2 participants