Skip to content

Commit e69fc05

Browse files
committed
fix: implement function to remove constraints from forward references and add tests
Refs: pydantic/pydantic#3745 Refs: koxudaxi/datamodel-code-generator#2447
1 parent 35b9ad4 commit e69fc05

3 files changed

Lines changed: 275 additions & 0 deletions

File tree

src/osw/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import osw.model.entity as model
4141
from osw.defaults import params as default_params
42+
from osw.utils.code_postprocessing import remove_constraints_from_forward_refs
4243
from osw.utils.oold import (
4344
AggregateGeneratedSchemasParam,
4445
AggregateGeneratedSchemasParamMode,
@@ -964,6 +965,9 @@ def _fetch_schema(
964965
# add imports to the beginning of the file
965966
content = "\n".join(sorted(imports)) + "\n\n" + content
966967

968+
# remove contrains from ForwardRefs
969+
content = remove_constraints_from_forward_refs(content)
970+
967971
# run formatting tool black on the combined content
968972
# consolidate imports as well
969973
try:
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import re
2+
3+
4+
def remove_constraints_from_forward_refs(src_code):
5+
"""Remove min_items/max_items from Fields with forward reference types."""
6+
# First, identify all class definitions and their positions
7+
class_pattern = r"^class (\w+)\("
8+
classes = {}
9+
for match in re.finditer(class_pattern, src_code, re.MULTILINE):
10+
classes[match.group(1)] = match.start()
11+
12+
# Pattern to match Field definitions with types
13+
pattern = r"(\w+):\s*list\[(\w+)\][^=]*=\s*Field\(((?:[^()]|\([^()]*\))*?)\)"
14+
15+
def replace_field(match):
16+
field_name = match.group(1)
17+
type_name = match.group(2)
18+
field_args = match.group(3)
19+
field_start = match.start()
20+
21+
# Check if type_name is a forward reference (defined later in the file)
22+
is_forward_ref = type_name in classes and classes[type_name] > field_start
23+
24+
if is_forward_ref:
25+
if re.search(r"\bmin_items\s*=\s*\d+", field_args) or re.search(
26+
r"\bmax_items\s*=\s*\d+", field_args
27+
):
28+
# Remove min_items and max_items
29+
new_args = re.sub(r",?\s*min_items\s*=\s*\d+", "", field_args)
30+
new_args = re.sub(r",?\s*max_items\s*=\s*\d+", "", new_args)
31+
32+
# Clean up extra commas and whitespace
33+
new_args = re.sub(r",\s*,", ",", new_args)
34+
new_args = re.sub(r"^\s*,\s*", "", new_args)
35+
new_args = re.sub(r",\s*$", "", new_args)
36+
37+
result = f"{field_name}: list[{type_name}] | None = Field({new_args})"
38+
# if min_items or max_items were in field args add comment:
39+
result += (
40+
"\n# note: removed min_items/max_items due to forward reference"
41+
)
42+
return result
43+
44+
return match.group(0)
45+
46+
return re.sub(pattern, replace_field, src_code)
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import re
2+
3+
from osw.utils.code_postprocessing import remove_constraints_from_forward_refs
4+
5+
6+
def test_remove_constraints_from_forward_refs():
7+
"""Test that min_items/max_items are removed only from forward references."""
8+
9+
src_code = '''class Distribution(OswBaseModel):
10+
class Config:
11+
schema_extra = {"title": "Distribution"}
12+
13+
download_url: str = Field(..., title="Download URL")
14+
15+
16+
class DatasetSchema(Item):
17+
class Config:
18+
schema_extra = {
19+
"@context": [{"schema_definition": "Property:HasSchemaDefinition"}],
20+
"title": "DatasetSchema",
21+
"defaultProperties": ["schema_definition", "label", "uuid"],
22+
}
23+
24+
schema_definition: str | None = Field(None, title="Dataset schema")
25+
"""
26+
Defintion of a JSON based Schema for a dataset.
27+
"""
28+
type: list[str] | None = ["Category:OSW0d6584b3e2b64e9595733c4c3963c486"]
29+
30+
31+
class Dataset(Data):
32+
class Config:
33+
schema_extra = {
34+
"@context": [
35+
"/wiki/Category:OSW2ac4493f8635481eaf1db961b63c8325?action=raw&slot=jsonschema",
36+
{
37+
"url": "dcat:landingPage",
38+
"url*": "Property:HasUrl",
39+
"themes": {"@type": "@id", "@id": "dcat:theme"},
40+
"themes*": {"@type": "@id", "@id": "Property:IsRelatedTo"},
41+
"distributions": {"@type": "@id", "@id": "dcat:distribution"},
42+
"distributions*": {
43+
"@type": "@id",
44+
"@id": "Property:HasDistribution",
45+
},
46+
"download_url": "dcat:downloadURL",
47+
"download_url*": "Property:HasUrl",
48+
"data_format": "Property:HasDataFormat",
49+
"dataset_schema": "Property:HasDatasetSchema",
50+
},
51+
],
52+
"title": "Dataset",
53+
"defaultProperties": ["url", "themes"],
54+
"uuid": "fe729745-90fd-4e8b-a94c-d4e8366375e8",
55+
"title*": {"en": "Dataset"},
56+
"description": "",
57+
"description*": {},
58+
}
59+
60+
type: list[str] | None = ["Category:OSWfe72974590fd4e8ba94cd4e8366375e8"]
61+
url: list[str] | None = Field(None, title="URL / Websites")
62+
"""
63+
Landing page(s) that documents the dataset
64+
"""
65+
themes: list[Entity] | None = Field(
66+
None, range="Category:Entity", title="Themes / Topics"
67+
)
68+
"""
69+
Terms to categorizes this dataset
70+
"""
71+
distributions: list[Distribution] | None = Field(
72+
None, title="Distributions / Downloads"
73+
)
74+
"""
75+
Actual download options for this dataset
76+
"""
77+
data_format: list[DataFormat] | None = Field(
78+
None,
79+
min_items=1,
80+
range="Category:OSWccac243b31f94574847861e5d9685b82",
81+
title="Data format",
82+
)
83+
data_f: list[DataFormat] | None = Field(None, min_items=1, title="Test")
84+
"""
85+
Gives a structure to define specific formats of data.
86+
"""
87+
dataset_schema: list[DatasetSchema] | None = Field(
88+
None,
89+
min_items=1,
90+
range="Category:OSW0d6584b3e2b64e9595733c4c3963c486",
91+
title="Dataset schema",
92+
)
93+
94+
95+
class DataTerm(DefinedTerm):
96+
class Config:
97+
schema_extra = {
98+
"@context": [
99+
"/wiki/Category:OSWa5812d3b5119416c8da1606cbe7054eb?action=raw&slot=jsonschema"
100+
],
101+
"title": "DataTerm",
102+
}
103+
104+
type: list[str] | None = ["Category:OSW34f9d66c7e1241c8b9543231526f126d"]
105+
106+
107+
class DataFormat(DataTerm):
108+
class Config:
109+
schema_extra = {
110+
"defaultProperties": [
111+
"specification",
112+
"description",
113+
"label",
114+
"short_name",
115+
],
116+
"title": "DataFormat",
117+
}
118+
119+
specification: str | None = Field(
120+
None,
121+
description_={"de": "Entweder Text oder Link"},
122+
title="Specification",
123+
title_={"de": "Spezifikation"},
124+
)
125+
"""
126+
Either text or link
127+
"""
128+
type: list[str] | None = ["Category:OSWccac243b31f94574847861e5d9685b82"]
129+
''' # noqa
130+
131+
result = remove_constraints_from_forward_refs(src_code)
132+
133+
# Test 1: DataFormat forward references should have min_items removed
134+
assert (
135+
"data_format: list[DataFormat] | None = Field(\n None,\n min_items=1," # noqa
136+
not in result
137+
)
138+
assert (
139+
"data_format: list[DataFormat] | None = Field(\n None,\n range="
140+
in result
141+
)
142+
143+
# Test 2: data_f field should have min_items removed
144+
assert (
145+
'data_f: list[DataFormat] | None = Field(None, min_items=1, title="Test")'
146+
not in result
147+
)
148+
assert 'data_f: list[DataFormat] | None = Field(None, title="Test")' in result
149+
150+
# Test 3: DatasetSchema is NOT a forward ref (defined before Dataset),
151+
# so min_items should remain
152+
assert (
153+
"dataset_schema: list[DatasetSchema] | None = Field(\n None,\n min_items=1," # noqa
154+
in result
155+
)
156+
157+
# Test 4: Other fields should remain unchanged
158+
assert (
159+
'distributions: list[Distribution] | None = Field(\n None, title="Distributions / Downloads"\n )' # noqa
160+
in result
161+
)
162+
assert (
163+
'themes: list[Entity] | None = Field(\n None, range="Category:Entity", title="Themes / Topics"\n )' # noqa
164+
in result
165+
)
166+
167+
# Test 5: Class definitions should remain unchanged
168+
assert "class Distribution(OswBaseModel):" in result
169+
assert "class DatasetSchema(Item):" in result
170+
assert "class Dataset(Data):" in result
171+
assert "class DataFormat(DataTerm):" in result
172+
173+
print("All tests passed!")
174+
175+
176+
def test_edge_cases():
177+
"""Test edge cases for the regex function."""
178+
179+
# Test case 1: max_items should also be removed
180+
src_code_with_max = """class Dataset(Data):
181+
data_format: list[DataFormat] | None = Field(None, min_items=1, max_items=10, title="Data format") # noqa
182+
183+
class DataFormat(DataTerm):
184+
pass
185+
"""
186+
result = remove_constraints_from_forward_refs(src_code_with_max)
187+
assert "min_items=1" not in result
188+
assert "max_items=10" not in result
189+
assert 'title="Data format"' in result
190+
191+
# Test case 2: No forward references - should not remove constraints
192+
src_code_no_forward = """class DataFormat(DataTerm):
193+
pass
194+
195+
class Dataset(Data):
196+
data_format: list[DataFormat] | None = Field(None, min_items=1, title="Data format")
197+
"""
198+
result = remove_constraints_from_forward_refs(src_code_no_forward)
199+
assert "min_items=1" in result # Still forward ref since DataFormat is before
200+
201+
# Test case 3: Multiple constraints on same line
202+
src_code_multiple = """class Dataset(Data):
203+
field1: list[ForwardType] | None = Field(None, min_items=1, max_items=5, range="test")
204+
field2: list[BackwardType] | None = Field(None, min_items=2, title="Test")
205+
206+
class BackwardType(Item):
207+
pass
208+
209+
class ForwardType(Item):
210+
pass
211+
"""
212+
result = remove_constraints_from_forward_refs(src_code_multiple)
213+
# field1 references ForwardType (defined later) - constraints should be removed
214+
assert "field1: list[ForwardType]" in result
215+
assert re.search(r"field1:.*min_items=1", result) is None
216+
assert re.search(r"field1:.*max_items=5", result) is None
217+
# field2 references BackwardType (defined later) - constraints should be removed
218+
assert re.search(r"field2:.*min_items=2", result) is None
219+
220+
print("All edge case tests passed!")
221+
222+
223+
if __name__ == "__main__":
224+
test_remove_constraints_from_forward_refs()
225+
test_edge_cases()

0 commit comments

Comments
 (0)