crashbytes-testkit 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ * @CrashBytes
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e ".[dev]"
21
+ - run: ruff check src/ tests/
22
+ - run: mypy --strict src/
23
+ - run: pytest --cov=crashbytes_testkit --cov-branch --cov-fail-under=90
@@ -0,0 +1,19 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install build
18
+ - run: python -m build
19
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CrashBytes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: crashbytes-testkit
3
+ Version: 1.0.0
4
+ Summary: Test data builders for Python — auto-generate dataclass instances from type hints.
5
+ Project-URL: Homepage, https://github.com/CrashBytes/crashbytes-testkit
6
+ Project-URL: Repository, https://github.com/CrashBytes/crashbytes-testkit
7
+ Project-URL: Issues, https://github.com/CrashBytes/crashbytes-testkit/issues
8
+ Author-email: CrashBytes <crashbytes@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: builder,dataclass,fixture,test-data,testing
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy; extra == 'dev'
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: ruff; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # crashbytes-testkit
30
+
31
+ Test data builders for Python — auto-generate dataclass instances from type hints.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install crashbytes-testkit
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from dataclasses import dataclass
43
+ from crashbytes_testkit import Fixture, Builder
44
+
45
+ @dataclass
46
+ class User:
47
+ name: str
48
+ age: int
49
+ email: str
50
+
51
+ # Auto-generate from type hints
52
+ user = Fixture.create(User)
53
+ users = Fixture.create_many(User, 5)
54
+
55
+ # Override specific fields
56
+ admin = Fixture.create(User, name="Admin", age=30)
57
+
58
+ # Fluent builder
59
+ user = (
60
+ Builder(User)
61
+ .with_field("name", "Alice")
62
+ .with_field("age", 25)
63
+ .build()
64
+ )
65
+ ```
66
+
67
+ Supports: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`, `list[T]`, `dict[K,V]`, `set[T]`, `tuple`, `Optional[T]`, nested dataclasses.
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,43 @@
1
+ # crashbytes-testkit
2
+
3
+ Test data builders for Python — auto-generate dataclass instances from type hints.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install crashbytes-testkit
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from dataclasses import dataclass
15
+ from crashbytes_testkit import Fixture, Builder
16
+
17
+ @dataclass
18
+ class User:
19
+ name: str
20
+ age: int
21
+ email: str
22
+
23
+ # Auto-generate from type hints
24
+ user = Fixture.create(User)
25
+ users = Fixture.create_many(User, 5)
26
+
27
+ # Override specific fields
28
+ admin = Fixture.create(User, name="Admin", age=30)
29
+
30
+ # Fluent builder
31
+ user = (
32
+ Builder(User)
33
+ .with_field("name", "Alice")
34
+ .with_field("age", 25)
35
+ .build()
36
+ )
37
+ ```
38
+
39
+ Supports: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`, `list[T]`, `dict[K,V]`, `set[T]`, `tuple`, `Optional[T]`, nested dataclasses.
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "crashbytes-testkit"
7
+ version = "1.0.0"
8
+ description = "Test data builders for Python — auto-generate dataclass instances from type hints."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "CrashBytes", email = "crashbytes@users.noreply.github.com" }]
13
+ keywords = ["testing", "fixture", "builder", "dataclass", "test-data"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest", "pytest-cov", "mypy", "ruff"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/CrashBytes/crashbytes-testkit"
31
+ Repository = "https://github.com/CrashBytes/crashbytes-testkit"
32
+ Issues = "https://github.com/CrashBytes/crashbytes-testkit/issues"
33
+
34
+ [tool.ruff]
35
+ target-version = "py310"
36
+ line-length = 99
37
+
38
+ [tool.ruff.lint]
39
+ select = ["E", "F", "I", "N", "UP", "B", "SIM", "TCH"]
40
+
41
+ [tool.mypy]
42
+ strict = true
43
+ python_version = "3.10"
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+
48
+ [tool.coverage.run]
49
+ branch = true
50
+ source = ["crashbytes_testkit"]
51
+
52
+ [tool.coverage.report]
53
+ fail_under = 90
@@ -0,0 +1,5 @@
1
+ """crashbytes-testkit — Test data builders for Python."""
2
+
3
+ from crashbytes_testkit._core import Builder, Fixture
4
+
5
+ __all__ = ["Builder", "Fixture"]
@@ -0,0 +1,146 @@
1
+ """Test data builders — auto-generate dataclass instances from type hints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import random
7
+ import string
8
+ import uuid
9
+ from datetime import date, datetime
10
+ from typing import Any, TypeVar, get_type_hints
11
+
12
+ T = TypeVar("T")
13
+
14
+ _counter = 0
15
+
16
+
17
+ def _next_id() -> int:
18
+ global _counter # noqa: PLW0603
19
+ _counter += 1
20
+ return _counter
21
+
22
+
23
+ def _random_string(length: int = 8) -> str:
24
+ return "".join(random.choices(string.ascii_lowercase, k=length))
25
+
26
+
27
+ def _generate_value(type_hint: Any, field_name: str = "") -> Any:
28
+ """Generate a plausible value for a given type hint."""
29
+ origin = getattr(type_hint, "__origin__", None)
30
+ args = getattr(type_hint, "__args__", ())
31
+
32
+ # Handle Union types (Optional[X], X | Y, etc.)
33
+ import types
34
+ import typing
35
+ if origin is typing.Union or isinstance(type_hint, types.UnionType):
36
+ type_args = args if args else getattr(type_hint, "__args__", ())
37
+ non_none = [a for a in type_args if a is not type(None)]
38
+ if non_none:
39
+ return _generate_value(non_none[0], field_name)
40
+ return None
41
+
42
+ if type_hint is str:
43
+ return f"{field_name}_{_random_string()}" if field_name else _random_string()
44
+ if type_hint is int:
45
+ return _next_id()
46
+ if type_hint is float:
47
+ return round(random.uniform(0.0, 100.0), 2)
48
+ if type_hint is bool:
49
+ return random.choice([True, False])
50
+ if type_hint is bytes:
51
+ return _random_string().encode()
52
+ if type_hint is datetime:
53
+ return datetime(2024, 1, 1, 12, 0, 0)
54
+ if type_hint is date:
55
+ return date(2024, 1, 1)
56
+
57
+ # list[X]
58
+ if origin is list:
59
+ inner = args[0] if args else str
60
+ return [_generate_value(inner, field_name) for _ in range(2)]
61
+
62
+ # dict[K, V]
63
+ if origin is dict:
64
+ k_type = args[0] if args else str
65
+ v_type = args[1] if len(args) > 1 else str # type: ignore[misc]
66
+ return {_generate_value(k_type): _generate_value(v_type) for _ in range(2)}
67
+
68
+ # set[X]
69
+ if origin is set:
70
+ inner = args[0] if args else str
71
+ return {_generate_value(inner, field_name) for _ in range(2)}
72
+
73
+ # tuple[X, ...]
74
+ if origin is tuple:
75
+ if args:
76
+ return tuple(_generate_value(a, field_name) for a in args if a is not Ellipsis)
77
+ return ()
78
+
79
+ # UUID
80
+ if type_hint is uuid.UUID:
81
+ return uuid.uuid4()
82
+
83
+ # Nested dataclass
84
+ if dataclasses.is_dataclass(type_hint) and isinstance(type_hint, type):
85
+ return _create_instance(type_hint)
86
+
87
+ # Fallback
88
+ return None
89
+
90
+
91
+ def _create_instance(cls: type[T], overrides: dict[str, Any] | None = None) -> T:
92
+ """Create an instance of *cls* with auto-generated values."""
93
+ hints = get_type_hints(cls)
94
+ kwargs: dict[str, Any] = {}
95
+
96
+ for field in dataclasses.fields(cls): # type: ignore[arg-type]
97
+ if overrides and field.name in overrides:
98
+ kwargs[field.name] = overrides[field.name]
99
+ elif field.default is not dataclasses.MISSING:
100
+ kwargs[field.name] = field.default
101
+ elif field.default_factory is not dataclasses.MISSING:
102
+ kwargs[field.name] = field.default_factory()
103
+ else:
104
+ type_hint = hints.get(field.name, str)
105
+ kwargs[field.name] = _generate_value(type_hint, field.name)
106
+
107
+ return cls(**kwargs)
108
+
109
+
110
+ class Fixture:
111
+ """Auto-generate test data from dataclass type hints."""
112
+
113
+ @staticmethod
114
+ def create(cls: type[T], **overrides: Any) -> T:
115
+ """Create a single instance of *cls*."""
116
+ if not dataclasses.is_dataclass(cls):
117
+ raise TypeError(f"{cls.__name__} is not a dataclass")
118
+ return _create_instance(cls, overrides or None)
119
+
120
+ @staticmethod
121
+ def create_many(cls: type[T], count: int = 3, **overrides: Any) -> list[T]:
122
+ """Create *count* instances of *cls*."""
123
+ return [Fixture.create(cls, **overrides) for _ in range(count)]
124
+
125
+
126
+ class Builder:
127
+ """Fluent builder for constructing test data."""
128
+
129
+ def __init__(self, cls: type[Any]) -> None:
130
+ if not dataclasses.is_dataclass(cls):
131
+ raise TypeError(f"{cls.__name__} is not a dataclass")
132
+ self._cls = cls
133
+ self._overrides: dict[str, Any] = {}
134
+
135
+ def with_field(self, name: str, value: Any) -> Builder:
136
+ """Set a specific field value."""
137
+ self._overrides[name] = value
138
+ return self
139
+
140
+ def build(self) -> Any:
141
+ """Build the instance."""
142
+ return _create_instance(self._cls, self._overrides)
143
+
144
+ def build_many(self, count: int = 3) -> list[Any]:
145
+ """Build *count* instances."""
146
+ return [self.build() for _ in range(count)]
@@ -0,0 +1,289 @@
1
+ """Tests for crashbytes-testkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import dataclass, field
7
+ from datetime import date, datetime
8
+
9
+ import pytest
10
+
11
+ from crashbytes_testkit import Builder, Fixture
12
+
13
+
14
+ @dataclass
15
+ class SimpleUser:
16
+ name: str
17
+ age: int
18
+ active: bool
19
+
20
+
21
+ @dataclass
22
+ class Address:
23
+ street: str
24
+ city: str
25
+ zip_code: str
26
+
27
+
28
+ @dataclass
29
+ class UserWithAddress:
30
+ name: str
31
+ address: Address
32
+
33
+
34
+ @dataclass
35
+ class UserWithDefaults:
36
+ name: str
37
+ role: str = "user"
38
+ tags: list[str] = field(default_factory=list)
39
+
40
+
41
+ @dataclass
42
+ class UserWithOptional:
43
+ name: str
44
+ nickname: str | None = None
45
+
46
+
47
+ @dataclass
48
+ class UserWithCollections:
49
+ name: str
50
+ scores: list[int] = field(default_factory=list)
51
+ metadata: dict[str, str] = field(default_factory=dict)
52
+ unique_ids: set[int] = field(default_factory=set)
53
+
54
+
55
+ @dataclass
56
+ class UserWithTypes:
57
+ id: uuid.UUID
58
+ created_at: datetime
59
+ birth_date: date
60
+ score: float
61
+ data: bytes
62
+
63
+
64
+ @dataclass
65
+ class UserWithTuple:
66
+ name: str
67
+ coords: tuple[float, float] = (0.0, 0.0)
68
+
69
+
70
+ class TestFixtureCreate:
71
+ def test_creates_instance(self) -> None:
72
+ user = Fixture.create(SimpleUser)
73
+ assert isinstance(user, SimpleUser)
74
+ assert isinstance(user.name, str)
75
+ assert isinstance(user.age, int)
76
+ assert isinstance(user.active, bool)
77
+
78
+ def test_overrides(self) -> None:
79
+ user = Fixture.create(SimpleUser, name="John", age=30)
80
+ assert user.name == "John"
81
+ assert user.age == 30
82
+
83
+ def test_with_defaults(self) -> None:
84
+ user = Fixture.create(UserWithDefaults)
85
+ assert user.role == "user"
86
+ assert user.tags == []
87
+
88
+ def test_with_optional(self) -> None:
89
+ user = Fixture.create(UserWithOptional)
90
+ assert isinstance(user.name, str)
91
+ # nickname should be generated as a string (non-None branch)
92
+ assert user.nickname is None or isinstance(user.nickname, str)
93
+
94
+ def test_nested_dataclass(self) -> None:
95
+ user = Fixture.create(UserWithAddress)
96
+ assert isinstance(user.address, Address)
97
+ assert isinstance(user.address.street, str)
98
+
99
+ def test_with_types(self) -> None:
100
+ user = Fixture.create(UserWithTypes)
101
+ assert isinstance(user.id, uuid.UUID)
102
+ assert isinstance(user.created_at, datetime)
103
+ assert isinstance(user.birth_date, date)
104
+ assert isinstance(user.score, float)
105
+ assert isinstance(user.data, bytes)
106
+
107
+ def test_raises_for_non_dataclass(self) -> None:
108
+ with pytest.raises(TypeError, match="not a dataclass"):
109
+ Fixture.create(dict) # type: ignore[arg-type]
110
+
111
+
112
+ class TestFixtureCreateMany:
113
+ def test_creates_multiple(self) -> None:
114
+ users = Fixture.create_many(SimpleUser, 5)
115
+ assert len(users) == 5
116
+ assert all(isinstance(u, SimpleUser) for u in users)
117
+
118
+ def test_default_count(self) -> None:
119
+ users = Fixture.create_many(SimpleUser)
120
+ assert len(users) == 3
121
+
122
+ def test_with_overrides(self) -> None:
123
+ users = Fixture.create_many(SimpleUser, 3, active=True)
124
+ assert all(u.active is True for u in users)
125
+
126
+
127
+ class TestBuilder:
128
+ def test_basic_build(self) -> None:
129
+ user = Builder(SimpleUser).build()
130
+ assert isinstance(user, SimpleUser)
131
+
132
+ def test_with_field(self) -> None:
133
+ user = Builder(SimpleUser).with_field("name", "Alice").with_field("age", 25).build()
134
+ assert user.name == "Alice"
135
+ assert user.age == 25
136
+
137
+ def test_build_many(self) -> None:
138
+ users = Builder(SimpleUser).with_field("active", True).build_many(4)
139
+ assert len(users) == 4
140
+ assert all(u.active is True for u in users)
141
+
142
+ def test_fluent_chaining(self) -> None:
143
+ builder = Builder(SimpleUser)
144
+ result = builder.with_field("name", "Bob")
145
+ assert result is builder # Returns self
146
+
147
+ def test_raises_for_non_dataclass(self) -> None:
148
+ with pytest.raises(TypeError, match="not a dataclass"):
149
+ Builder(list) # type: ignore[arg-type]
150
+
151
+
152
+ class TestCollections:
153
+ def test_list_generation(self) -> None:
154
+ user = Fixture.create(UserWithCollections)
155
+ # Default factory will be used
156
+ assert isinstance(user.scores, list)
157
+ assert isinstance(user.metadata, dict)
158
+
159
+ def test_tuple_with_default(self) -> None:
160
+ user = Fixture.create(UserWithTuple)
161
+ assert user.coords == (0.0, 0.0)
162
+
163
+
164
+ # Additional types for coverage
165
+
166
+
167
+ @dataclass
168
+ class WithList:
169
+ items: list[str]
170
+
171
+
172
+ @dataclass
173
+ class WithDict:
174
+ data: dict[str, int]
175
+
176
+
177
+ @dataclass
178
+ class WithSet:
179
+ ids: set[int]
180
+
181
+
182
+ @dataclass
183
+ class WithTuple:
184
+ pair: tuple[int, str]
185
+
186
+
187
+ @dataclass
188
+ class WithBareList:
189
+ items: list[str]
190
+
191
+
192
+ @dataclass
193
+ class WithOptionalStr:
194
+ name: str
195
+ bio: str | None = None
196
+
197
+
198
+ class TestGenerateValuePaths:
199
+ def test_list_type(self) -> None:
200
+ obj = Fixture.create(WithList)
201
+ assert isinstance(obj.items, list)
202
+ assert len(obj.items) == 2
203
+ assert all(isinstance(i, str) for i in obj.items)
204
+
205
+ def test_dict_type(self) -> None:
206
+ obj = Fixture.create(WithDict)
207
+ assert isinstance(obj.data, dict)
208
+ assert len(obj.data) == 2
209
+
210
+ def test_set_type(self) -> None:
211
+ obj = Fixture.create(WithSet)
212
+ assert isinstance(obj.ids, set)
213
+ assert all(isinstance(i, int) for i in obj.ids)
214
+
215
+ def test_tuple_type(self) -> None:
216
+ obj = Fixture.create(WithTuple)
217
+ assert isinstance(obj.pair, tuple)
218
+ assert isinstance(obj.pair[0], int)
219
+ assert isinstance(obj.pair[1], str)
220
+
221
+ def test_optional_generates_value(self) -> None:
222
+ # Test the Union/Optional branch
223
+ obj = Fixture.create(WithOptionalStr)
224
+ assert isinstance(obj.name, str)
225
+
226
+ def test_bytes_type(self) -> None:
227
+ obj = Fixture.create(UserWithTypes)
228
+ assert isinstance(obj.data, bytes)
229
+
230
+ def test_float_type(self) -> None:
231
+ obj = Fixture.create(UserWithTypes)
232
+ assert isinstance(obj.score, float)
233
+ assert 0.0 <= obj.score <= 100.0
234
+
235
+ def test_uuid_type(self) -> None:
236
+ obj = Fixture.create(UserWithTypes)
237
+ assert isinstance(obj.id, uuid.UUID)
238
+
239
+ def test_datetime_type(self) -> None:
240
+ obj = Fixture.create(UserWithTypes)
241
+ assert isinstance(obj.created_at, datetime)
242
+
243
+ def test_date_type(self) -> None:
244
+ obj = Fixture.create(UserWithTypes)
245
+ assert isinstance(obj.birth_date, date)
246
+
247
+ def test_nested_dataclass(self) -> None:
248
+ obj = Fixture.create(UserWithAddress)
249
+ assert isinstance(obj.address, Address)
250
+ assert isinstance(obj.address.city, str)
251
+
252
+ def test_field_name_in_string(self) -> None:
253
+ user = Fixture.create(SimpleUser)
254
+ assert "name" in user.name
255
+
256
+ def test_create_many_unique(self) -> None:
257
+ users = Fixture.create_many(SimpleUser, 5)
258
+ names = [u.name for u in users]
259
+ assert len(set(names)) == 5 # All unique
260
+
261
+
262
+ @dataclass
263
+ class WithBareTuple:
264
+ data: tuple[()]
265
+
266
+
267
+ @dataclass
268
+ class WithUnknownType:
269
+ value: object
270
+
271
+
272
+ @dataclass
273
+ class WithOptionalTyping:
274
+ name: str | None
275
+
276
+
277
+ class TestEdgeCases:
278
+ def test_empty_tuple(self) -> None:
279
+ obj = Fixture.create(WithBareTuple)
280
+ assert isinstance(obj.data, tuple)
281
+
282
+ def test_unknown_type_returns_none(self) -> None:
283
+ obj = Fixture.create(WithUnknownType)
284
+ assert obj.value is None
285
+
286
+ def test_union_type_syntax(self) -> None:
287
+ obj = Fixture.create(WithOptionalTyping)
288
+ # Should generate a string (non-None branch of str | None)
289
+ assert obj.name is None or isinstance(obj.name, str)