fixturify 0.1.9__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.
Files changed (71) hide show
  1. fixturify/__init__.py +21 -0
  2. fixturify/_utils/__init__.py +7 -0
  3. fixturify/_utils/_constants.py +10 -0
  4. fixturify/_utils/_fixture_discovery.py +165 -0
  5. fixturify/_utils/_path_resolver.py +135 -0
  6. fixturify/http_d/__init__.py +80 -0
  7. fixturify/http_d/_config.py +214 -0
  8. fixturify/http_d/_decorator.py +267 -0
  9. fixturify/http_d/_exceptions.py +153 -0
  10. fixturify/http_d/_fixture_discovery.py +33 -0
  11. fixturify/http_d/_matcher.py +372 -0
  12. fixturify/http_d/_mock_context.py +154 -0
  13. fixturify/http_d/_models.py +205 -0
  14. fixturify/http_d/_patcher.py +524 -0
  15. fixturify/http_d/_player.py +222 -0
  16. fixturify/http_d/_recorder.py +1350 -0
  17. fixturify/http_d/_stubs/__init__.py +8 -0
  18. fixturify/http_d/_stubs/_aiohttp.py +220 -0
  19. fixturify/http_d/_stubs/_connection.py +478 -0
  20. fixturify/http_d/_stubs/_httpcore.py +269 -0
  21. fixturify/http_d/_stubs/_tornado.py +95 -0
  22. fixturify/http_d/_utils.py +194 -0
  23. fixturify/json_assert/__init__.py +13 -0
  24. fixturify/json_assert/_actual_saver.py +67 -0
  25. fixturify/json_assert/_assert.py +173 -0
  26. fixturify/json_assert/_comparator.py +183 -0
  27. fixturify/json_assert/_diff_formatter.py +265 -0
  28. fixturify/json_assert/_normalizer.py +83 -0
  29. fixturify/object_mapper/__init__.py +5 -0
  30. fixturify/object_mapper/_deserializers/__init__.py +19 -0
  31. fixturify/object_mapper/_deserializers/_base.py +186 -0
  32. fixturify/object_mapper/_deserializers/_dataclass.py +52 -0
  33. fixturify/object_mapper/_deserializers/_plain.py +55 -0
  34. fixturify/object_mapper/_deserializers/_pydantic_v1.py +38 -0
  35. fixturify/object_mapper/_deserializers/_pydantic_v2.py +41 -0
  36. fixturify/object_mapper/_deserializers/_sqlalchemy.py +72 -0
  37. fixturify/object_mapper/_deserializers/_sqlmodel.py +43 -0
  38. fixturify/object_mapper/_detectors/__init__.py +5 -0
  39. fixturify/object_mapper/_detectors/_type_detector.py +186 -0
  40. fixturify/object_mapper/_serializers/__init__.py +19 -0
  41. fixturify/object_mapper/_serializers/_base.py +260 -0
  42. fixturify/object_mapper/_serializers/_dataclass.py +55 -0
  43. fixturify/object_mapper/_serializers/_plain.py +49 -0
  44. fixturify/object_mapper/_serializers/_pydantic_v1.py +49 -0
  45. fixturify/object_mapper/_serializers/_pydantic_v2.py +49 -0
  46. fixturify/object_mapper/_serializers/_sqlalchemy.py +70 -0
  47. fixturify/object_mapper/_serializers/_sqlmodel.py +54 -0
  48. fixturify/object_mapper/mapper.py +256 -0
  49. fixturify/read_d/__init__.py +5 -0
  50. fixturify/read_d/_decorator.py +193 -0
  51. fixturify/read_d/_fixture_loader.py +88 -0
  52. fixturify/sql_d/__init__.py +7 -0
  53. fixturify/sql_d/_config.py +30 -0
  54. fixturify/sql_d/_decorator.py +373 -0
  55. fixturify/sql_d/_driver_registry.py +133 -0
  56. fixturify/sql_d/_executor.py +82 -0
  57. fixturify/sql_d/_fixture_discovery.py +55 -0
  58. fixturify/sql_d/_phase.py +10 -0
  59. fixturify/sql_d/_strategies/__init__.py +11 -0
  60. fixturify/sql_d/_strategies/_aiomysql.py +63 -0
  61. fixturify/sql_d/_strategies/_aiosqlite.py +29 -0
  62. fixturify/sql_d/_strategies/_asyncpg.py +34 -0
  63. fixturify/sql_d/_strategies/_base.py +118 -0
  64. fixturify/sql_d/_strategies/_mysql.py +70 -0
  65. fixturify/sql_d/_strategies/_psycopg.py +35 -0
  66. fixturify/sql_d/_strategies/_psycopg2.py +40 -0
  67. fixturify/sql_d/_strategies/_registry.py +109 -0
  68. fixturify/sql_d/_strategies/_sqlite.py +33 -0
  69. fixturify-0.1.9.dist-info/METADATA +122 -0
  70. fixturify-0.1.9.dist-info/RECORD +71 -0
  71. fixturify-0.1.9.dist-info/WHEEL +4 -0
