crashbytes-testkit 1.0.0__py3-none-any.whl
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.
- crashbytes_testkit/__init__.py +5 -0
- crashbytes_testkit/_core.py +146 -0
- crashbytes_testkit/py.typed +0 -0
- crashbytes_testkit-1.0.0.dist-info/METADATA +71 -0
- crashbytes_testkit-1.0.0.dist-info/RECORD +7 -0
- crashbytes_testkit-1.0.0.dist-info/WHEEL +4 -0
- crashbytes_testkit-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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)]
|
|
File without changes
|
|
@@ -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,7 @@
|
|
|
1
|
+
crashbytes_testkit/__init__.py,sha256=1d-qRcn-tNj1OFasUvXwSo9gcAglurlrpvRhdytLH6Q,149
|
|
2
|
+
crashbytes_testkit/_core.py,sha256=eg2qV-5xk96lch0BxKb3DhWYhg23lLVY6QpErnS_XMo,4692
|
|
3
|
+
crashbytes_testkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
crashbytes_testkit-1.0.0.dist-info/METADATA,sha256=21pRAJoRu8sECcESGT_L8PcMGWhwrREkhhxCKwQBFB4,2017
|
|
5
|
+
crashbytes_testkit-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
crashbytes_testkit-1.0.0.dist-info/licenses/LICENSE,sha256=Ic61HOO4EsyXXAGNY-D-1nG62feh_IBbipY1v_QcYcY,1067
|
|
7
|
+
crashbytes_testkit-1.0.0.dist-info/RECORD,,
|
|
@@ -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.
|