@@ -29,38 +29,33 @@ class TestFormatType:
2929
3030 def test_plain_str_renders_as_string (self ) -> None :
3131 ti = analyze_type (str )
32- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
33- assert format_type (field ) == "`string`"
32+ assert format_type (_make_field (ti )) == "`string`"
3433
3534 def test_optional_adds_qualifier (self ) -> None :
3635 ti = analyze_type (str | None )
37- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = False )
38- assert format_type (field ) == "`string` (optional)"
36+ assert format_type (_make_field (ti , is_required = False )) == "`string` (optional)"
3937
4038 def test_literal_renders_as_quoted_value (self ) -> None :
4139 ti = analyze_type (Literal ["places" ])
42- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
43- assert format_type (field ) == '`"places"`'
40+ assert format_type (_make_field (ti )) == '`"places"`'
4441
4542 def test_multi_value_literal_renders_comma_separated (self ) -> None :
4643 ti = analyze_type (Literal ["a" , "b" , "c" ])
47- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
48- assert format_type (field ) == '`"a"` \\ | `"b"` \\ | `"c"`'
44+ assert format_type (_make_field (ti )) == '`"a"` \\ | `"b"` \\ | `"c"`'
4945
5046 def test_enum_without_context_renders_as_code (self ) -> None :
5147 class Color (str , Enum ):
5248 RED = "red"
5349
5450 ti = analyze_type (Color )
55- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
56- assert format_type (field ) == "`Color`"
51+ assert format_type (_make_field (ti )) == "`Color`"
5752
5853 def test_enum_with_link_context (self ) -> None :
5954 class Color (str , Enum ):
6055 RED = "red"
6156
6257 ti = analyze_type (Color )
63- field = FieldSpec ( name = "x" , type_info = ti , description = None , is_required = True )
58+ field = _make_field ( ti )
6459 ctx = LinkContext (
6560 page_path = PurePosixPath ("buildings/building/building.md" ),
6661 registry = {
@@ -71,18 +66,15 @@ class Color(str, Enum):
7166
7267 def test_list_of_primitives (self ) -> None :
7368 ti = analyze_type (list [str ])
74- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
75- assert format_type (field ) == "`list<string>`"
69+ assert format_type (_make_field (ti )) == "`list<string>`"
7670
7771 def test_nested_list_of_primitives (self ) -> None :
7872 ti = analyze_type (list [list [str ]])
79- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
80- assert format_type (field ) == "`list<list<string>>`"
73+ assert format_type (_make_field (ti )) == "`list<list<string>>`"
8174
8275 def test_registered_primitive_not_linked (self ) -> None :
8376 ti = analyze_type (int32 )
84- field = FieldSpec (name = "x" , type_info = ti , description = None , is_required = True )
85- result = format_type (field )
77+ result = format_type (_make_field (ti ))
8678 assert result == "`int32`"
8779 assert "](int32.md)" not in result
8880
@@ -102,17 +94,19 @@ def test_dict_with_newtype_shows_semantic_name(self) -> None:
10294 assert result == "map<MyKey, int64>"
10395
10496
105- def _make_union_field (ti : TypeInfo , * , is_required : bool = True ) -> FieldSpec :
106- """Build a FieldSpec wrapping a union TypeInfo for test convenience."""
107- return FieldSpec (name = "x" , type_info = ti , description = None , is_required = is_required )
97+ def _make_field (
98+ ti : TypeInfo , * , name : str = "x" , is_required : bool = True
99+ ) -> FieldSpec :
100+ """Build a FieldSpec for test convenience."""
101+ return FieldSpec (name = name , type_info = ti , description = None , is_required = is_required )
108102
109103
110104class TestFormatUnionType :
111105 """Tests for UNION-kind TypeInfo in format_type."""
112106
113107 def test_union_renders_all_members (self ) -> None :
114108 ti = analyze_type (_ModelA | _ModelB )
115- result = format_type (_make_union_field (ti ))
109+ result = format_type (_make_field (ti ))
116110 assert "`_ModelA`" in result
117111 assert "`_ModelB`" in result
118112 # Pipe separator escaped for table cells
@@ -131,13 +125,13 @@ def test_union_with_link_context_links_each_member(self) -> None:
131125 ),
132126 },
133127 )
134- result = format_type (_make_union_field (ti ), ctx )
128+ result = format_type (_make_field (ti ), ctx )
135129 assert "[`_ModelA`](types/model_a.md)" in result
136130 assert "[`_ModelB`](types/model_b.md)" in result
137131
138132 def test_optional_union_adds_qualifier (self ) -> None :
139133 ti = analyze_type (_ModelA | _ModelB | None )
140- result = format_type (_make_union_field (ti , is_required = False ))
134+ result = format_type (_make_field (ti , is_required = False ))
141135 assert "(optional)" in result
142136 assert "`_ModelA`" in result
143137 assert "`_ModelB`" in result
@@ -149,14 +143,14 @@ def test_list_of_union_adds_qualifier(self) -> None:
149143 list_depth = 1 ,
150144 union_members = (_ModelA , _ModelB ),
151145 )
152- result = format_type (_make_union_field (ti ))
146+ result = format_type (_make_field (ti ))
153147 assert "(list)" in result
154148 assert "`_ModelA`" in result
155149 assert "`_ModelB`" in result
156150
157151 def test_union_members_unlinked_without_context (self ) -> None :
158152 ti = analyze_type (_ModelA | _ModelB )
159- result = format_type (_make_union_field (ti ))
153+ result = format_type (_make_field (ti ))
160154 # No markdown links without context
161155 assert "]()" not in result
162156 assert "[`" not in result
@@ -172,7 +166,7 @@ def test_union_partial_links(self) -> None:
172166 )
173167 },
174168 )
175- result = format_type (_make_union_field (ti ), ctx )
169+ result = format_type (_make_field (ti ), ctx )
176170 assert "[`_ModelA`](types/model_a.md)" in result
177171 assert "`_ModelB`" in result
178172 # _ModelB should NOT be linked
@@ -184,7 +178,7 @@ class TestPydanticTypeLinking:
184178
185179 def test_pydantic_type_linked_when_in_registry (self ) -> None :
186180 ti = analyze_type (HttpUrl )
187- field = FieldSpec ( name = "x" , type_info = ti , description = None , is_required = True )
181+ field = _make_field ( ti )
188182 ctx = LinkContext (
189183 page_path = PurePosixPath ("places/place/place.md" ),
190184 registry = {
@@ -199,7 +193,7 @@ def test_pydantic_type_linked_when_in_registry(self) -> None:
199193
200194 def test_pydantic_type_unlinked_without_registry_entry (self ) -> None :
201195 ti = analyze_type (HttpUrl )
202- field = FieldSpec ( name = "x" , type_info = ti , description = None , is_required = True )
196+ field = _make_field ( ti )
203197 ctx = LinkContext (
204198 page_path = PurePosixPath ("places/place/place.md" ),
205199 registry = {},
@@ -210,7 +204,7 @@ def test_pydantic_type_unlinked_without_registry_entry(self) -> None:
210204
211205 def test_list_of_pydantic_type_linked (self ) -> None :
212206 ti = analyze_type (list [HttpUrl ])
213- field = FieldSpec ( name = "x" , type_info = ti , description = None , is_required = True )
207+ field = _make_field ( ti )
214208 ctx = LinkContext (
215209 page_path = PurePosixPath ("places/place/place.md" ),
216210 registry = {
@@ -226,7 +220,7 @@ def test_list_of_pydantic_type_linked(self) -> None:
226220 def test_registered_primitive_links_to_aggregate_page (self ) -> None :
227221 """int32 links to the primitives aggregate page when in registry."""
228222 ti = analyze_type (int32 )
229- field = FieldSpec ( name = "x" , type_info = ti , description = None , is_required = True )
223+ field = _make_field ( ti )
230224 ctx = LinkContext (
231225 page_path = PurePosixPath ("places/place/place.md" ),
232226 registry = {
@@ -240,6 +234,59 @@ def test_registered_primitive_links_to_aggregate_page(self) -> None:
240234 assert "system/primitive/primitives.md" in result
241235
242236
237+ class TestListOfSemanticNewtype :
238+ """Tests for list[SemanticNewType] rendering.
239+
240+ When a scalar NewType appears inside list[], the type renders as
241+ list<NewTypeName> rather than NewTypeName (list). The (list) qualifier
242+ is reserved for NewTypes that internally wrap a list.
243+ """
244+
245+ def test_list_of_scalar_newtype_renders_list_syntax (self ) -> None :
246+ """list[ScalarNewType] renders as list<Name>, not Name (list)."""
247+ ScalarNT = NewType ("ScalarNT" , str )
248+ ti = analyze_type (list [ScalarNT ])
249+ result = format_type (_make_field (ti ))
250+ assert "list<" in result
251+ assert "ScalarNT" in result
252+ assert "(list)" not in result
253+
254+ def test_newtype_wrapping_list_renders_qualifier (self ) -> None :
255+ """NewType wrapping list[X] renders as Name (list)."""
256+ ListNT = NewType ("ListNT" , list [str ])
257+ ti = analyze_type (ListNT )
258+ result = format_type (_make_field (ti ))
259+ assert "(list)" in result
260+ assert "ListNT" in result
261+
262+ def test_list_of_scalar_newtype_with_link (self ) -> None :
263+ """list[ScalarNewType] with link context renders linked list<Name>."""
264+ ScalarNT = NewType ("ScalarNT" , str )
265+ ti = analyze_type (list [ScalarNT ])
266+ field = _make_field (ti )
267+ ctx = LinkContext (
268+ page_path = PurePosixPath ("places/place/place.md" ),
269+ registry = {
270+ TypeIdentity (ScalarNT , "ScalarNT" ): PurePosixPath ("system/scalar_nt.md" )
271+ },
272+ )
273+ result = format_type (field , ctx )
274+ assert "list<" in result
275+ assert "ScalarNT" in result
276+ assert "system/scalar_nt.md" in result
277+ assert "(list)" not in result
278+
279+ def test_nested_list_of_scalar_newtype_renders_nested_list_syntax (self ) -> None :
280+ """list[list[ScalarNewType]] renders as list<list<Name>>."""
281+ ScalarNT = NewType ("ScalarNT" , str )
282+ ti = analyze_type (list [list [ScalarNT ]])
283+ result = format_type (_make_field (ti ))
284+ assert "list<" in result
285+ assert "list<`" in result or "`list<list<" in result
286+ assert "ScalarNT" in result
287+ assert "(list)" not in result
288+
289+
243290class TestFormatUnderlyingUnionType :
244291 """Tests for UNION-kind TypeInfo in format_underlying_type."""
245292
0 commit comments