@@ -0,0 +1,70 @@
1
+ """SQLAlchemy model serializer."""
2
+
3
+ from typing import Any
4
+
5
+ from fixturify.object_mapper._serializers._base import _BaseSerializer
6
+
7
+
8
+ class _SQLAlchemySerializer(_BaseSerializer):
9
+ """Serializer for SQLAlchemy models."""
10
+
11
+ def serialize(self, obj: Any) -> dict:
12
+ """
13
+ Serialize a SQLAlchemy model to a dictionary.
14
+
15
+ Args:
16
+ obj: A SQLAlchemy model instance
17
+
18
+ Returns:
19
+ Dictionary representation of the model
20
+ """
21
+ self.reset()
22
+ return self._serialize_with_path(obj, "#")
23
+
24
+ def _serialize_object(self, obj: Any, path: str) -> dict:
25
+ """Serialize a SQLAlchemy model."""
26
+ obj_type = type(obj)
27
+
28
+ # Check if this is a SQLAlchemy model
29
+ if hasattr(obj_type, "__tablename__") and hasattr(obj_type, "__mapper__"):
30
+ return self._serialize_sqlalchemy(obj, path)
31
+
32
+ # Fall back to base implementation for nested non-SQLAlchemy objects
33
+ return super()._serialize_object(obj, path)
34
+
35
+ def _serialize_sqlalchemy(self, obj: Any, path: str) -> dict:
36
+ """Serialize a SQLAlchemy model using its mapper columns."""
37
+ result = {}
38
+
39
+ # Get the mapper for column information
40
+ mapper = getattr(type(obj), "__mapper__", None)
41
+ if mapper is None:
42
+ return result
43
+
44
+ # Iterate over column properties
45
+ for column in mapper.columns:
46
+ column_name = column.key
47
+
48
+ # Skip dunder fields
49
+ if self._is_dunder(column_name):
50
+ continue
51
+
52
+ value = getattr(obj, column_name, None)
53
+ item_path = f"{path}/{column_name}"
54
+ result[column_name] = self._serialize_value(value, item_path)
55
+
56
+ # Also handle relationships if loaded (not lazy)
57
+ for relationship in mapper.relationships:
58
+ rel_name = relationship.key
59
+
60
+ # Skip dunder fields
61
+ if self._is_dunder(rel_name):
62
+ continue
63
+
64
+ # Check if the relationship is loaded
65
+ if rel_name in obj.__dict__:
66
+ value = getattr(obj, rel_name, None)
67
+ item_path = f"{path}/{rel_name}"
68
+ result[rel_name] = self._serialize_value(value, item_path)
69
+
70
+ return result
@@ -0,0 +1,54 @@
1
+ """SQLModel model serializer."""
2
+
3
+ from typing import Any
4
+
5
+ from fixturify.object_mapper._serializers._base import _BaseSerializer
6
+
7
+
8
+ class _SQLModelSerializer(_BaseSerializer):
9
+ """Serializer for SQLModel models."""
10
+
11
+ def serialize(self, obj: Any) -> dict:
12
+ """
13
+ Serialize a SQLModel model to a dictionary.
14
+
15
+ SQLModel combines Pydantic v2 and SQLAlchemy, so we use
16
+ Pydantic's model_fields for serialization.
17
+
18
+ Args:
19
+ obj: A SQLModel model instance
20
+
21
+ Returns:
22
+ Dictionary representation of the model
23
+ """
24
+ self.reset()
25
+ return self._serialize_with_path(obj, "#")
26
+
27
+ def _serialize_object(self, obj: Any, path: str) -> dict:
28
+ """Serialize a SQLModel model."""
29
+ obj_type = type(obj)
30
+
31
+ # Check if this is a SQLModel model (has both __tablename__ and model_fields)
32
+ if hasattr(obj_type, "__tablename__") and hasattr(obj_type, "model_fields"):
33
+ return self._serialize_sqlmodel(obj, path)
34
+
35
+ # Fall back to base implementation for nested non-SQLModel objects
36
+ return super()._serialize_object(obj, path)
37
+
38
+ def _serialize_sqlmodel(self, obj: Any, path: str) -> dict:
39
+ """Serialize a SQLModel using its model_fields (Pydantic v2 style)."""
40
+ result = {}
41
+
42
+ # Get fields from model_fields (Pydantic v2)
43
+ model_fields = getattr(type(obj), "model_fields", {})
44
+
45
+ for field_name in model_fields:
46
+ # Skip dunder fields
47
+ if self._is_dunder(field_name):
48
+ continue
49
+
50
+ value = getattr(obj, field_name)
51
+ item_path = f"{path}/{field_name}"
52
+ result[field_name] = self._serialize_value(value, item_path)
53
+
54
+ return result
@@ -0,0 +1,256 @@
1
+ """Main ObjectMapper class for bidirectional object-to-JSON mapping."""
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Any, List, Optional, Type, TypeVar, Union
6
+
7
+ from fixturify.object_mapper._detectors import _TypeDetector, ObjectType
8
+ from fixturify.object_mapper._serializers import (
9
+ _BaseSerializer,
10
+ _DataclassSerializer,
11
+ _PlainObjectSerializer,
12
+ _PydanticV1Serializer,
13
+ _PydanticV2Serializer,
14
+ _SQLAlchemySerializer,
15
+ _SQLModelSerializer,
16
+ )
17
+ from fixturify.object_mapper._deserializers import (
18
+ _DataclassDeserializer,
19
+ _PlainObjectDeserializer,
20
+ _PydanticV1Deserializer,
21
+ _PydanticV2Deserializer,
22
+ _SQLAlchemyDeserializer,
23
+ _SQLModelDeserializer,
24
+ )
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ class ObjectMapper:
30
+ """
31
+ Universal object-to-JSON mapper supporting multiple object types.
32
+
33
+ Supports:
34
+ - Plain Python objects
35
+ - Dataclasses
36
+ - Pydantic v1 models
37
+ - Pydantic v2 models
38
+ - SQLAlchemy models
39
+ - SQLModel models
40
+ - Collections (list, tuple, set)
41
+ - Dictionaries
42
+ """
43
+
44
+ # Serializer instances (lazy-loaded)
45
+ _serializers = {
46
+ ObjectType.DATACLASS: _DataclassSerializer,
47
+ ObjectType.PYDANTIC_V1: _PydanticV1Serializer,
48
+ ObjectType.PYDANTIC_V2: _PydanticV2Serializer,
49
+ ObjectType.SQLALCHEMY: _SQLAlchemySerializer,
50
+ ObjectType.SQLMODEL: _SQLModelSerializer,
51
+ ObjectType.PLAIN_OBJECT: _PlainObjectSerializer,
52
+ }
53
+
54
+ # Deserializer instances (lazy-loaded)
55
+ _deserializers = {
56
+ ObjectType.DATACLASS: _DataclassDeserializer,
57
+ ObjectType.PYDANTIC_V1: _PydanticV1Deserializer,
58
+ ObjectType.PYDANTIC_V2: _PydanticV2Deserializer,
59
+ ObjectType.SQLALCHEMY: _SQLAlchemyDeserializer,
60
+ ObjectType.SQLMODEL: _SQLModelDeserializer,
61
+ ObjectType.PLAIN_OBJECT: _PlainObjectDeserializer,
62
+ }
63
+
64
+ # Flag to track if serializer registry has been set
65
+ _registry_initialized = False
66
+
67
+ @classmethod
68
+ def _ensure_registry_initialized(cls) -> None:
69
+ """Initialize the serializer registry for nested object routing."""
70
+ if not cls._registry_initialized:
71
+ _BaseSerializer.set_serializer_registry(cls._serializers)
72
+ cls._registry_initialized = True
73
+
74
+ def __init__(self, data: Any) -> None:
75
+ """
76
+ Initialize ObjectMapper with data.
77
+
78
+ Args:
79
+ data: Any data - object, collection, dict, or JSON string
80
+ """
81
+ # Ensure serializer registry is initialized for nested type routing
82
+ ObjectMapper._ensure_registry_initialized()
83
+
84
+ self._data = data
85
+ self._data_type = _TypeDetector.detect(data)
86
+
87
+ def to_json(self) -> Any:
88
+ """
89
+ Convert stored object/collection to JSON-compatible value.
90
+
91
+ Returns:
92
+ JSON-compatible value: dict, list, or primitive (str, int, float, bool, None)
93
+
94
+ Raises:
95
+ ValueError: If serialization fails
96
+ """
97
+ try:
98
+ return self._serialize(self._data)
99
+ except Exception as e:
100
+ raise ValueError(f"Serialization failed: {e}") from e
101
+
102
+ def to_json_string(self, indent: Optional[int] = None) -> str:
103
+ """
104
+ Convert stored object/collection to JSON string.
105
+
106
+ Args:
107
+ indent: Optional indentation level for pretty printing
108
+
109
+ Returns:
110
+ JSON string representation
111
+
112
+ Raises:
113
+ ValueError: If serialization fails
114
+ """
115
+ try:
116
+ serialized = self._serialize(self._data)
117
+ return json.dumps(serialized, indent=indent, ensure_ascii=False)
118
+ except Exception as e:
119
+ raise ValueError(f"Serialization failed: {e}") from e
120
+
121
+ def to_object(self, target_class: Type[T]) -> Union[T, List[T]]:
122
+ """
123
+ Convert stored JSON/dict to object or list of objects.
124
+
125
+ Args:
126
+ target_class: The class to deserialize into
127
+
128
+ Returns:
129
+ Instance of target_class, or list of instances if input is a list
130
+
131
+ Raises:
132
+ ValueError: If deserialization fails
133
+ TypeError: If target_class is unsupported
134
+ """
135
+ try:
136
+ # Parse JSON string if needed
137
+ if self._data_type == ObjectType.JSON_STRING:
138
+ data = json.loads(self._data)
139
+ # If parsed data is a primitive (e.g., string for enum), handle it
140
+ if not isinstance(data, (dict, list)):
141
+ return self._deserialize_primitive(data, target_class)
142
+ elif self._data_type == ObjectType.DICT:
143
+ data = self._data
144
+ elif self._data_type == ObjectType.COLLECTION:
145
+ # If it's already a collection, use it directly
146
+ data = list(self._data)
147
+ elif self._data_type == ObjectType.PRIMITIVE:
148
+ # Handle primitive types (for Enum deserialization)
149
+ return self._deserialize_primitive(self._data, target_class)
150
+ else:
151
+ raise ValueError(
152
+ f"to_object() requires JSON string, dict, or collection. "
153
+ f"Got: {self._data_type}"
154
+ )
155
+
156
+ # Handle list of objects
157
+ if isinstance(data, list):
158
+ return self._deserialize_list(data, target_class)
159
+
160
+ # Handle single object
161
+ return self._deserialize(data, target_class)
162
+
163
+ except Exception as e:
164
+ if isinstance(e, (ValueError, TypeError)):
165
+ raise
166
+ raise ValueError(f"Deserialization failed: {e}") from e
167
+
168
+ def _serialize(self, data: Any) -> Any:
169
+ """
170
+ Serialize data to a JSON-compatible format.
171
+
172
+ Uses a single serializer instance for the entire serialization to ensure
173
+ shared reference tracking (via $ref) works correctly across all items
174
+ in root-level dicts and collections.
175
+
176
+ Args:
177
+ data: Data to serialize
178
+
179
+ Returns:
180
+ JSON-compatible dict, list, or primitive
181
+ """
182
+ data_type = _TypeDetector.detect(data)
183
+
184
+ # Handle primitives directly (no need for serializer)
185
+ if data_type == ObjectType.PRIMITIVE:
186
+ if isinstance(data, Enum):
187
+ return data.value
188
+ return data
189
+
190
+ # Handle JSON string (return parsed)
191
+ if data_type == ObjectType.JSON_STRING:
192
+ return json.loads(data)
193
+
194
+ # For dicts, collections, and objects, use a single serializer instance
195
+ # to maintain shared visited state across the entire serialization tree.
196
+ # This ensures $ref works correctly for shared objects in root collections/dicts.
197
+ serializer = _PlainObjectSerializer()
198
+ serializer.reset()
199
+ return serializer._serialize_value(data, "#")
200
+
201
+ def _deserialize(self, data: dict, target_class: Type[T]) -> T:
202
+ """
203
+ Deserialize a dict to an object.
204
+
205
+ Args:
206
+ data: Dictionary data
207
+ target_class: Target class
208
+
209
+ Returns:
210
+ Instance of target_class
211
+ """
212
+ # Detect target class type
213
+ class_type = _TypeDetector.detect_class(target_class)
214
+
215
+ # Get appropriate deserializer
216
+ deserializer_class = self._deserializers.get(class_type)
217
+ if deserializer_class is None:
218
+ raise TypeError(f"Unsupported target class type: {class_type}")
219
+
220
+ deserializer = deserializer_class()
221
+ return deserializer.deserialize(data, target_class)
222
+
223
+ def _deserialize_list(self, data: list, target_class: Type[T]) -> List[T]:
224
+ """
225
+ Deserialize a list to a list of objects.
226
+
227
+ Args:
228
+ data: List of items (dicts for objects, or primitives for Enums)
229
+ target_class: Target class for each item
230
+
231
+ Returns:
232
+ List of target_class instances
233
+ """
234
+ result = []
235
+ for item in data:
236
+ if isinstance(item, dict):
237
+ result.append(self._deserialize(item, target_class))
238
+ else:
239
+ # Handle primitives (e.g., list of Enums or simple values)
240
+ result.append(self._deserialize_primitive(item, target_class))
241
+ return result
242
+
243
+ def _deserialize_primitive(self, data: Any, target_class: Type[T]) -> T:
244
+ """
245
+ Deserialize a primitive value to a target class (e.g., Enum).
246
+
247
+ Args:
248
+ data: Primitive value
249
+ target_class: Target class (e.g., Enum subclass)
250
+
251
+ Returns:
252
+ Instance of target_class
253
+ """
254
+ if isinstance(target_class, type) and issubclass(target_class, Enum):
255
+ return target_class(data)
256
+ return data
@@ -0,0 +1,5 @@
1
+ """Read decorator module for injecting file-based test data."""
2
+
3
+ from fixturify.read_d._decorator import read
4
+
5
+ __all__ = ["read"]
@@ -0,0 +1,193 @@
1
+ """Read decorator implementation for injecting test fixtures."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import inspect
6
+ from typing import Callable, Optional, Type, TypeVar, List
7
+
8
+ from fixturify.read_d._fixture_loader import _FixtureLoader
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ def _unwrap_func(func: Callable) -> Callable:
14
+ """Return the original function by walking the __wrapped__ chain."""
15
+ original = func
16
+ while hasattr(original, "__wrapped__"):
17
+ original = original.__wrapped__
18
+ return original
19
+
20
+
21
+ def _get_base_signature(func: Callable) -> inspect.Signature:
22
+ """Return the most accurate signature, honoring custom __signature__."""
23
+ if hasattr(func, "__signature__"):
24
+ return func.__signature__ # type: ignore
25
+ return inspect.signature(func)
26
+
27
+
28
+ def _create_new_signature(
29
+ original_sig: inspect.Signature, fixture_names: List[str]
30
+ ) -> inspect.Signature:
31
+ """
32
+ Create a new signature that excludes the fixture parameter names.
33
+
34
+ This prevents pytest from trying to inject these parameters as pytest fixtures.
35
+ """
36
+ new_params = [
37
+ param
38
+ for param in original_sig.parameters.values()
39
+ if param.name not in fixture_names
40
+ ]
41
+ return original_sig.replace(parameters=new_params)
42
+
43
+
44
+ class read:
45
+ """
46
+ Namespace class for read decorators.
47
+
48
+ Provides the @read.fixture() decorator for injecting JSON file data
49
+ into test functions.
50
+ """
51
+
52
+ @staticmethod
53
+ def fixture(
54
+ path: str, fixture_name: str, object_class: Optional[Type[T]] = None
55
+ ) -> Callable:
56
+ """
57
+ Decorator that injects a JSON file as a typed object into a test function.
58
+
59
+ This decorator loads JSON data from a file and injects it as a named
60
+ parameter into the decorated function. The path is resolved relative
61
+ to the test file's location.
62
+
63
+ Args:
64
+ path: Relative path to JSON file (from test file location)
65
+ fixture_name: Name of the parameter to inject
66
+ object_class: Optional class to deserialize JSON into (via ObjectMapper).
67
+ If None, returns raw dict/list from JSON.
68
+
69
+ Returns:
70
+ Decorated function with fixture injected
71
+
72
+ Example:
73
+ @read.fixture(path="./fixtures/user.json", fixture_name="user", object_class=User)
74
+ def test_user_creation(user: User):
75
+ assert user.name == "John"
76
+
77
+ Note:
78
+ This is NOT a pytest fixture. It's a pure Python function wrapper
79
+ that works with any test framework (pytest, unittest, nose, etc.).
80
+ """
81
+
82
+ def decorator(func: Callable) -> Callable:
83
+ # Get the test file path at decoration time
84
+ # If the function is already wrapped by another @read.fixture,
85
+ # use the stored test file path from the first decorator
86
+ if hasattr(func, "__pytools_test_file__"):
87
+ test_file_path = func.__pytools_test_file__
88
+ else:
89
+ test_file_path = inspect.getfile(_unwrap_func(func))
90
+
91
+ # Get the original function's signature for validation
92
+ original_func = _unwrap_func(func)
93
+ original_sig = inspect.signature(original_func)
94
+ original_params = set(original_sig.parameters.keys())
95
+
96
+ # Check if function accepts **kwargs (VAR_KEYWORD)
97
+ has_var_keyword = any(
98
+ param.kind == inspect.Parameter.VAR_KEYWORD
99
+ for param in original_sig.parameters.values()
100
+ )
101
+
102
+ # Validate that fixture_name exists in the function signature
103
+ # (excluding 'self' for methods, and skip if **kwargs is present)
104
+ if not has_var_keyword:
105
+ params_to_check = original_params - {"self", "cls"}
106
+ if fixture_name not in params_to_check:
107
+ raise ValueError(
108
+ f"Fixture name '{fixture_name}' not found in function '{func.__name__}' signature. "
109
+ f"Available parameters: {', '.join(sorted(params_to_check)) or 'none'}"
110
+ )
111
+
112
+ # Get or create fixtures list on the function
113
+ if not hasattr(func, "__pytools_fixtures__"):
114
+ func.__pytools_fixtures__ = []
115
+
116
+ # Check for duplicate fixture names across stacked decorators
117
+ existing_names = [
118
+ loader.fixture_name
119
+ for loader in getattr(func, "__pytools_fixtures__", [])
120
+ ]
121
+ if fixture_name in existing_names:
122
+ raise ValueError(
123
+ f"Duplicate fixture name '{fixture_name}' in decorators "
124
+ f"for function '{func.__name__}'"
125
+ )
126
+
127
+ # Create and add the fixture loader
128
+ loader = _FixtureLoader(
129
+ path=path,
130
+ fixture_name=fixture_name,
131
+ object_class=object_class,
132
+ test_file_path=test_file_path,
133
+ )
134
+ func.__pytools_fixtures__.append(loader)
135
+
136
+ # Get all fixture names for signature modification
137
+ all_fixture_names = [ldr.fixture_name for ldr in func.__pytools_fixtures__]
138
+
139
+ # Check if the function is async
140
+ if asyncio.iscoroutinefunction(func):
141
+
142
+ @functools.wraps(func)
143
+ async def async_wrapper(*args, **kwargs):
144
+ request = kwargs.pop("request", None)
145
+ if request is None:
146
+ request = kwargs.pop("_pytools_request", None)
147
+ if getattr(func, "__pytools_wrapper__", False) and request is not None:
148
+ kwargs["_pytools_request"] = request
149
+
150
+ # Load all fixtures (sync operation)
151
+ for fixture_loader in async_wrapper.__pytools_fixtures__:
152
+ kwargs[fixture_loader.fixture_name] = fixture_loader.load()
153
+ return await func(*args, **kwargs)
154
+
155
+ async_wrapper.__pytools_fixtures__ = func.__pytools_fixtures__
156
+ async_wrapper.__pytools_test_file__ = test_file_path
157
+ async_wrapper.__pytools_wrapper__ = True # type: ignore
158
+
159
+ # Modify signature to exclude fixture parameters
160
+ base_sig = _get_base_signature(func)
161
+ async_wrapper.__signature__ = _create_new_signature(
162
+ base_sig, all_fixture_names
163
+ )
164
+
165
+ return async_wrapper
166
+ else:
167
+
168
+ @functools.wraps(func)
169
+ def sync_wrapper(*args, **kwargs):
170
+ request = kwargs.pop("request", None)
171
+ if request is None:
172
+ request = kwargs.pop("_pytools_request", None)
173
+ if getattr(func, "__pytools_wrapper__", False) and request is not None:
174
+ kwargs["_pytools_request"] = request
175
+
176
+ # Load all fixtures
177
+ for fixture_loader in sync_wrapper.__pytools_fixtures__:
178
+ kwargs[fixture_loader.fixture_name] = fixture_loader.load()
179
+ return func(*args, **kwargs)
180
+
181
+ sync_wrapper.__pytools_fixtures__ = func.__pytools_fixtures__
182
+ sync_wrapper.__pytools_test_file__ = test_file_path
183
+ sync_wrapper.__pytools_wrapper__ = True # type: ignore
184
+
185
+ # Modify signature to exclude fixture parameters
186
+ base_sig = _get_base_signature(func)
187
+ sync_wrapper.__signature__ = _create_new_signature(
188
+ base_sig, all_fixture_names
189
+ )
190
+
191
+ return sync_wrapper
192
+
193
+ return decorator
@@ -0,0 +1,88 @@
1
+ """Fixture loading logic for reading JSON files and mapping to objects."""
2
+
3
+ import json
4
+ from typing import Optional, Type, TypeVar, Union, List
5
+
6
+ from fixturify._utils._constants import ENCODING
7
+ from fixturify._utils._path_resolver import _PathResolver
8
+ from fixturify.object_mapper import ObjectMapper
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class _FixtureLoader:
14
+ """
15
+ Loads JSON fixtures and optionally maps them to objects.
16
+
17
+ Handles file reading, JSON parsing, and object conversion.
18
+ Each call to load() reads the file fresh (no caching).
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ path: str,
24
+ fixture_name: str,
25
+ object_class: Optional[Type[T]],
26
+ test_file_path: str,
27
+ ):
28
+ """
29
+ Initialize the fixture loader.
30
+
31
+ Args:
32
+ path: Relative path to the JSON file
33
+ fixture_name: Name of the parameter to inject
34
+ object_class: Optional class to deserialize JSON into
35
+ test_file_path: Absolute path to the test file (for path resolution)
36
+ """
37
+ self._path = path
38
+ self._fixture_name = fixture_name
39
+ self._object_class = object_class
40
+ self._test_file_path = test_file_path
41
+
42
+ @property
43
+ def fixture_name(self) -> str:
44
+ """Return the fixture name for parameter injection."""
45
+ return self._fixture_name
46
+
47
+ def load(self) -> Union[T, List[T], dict, list]:
48
+ """
49
+ Load and optionally map the fixture.
50
+
51
+ Each call reads the file fresh from disk.
52
+
53
+ Returns:
54
+ The loaded fixture data, either as raw dict/list or mapped object(s)
55
+
56
+ Raises:
57
+ FileNotFoundError: If the JSON file doesn't exist
58
+ ValueError: If JSON parsing or object mapping fails
59
+ """
60
+ # Resolve the path relative to the test file
61
+ try:
62
+ resolved_path = _PathResolver.resolve(self._path, self._test_file_path)
63
+ except FileNotFoundError as e:
64
+ raise FileNotFoundError(
65
+ f"Fixture file not found: {self._path} "
66
+ f"(resolved from test file: {self._test_file_path})"
67
+ ) from e
68
+
69
+ # Read and parse the JSON file
70
+ try:
71
+ content = resolved_path.read_text(encoding=ENCODING)
72
+ data = json.loads(content)
73
+ except json.JSONDecodeError as e:
74
+ raise ValueError(
75
+ f"Invalid JSON in fixture file {resolved_path}: {e}"
76
+ ) from e
77
+
78
+ # If no object_class specified, return raw dict/list
79
+ if self._object_class is None:
80
+ return data
81
+
82
+ # Map to object using ObjectMapper
83
+ try:
84
+ return ObjectMapper(data).to_object(self._object_class)
85
+ except Exception as e:
86
+ raise ValueError(
87
+ f"Failed to convert fixture to {self._object_class.__name__}: {e}"
88
+ ) from e
@@ -0,0 +1,7 @@
1
+ """SQL module for executing SQL files before/after tests."""
2
+
3
+ from fixturify.sql_d._config import SqlTestConfig
4
+ from fixturify.sql_d._decorator import sql
5
+ from fixturify.sql_d._phase import Phase
6
+
7
+ __all__ = ["sql", "Phase", "SqlTestConfig"]