|
9 | 9 | from datamodel_code_generator.imports import IMPORT_DECIMAL, IMPORT_LIST, IMPORT_SET |
10 | 10 | from datamodel_code_generator.model.base import BaseClassDataType |
11 | 11 | from datamodel_code_generator.model.pydantic_v2 import BaseModel, DataModelField |
12 | | -from datamodel_code_generator.parser.generation import GenerationStore, set_model_base_classes |
| 12 | +from datamodel_code_generator.parser.generation import ( |
| 13 | + GENERATION_STORE_MUTATION_METHODS, |
| 14 | + GenerationStore, |
| 15 | + set_model_base_classes, |
| 16 | +) |
13 | 17 | from datamodel_code_generator.reference import Reference |
14 | 18 | from datamodel_code_generator.types import DataType |
15 | 19 |
|
| 20 | +IMPORT_CACHE_CLEARING_MUTATION_METHODS = frozenset({ |
| 21 | + "append_field", |
| 22 | + "collapse_root_data_type", |
| 23 | + "detach_data_type_ref", |
| 24 | + "detach_model_data_type_refs", |
| 25 | + "insert_field", |
| 26 | + "move_model", |
| 27 | + "redirect_model_reference_users", |
| 28 | + "redirect_reference_users", |
| 29 | + "remove_field", |
| 30 | + "rename_model", |
| 31 | + "replace_data_type_ref", |
| 32 | + "replace_field_type", |
| 33 | + "replace_nested_data_type", |
| 34 | + "reset_base_classes", |
| 35 | + "set_base_classes", |
| 36 | + "set_fields", |
| 37 | + "set_nested_data_types", |
| 38 | + "update_model_reference", |
| 39 | +}) |
| 40 | +IMPORT_CACHE_NEUTRAL_MUTATION_METHODS = frozenset({ |
| 41 | + "defer_refresh", |
| 42 | + "register_model", |
| 43 | +}) |
| 44 | + |
| 45 | + |
| 46 | +def _base_model(name: str = "Model", fields: list[DataModelField] | None = None) -> BaseModel: |
| 47 | + return BaseModel(fields=fields or [], reference=Reference(path=name, original_name=name, name=name)) |
| 48 | + |
| 49 | + |
| 50 | +def test_generation_store_import_cache_contract_covers_mutation_surface() -> None: |
| 51 | + """New store mutation APIs must be classified for import-cache safety.""" |
| 52 | + assert ( |
| 53 | + IMPORT_CACHE_CLEARING_MUTATION_METHODS | IMPORT_CACHE_NEUTRAL_MUTATION_METHODS |
| 54 | + ) == GENERATION_STORE_MUTATION_METHODS |
| 55 | + |
16 | 56 |
|
17 | 57 | def test_generation_store_indexes_model_and_reference_order() -> None: |
18 | 58 | """Store facts preserve append order and expose model dependencies.""" |
@@ -621,6 +661,253 @@ def test_generation_store_field_type_replacement_clears_model_imports_cache() -> |
621 | 661 | assert IMPORT_SET in model.imports |
622 | 662 |
|
623 | 663 |
|
| 664 | +def test_generation_store_model_mutations_clear_cached_imports_contract() -> None: |
| 665 | + """Model-level store mutations must clear an already populated imports cache.""" |
| 666 | + model = _base_model() |
| 667 | + store = GenerationStore() |
| 668 | + store.register_model(model) |
| 669 | + |
| 670 | + assert model.imports is not None |
| 671 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 672 | + store.update_model_reference(model, reference_name="Renamed") |
| 673 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 674 | + |
| 675 | + assert model.imports is not None |
| 676 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 677 | + store.rename_model(model, reference_name="RenamedAgain") |
| 678 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 679 | + |
| 680 | + assert model.imports is not None |
| 681 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 682 | + store.move_model(model, new_path="pkg.RenamedAgain", new_file_path=Path("pkg.py")) |
| 683 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 684 | + |
| 685 | + assert model.imports is not None |
| 686 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 687 | + store.set_base_classes(model, [BaseClassDataType(type="object")]) |
| 688 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 689 | + |
| 690 | + assert model.imports is not None |
| 691 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 692 | + store.reset_base_classes(model) |
| 693 | + assert { |
| 694 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 695 | + "path": model.path, |
| 696 | + "reference_name": model.reference.name, |
| 697 | + "file_path": model.file_path, |
| 698 | + } == snapshot({ |
| 699 | + "cache_key": False, |
| 700 | + "path": "pkg.RenamedAgain", |
| 701 | + "reference_name": "RenamedAgain", |
| 702 | + "file_path": Path("pkg.py"), |
| 703 | + }) |
| 704 | + |
| 705 | + |
| 706 | +def test_generation_store_field_collection_mutations_clear_cached_imports_contract() -> None: |
| 707 | + """Field collection helpers must invalidate stale model imports.""" |
| 708 | + model = _base_model() |
| 709 | + store = GenerationStore() |
| 710 | + store.register_model(model) |
| 711 | + list_field = DataModelField(data_type=DataType(is_list=True)) |
| 712 | + set_field = DataModelField(data_type=DataType(is_set=True)) |
| 713 | + |
| 714 | + assert model.imports is not None |
| 715 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 716 | + store.append_field(model, list_field) |
| 717 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 718 | + assert IMPORT_LIST in model.imports |
| 719 | + |
| 720 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 721 | + store.insert_field(model, 0, set_field) |
| 722 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 723 | + assert IMPORT_LIST in model.imports |
| 724 | + assert IMPORT_SET in model.imports |
| 725 | + |
| 726 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 727 | + store.remove_field(model, list_field) |
| 728 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 729 | + assert IMPORT_LIST not in model.imports |
| 730 | + assert IMPORT_SET in model.imports |
| 731 | + |
| 732 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 733 | + store.set_fields(model, [DataModelField(data_type=DataType.from_import(IMPORT_DECIMAL))]) |
| 734 | + assert { |
| 735 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 736 | + "has_decimal": IMPORT_DECIMAL in model.imports, |
| 737 | + "has_set": IMPORT_SET in model.imports, |
| 738 | + "list_parent": list_field.parent is None, |
| 739 | + "set_parent": set_field.parent is None, |
| 740 | + } == snapshot({ |
| 741 | + "cache_key": False, |
| 742 | + "has_decimal": True, |
| 743 | + "has_set": False, |
| 744 | + "list_parent": True, |
| 745 | + "set_parent": True, |
| 746 | + }) |
| 747 | + |
| 748 | + |
| 749 | +def test_generation_store_data_type_mutations_clear_cached_imports_contract() -> None: |
| 750 | + """DataType helpers must clear the cache for the owner model they mutate.""" |
| 751 | + old_reference = Reference(path="Old", original_name="Old", name="Old") |
| 752 | + new_reference = Reference(path="New", original_name="New", name="New") |
| 753 | + data_type = DataType(reference=old_reference) |
| 754 | + field = DataModelField(data_type=data_type) |
| 755 | + model = _base_model(fields=[field]) |
| 756 | + store = GenerationStore() |
| 757 | + store.register_model(model) |
| 758 | + |
| 759 | + assert model.imports is not None |
| 760 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 761 | + store.replace_data_type_ref(data_type, new_reference) |
| 762 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 763 | + assert data_type.reference is new_reference |
| 764 | + |
| 765 | + assert model.imports is not None |
| 766 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 767 | + store.detach_data_type_ref(data_type) |
| 768 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 769 | + assert data_type.reference is None |
| 770 | + |
| 771 | + assert model.imports is not None |
| 772 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 773 | + store.replace_field_type(field, DataType(is_set=True)) |
| 774 | + assert { |
| 775 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 776 | + "has_set": IMPORT_SET in model.imports, |
| 777 | + "field_parent": field.data_type.parent is field, |
| 778 | + } == snapshot({ |
| 779 | + "cache_key": False, |
| 780 | + "has_set": True, |
| 781 | + "field_parent": True, |
| 782 | + }) |
| 783 | + |
| 784 | + |
| 785 | +def test_generation_store_nested_data_type_mutations_clear_cached_imports_contract() -> None: |
| 786 | + """Nested DataType helpers must invalidate the imports cache of the outer model.""" |
| 787 | + list_type = DataType(is_list=True, data_types=[DataType(type="str")]) |
| 788 | + model = _base_model(fields=[DataModelField(data_type=list_type)]) |
| 789 | + store = GenerationStore() |
| 790 | + store.register_model(model) |
| 791 | + |
| 792 | + assert model.imports is not None |
| 793 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 794 | + store.set_nested_data_types(list_type, [DataType.from_import(IMPORT_DECIMAL)]) |
| 795 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 796 | + assert IMPORT_DECIMAL in model.imports |
| 797 | + |
| 798 | + nested_type = list_type.data_types[0] |
| 799 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 800 | + store.replace_nested_data_type(list_type, nested_type, DataType(type="str")) |
| 801 | + assert { |
| 802 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 803 | + "has_decimal": IMPORT_DECIMAL in model.imports, |
| 804 | + "nested_types": [nested.type for nested in list_type.data_types], |
| 805 | + } == snapshot({ |
| 806 | + "cache_key": False, |
| 807 | + "has_decimal": False, |
| 808 | + "nested_types": ["str"], |
| 809 | + }) |
| 810 | + |
| 811 | + |
| 812 | +def test_generation_store_reference_detach_helpers_clear_cached_imports_contract() -> None: |
| 813 | + """Bulk reference helpers must clear the cache through their delegated mutations.""" |
| 814 | + reference = Reference(path="Referenced", original_name="Referenced", name="Referenced") |
| 815 | + root_type = DataType(reference=reference) |
| 816 | + model = _base_model(fields=[DataModelField(data_type=root_type)]) |
| 817 | + store = GenerationStore() |
| 818 | + store.register_model(model) |
| 819 | + |
| 820 | + assert model.imports is not None |
| 821 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 822 | + store.collapse_root_data_type(root_type, Reference(path="Inner", original_name="Inner", name="Inner")) |
| 823 | + assert model._IMPORTS_CACHE_KEY not in model.__dict__ |
| 824 | + assert root_type.reference is not None |
| 825 | + |
| 826 | + assert model.imports is not None |
| 827 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 828 | + store.detach_model_data_type_refs(model) |
| 829 | + assert { |
| 830 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 831 | + "root_reference": root_type.reference, |
| 832 | + } == snapshot({ |
| 833 | + "cache_key": False, |
| 834 | + "root_reference": None, |
| 835 | + }) |
| 836 | + |
| 837 | + |
| 838 | +def test_generation_store_reference_redirects_clear_each_owner_imports_cache_once() -> None: |
| 839 | + """Reference redirects should clear each affected owner model once.""" |
| 840 | + old_reference = Reference(path="Old", original_name="Old", name="Old") |
| 841 | + new_reference = Reference(path="New", original_name="New", name="New") |
| 842 | + model = _base_model( |
| 843 | + fields=[ |
| 844 | + DataModelField(data_type=DataType(reference=old_reference)), |
| 845 | + DataModelField(data_type=DataType(reference=old_reference)), |
| 846 | + ] |
| 847 | + ) |
| 848 | + store = GenerationStore() |
| 849 | + store.register_model(model) |
| 850 | + assert model.imports is not None |
| 851 | + assert model._IMPORTS_CACHE_KEY in model.__dict__ |
| 852 | + |
| 853 | + store.redirect_reference_users(old_reference, new_reference) |
| 854 | + |
| 855 | + assert { |
| 856 | + "cache_key": model._IMPORTS_CACHE_KEY in model.__dict__, |
| 857 | + "field_references": [field.data_type.reference.path for field in model.fields if field.data_type.reference], |
| 858 | + "old_children": old_reference.children, |
| 859 | + "new_children": len(new_reference.children), |
| 860 | + } == snapshot({ |
| 861 | + "cache_key": False, |
| 862 | + "field_references": ["New", "New"], |
| 863 | + "old_children": [], |
| 864 | + "new_children": 2, |
| 865 | + }) |
| 866 | + |
| 867 | + |
| 868 | +def test_generation_store_model_reference_redirect_clears_only_affected_owner_imports_cache() -> None: |
| 869 | + """Scoped model reference redirects should invalidate only matching owner models.""" |
| 870 | + target_model = _base_model("Target") |
| 871 | + owner_model = _base_model( |
| 872 | + "Owner", |
| 873 | + fields=[ |
| 874 | + DataModelField(data_type=DataType(reference=target_model.reference)), |
| 875 | + DataModelField(data_type=DataType(reference=target_model.reference)), |
| 876 | + ], |
| 877 | + ) |
| 878 | + other_model = _base_model("Other", fields=[DataModelField(data_type=DataType(reference=target_model.reference))]) |
| 879 | + store = GenerationStore() |
| 880 | + store.register_model(target_model) |
| 881 | + store.register_model(owner_model) |
| 882 | + store.register_model(other_model) |
| 883 | + assert owner_model.imports is not None |
| 884 | + assert other_model.imports is not None |
| 885 | + assert owner_model._IMPORTS_CACHE_KEY in owner_model.__dict__ |
| 886 | + assert other_model._IMPORTS_CACHE_KEY in other_model.__dict__ |
| 887 | + |
| 888 | + store.redirect_model_reference_users( |
| 889 | + target_model, |
| 890 | + [owner_model], |
| 891 | + Reference(path="NewTarget", original_name="NewTarget", name="NewTarget"), |
| 892 | + ) |
| 893 | + |
| 894 | + assert { |
| 895 | + "owner_cache_key": owner_model._IMPORTS_CACHE_KEY in owner_model.__dict__, |
| 896 | + "other_cache_key": other_model._IMPORTS_CACHE_KEY in other_model.__dict__, |
| 897 | + "owner_references": [ |
| 898 | + field.data_type.reference.path for field in owner_model.fields if field.data_type.reference |
| 899 | + ], |
| 900 | + "other_references": [ |
| 901 | + field.data_type.reference.path for field in other_model.fields if field.data_type.reference |
| 902 | + ], |
| 903 | + } == snapshot({ |
| 904 | + "owner_cache_key": False, |
| 905 | + "other_cache_key": True, |
| 906 | + "owner_references": ["NewTarget", "NewTarget"], |
| 907 | + "other_references": ["Target"], |
| 908 | + }) |
| 909 | + |
| 910 | + |
624 | 911 | def test_generation_store_redirects_model_reference_users_by_owner() -> None: |
625 | 912 | """Reference redirection should only affect children owned by the requested models.""" |
626 | 913 | reference_target = Reference(path="Target", original_name="Target", name="Target") |
|
0 commit comments