iceaxe 0.8.3__cp313-cp313-macosx_11_0_arm64.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.
Potentially problematic release.
This version of iceaxe might be problematic. Click here for more details.
- iceaxe/__init__.py +20 -0
- iceaxe/__tests__/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
- iceaxe/__tests__/benchmarks/test_select.py +114 -0
- iceaxe/__tests__/conf_models.py +133 -0
- iceaxe/__tests__/conftest.py +204 -0
- iceaxe/__tests__/docker_helpers.py +208 -0
- iceaxe/__tests__/helpers.py +268 -0
- iceaxe/__tests__/migrations/__init__.py +0 -0
- iceaxe/__tests__/migrations/conftest.py +36 -0
- iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
- iceaxe/__tests__/migrations/test_generator.py +140 -0
- iceaxe/__tests__/migrations/test_generics.py +91 -0
- iceaxe/__tests__/mountaineer/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
- iceaxe/__tests__/schemas/__init__.py +0 -0
- iceaxe/__tests__/schemas/test_actions.py +1265 -0
- iceaxe/__tests__/schemas/test_cli.py +25 -0
- iceaxe/__tests__/schemas/test_db_memory_serializer.py +1571 -0
- iceaxe/__tests__/schemas/test_db_serializer.py +435 -0
- iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
- iceaxe/__tests__/test_alias.py +83 -0
- iceaxe/__tests__/test_base.py +52 -0
- iceaxe/__tests__/test_comparison.py +383 -0
- iceaxe/__tests__/test_field.py +11 -0
- iceaxe/__tests__/test_helpers.py +9 -0
- iceaxe/__tests__/test_modifications.py +151 -0
- iceaxe/__tests__/test_queries.py +764 -0
- iceaxe/__tests__/test_queries_str.py +173 -0
- iceaxe/__tests__/test_session.py +1511 -0
- iceaxe/__tests__/test_text_search.py +287 -0
- iceaxe/alias_values.py +67 -0
- iceaxe/base.py +351 -0
- iceaxe/comparison.py +560 -0
- iceaxe/field.py +263 -0
- iceaxe/functions.py +1432 -0
- iceaxe/generics.py +140 -0
- iceaxe/io.py +107 -0
- iceaxe/logging.py +91 -0
- iceaxe/migrations/__init__.py +5 -0
- iceaxe/migrations/action_sorter.py +98 -0
- iceaxe/migrations/cli.py +228 -0
- iceaxe/migrations/client_io.py +62 -0
- iceaxe/migrations/generator.py +404 -0
- iceaxe/migrations/migration.py +86 -0
- iceaxe/migrations/migrator.py +101 -0
- iceaxe/modifications.py +176 -0
- iceaxe/mountaineer/__init__.py +10 -0
- iceaxe/mountaineer/cli.py +74 -0
- iceaxe/mountaineer/config.py +46 -0
- iceaxe/mountaineer/dependencies/__init__.py +6 -0
- iceaxe/mountaineer/dependencies/core.py +67 -0
- iceaxe/postgres.py +133 -0
- iceaxe/py.typed +0 -0
- iceaxe/queries.py +1459 -0
- iceaxe/queries_str.py +294 -0
- iceaxe/schemas/__init__.py +0 -0
- iceaxe/schemas/actions.py +864 -0
- iceaxe/schemas/cli.py +30 -0
- iceaxe/schemas/db_memory_serializer.py +711 -0
- iceaxe/schemas/db_serializer.py +347 -0
- iceaxe/schemas/db_stubs.py +529 -0
- iceaxe/session.py +860 -0
- iceaxe/session_optimized.c +12207 -0
- iceaxe/session_optimized.cpython-313-darwin.so +0 -0
- iceaxe/session_optimized.pyx +212 -0
- iceaxe/sql_types.py +149 -0
- iceaxe/typing.py +73 -0
- iceaxe-0.8.3.dist-info/METADATA +262 -0
- iceaxe-0.8.3.dist-info/RECORD +75 -0
- iceaxe-0.8.3.dist-info/WHEEL +6 -0
- iceaxe-0.8.3.dist-info/licenses/LICENSE +21 -0
- iceaxe-0.8.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from iceaxe.migrations.action_sorter import ActionTopologicalSorter
|
|
4
|
+
from iceaxe.schemas.actions import DatabaseActions
|
|
5
|
+
from iceaxe.schemas.db_stubs import DBObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MockNode(DBObject):
|
|
9
|
+
name: str
|
|
10
|
+
table_name: str = "None"
|
|
11
|
+
|
|
12
|
+
model_config = {
|
|
13
|
+
"frozen": True,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def representation(self):
|
|
17
|
+
return f"MockNode({self.name}, {self.table_name})"
|
|
18
|
+
|
|
19
|
+
async def create(self, actor: DatabaseActions):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
async def migrate(self, previous: DBObject, actor: DatabaseActions):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
async def destroy(self, actor: DatabaseActions):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def __hash__(self):
|
|
29
|
+
return hash(self.representation())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def custom_topological_sort(graph_edges):
|
|
33
|
+
sorter = ActionTopologicalSorter(graph_edges)
|
|
34
|
+
sorted_objects = sorter.sort()
|
|
35
|
+
return {obj: i for i, obj in enumerate(sorted_objects)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_simple_dag():
|
|
39
|
+
A = MockNode(name="A")
|
|
40
|
+
B = MockNode(name="B")
|
|
41
|
+
C = MockNode(name="C")
|
|
42
|
+
D = MockNode(name="D")
|
|
43
|
+
graph = {D: [B, C], C: [A], B: [A], A: []}
|
|
44
|
+
result = custom_topological_sort(graph)
|
|
45
|
+
assert list(result.keys()) == [A, C, B, D]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_disconnected_graph():
|
|
49
|
+
A = MockNode(name="A")
|
|
50
|
+
B = MockNode(name="B")
|
|
51
|
+
C = MockNode(name="C")
|
|
52
|
+
D = MockNode(name="D")
|
|
53
|
+
E = MockNode(name="E")
|
|
54
|
+
graph = {B: [A], A: [], D: [C], C: [], E: []}
|
|
55
|
+
result = custom_topological_sort(graph)
|
|
56
|
+
assert set(result.keys()) == {A, B, C, D, E}
|
|
57
|
+
assert result[A] < result[B]
|
|
58
|
+
assert result[C] < result[D]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_single_table_grouping():
|
|
62
|
+
A = MockNode(name="A", table_name="table1")
|
|
63
|
+
B = MockNode(name="B", table_name="table1")
|
|
64
|
+
C = MockNode(name="C", table_name="table1")
|
|
65
|
+
graph = {C: [], B: [C], A: [B]}
|
|
66
|
+
result = custom_topological_sort(graph)
|
|
67
|
+
assert list(result.keys()) == [C, B, A]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_multiple_table_grouping():
|
|
71
|
+
A = MockNode(name="A", table_name="table1")
|
|
72
|
+
B = MockNode(name="B", table_name="table1")
|
|
73
|
+
C = MockNode(name="C", table_name="table2")
|
|
74
|
+
D = MockNode(name="D", table_name="table2")
|
|
75
|
+
E = MockNode(name="E", table_name="table3")
|
|
76
|
+
graph = {E: [], D: [], C: [D, E], A: [C], B: [C]}
|
|
77
|
+
result = custom_topological_sort(graph)
|
|
78
|
+
assert set(result.keys()) == {A, B, C, D, E}
|
|
79
|
+
assert result[C] < result[A] and result[C] < result[B]
|
|
80
|
+
assert result[D] < result[C] and result[E] < result[C]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_cross_table_references():
|
|
84
|
+
A = MockNode(name="A", table_name="table1")
|
|
85
|
+
B = MockNode(name="B", table_name="table2")
|
|
86
|
+
C = MockNode(name="C", table_name="table1")
|
|
87
|
+
D = MockNode(name="D", table_name="table2")
|
|
88
|
+
graph = {D: [], C: [D], B: [C], A: [B]}
|
|
89
|
+
result = custom_topological_sort(graph)
|
|
90
|
+
assert list(result.keys()) == [D, C, B, A]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_nodes_without_table_name():
|
|
94
|
+
A = MockNode(name="A", table_name="table1")
|
|
95
|
+
B = MockNode(name="B")
|
|
96
|
+
C = MockNode(name="C", table_name="table2")
|
|
97
|
+
D = MockNode(name="D")
|
|
98
|
+
graph = {D: [], C: [D], B: [C], A: [B]}
|
|
99
|
+
result = custom_topological_sort(graph)
|
|
100
|
+
assert list(result.keys()) == [D, C, B, A]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_complex_graph():
|
|
104
|
+
A = MockNode(name="A", table_name="table1")
|
|
105
|
+
B = MockNode(name="B", table_name="table1")
|
|
106
|
+
C = MockNode(name="C", table_name="table2")
|
|
107
|
+
D = MockNode(name="D", table_name="table2")
|
|
108
|
+
E = MockNode(name="E", table_name="table3")
|
|
109
|
+
F = MockNode(name="F")
|
|
110
|
+
G = MockNode(name="G", table_name="table3")
|
|
111
|
+
graph = {G: [], F: [G], E: [G], D: [F], C: [E, F], A: [C, D], B: [C]}
|
|
112
|
+
result = custom_topological_sort(graph)
|
|
113
|
+
assert set(result.keys()) == {A, B, C, D, E, F, G}
|
|
114
|
+
assert result[C] < result[A] and result[D] < result[A]
|
|
115
|
+
assert result[C] < result[B]
|
|
116
|
+
assert result[E] < result[C] and result[F] < result[C]
|
|
117
|
+
assert result[F] < result[D]
|
|
118
|
+
assert result[G] < result[E] and result[G] < result[F]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_cyclic_graph():
|
|
122
|
+
A = MockNode(name="A")
|
|
123
|
+
B = MockNode(name="B")
|
|
124
|
+
C = MockNode(name="C")
|
|
125
|
+
graph = {A: [B], B: [C], C: [A]}
|
|
126
|
+
with pytest.raises(ValueError, match="Graph contains a cycle"):
|
|
127
|
+
custom_topological_sort(graph)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_empty_graph():
|
|
131
|
+
graph = {}
|
|
132
|
+
result = custom_topological_sort(graph)
|
|
133
|
+
assert result == {}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_single_node_graph():
|
|
137
|
+
A = MockNode(name="A")
|
|
138
|
+
graph = {A: []}
|
|
139
|
+
result = custom_topological_sort(graph)
|
|
140
|
+
assert result == {A: 0}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_all_nodes_same_table():
|
|
144
|
+
A = MockNode(name="A", table_name="table1")
|
|
145
|
+
B = MockNode(name="B", table_name="table1")
|
|
146
|
+
C = MockNode(name="C", table_name="table1")
|
|
147
|
+
D = MockNode(name="D", table_name="table1")
|
|
148
|
+
graph = {D: [], B: [D], C: [D], A: [B, C]}
|
|
149
|
+
result = custom_topological_sort(graph)
|
|
150
|
+
assert list(result.keys()) == [D, B, C, A]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_mixed_node_types():
|
|
154
|
+
A = MockNode(name="A", table_name="table1")
|
|
155
|
+
B = MockNode(name="B")
|
|
156
|
+
C = MockNode(name="C", table_name="table2")
|
|
157
|
+
D = MockNode(name="D", table_name="table3")
|
|
158
|
+
graph = {D: [], C: [D], B: [C], A: [B]}
|
|
159
|
+
result = custom_topological_sort(graph)
|
|
160
|
+
assert list(result.keys()) == [D, C, B, A]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_large_graph_performance():
|
|
164
|
+
import random
|
|
165
|
+
import string
|
|
166
|
+
import time
|
|
167
|
+
|
|
168
|
+
def generate_large_graph(size):
|
|
169
|
+
nodes = [MockNode(name=c) for c in string.ascii_uppercase] + [
|
|
170
|
+
MockNode(name=f"N{i}") for i in range(size - 26)
|
|
171
|
+
]
|
|
172
|
+
graph = {node: set() for node in nodes}
|
|
173
|
+
for i, node in enumerate(nodes):
|
|
174
|
+
graph[node] = set(random.sample(nodes[i + 1 :], min(5, len(nodes) - i - 1)))
|
|
175
|
+
return graph
|
|
176
|
+
|
|
177
|
+
large_graph = generate_large_graph(1000)
|
|
178
|
+
start_time = time.time()
|
|
179
|
+
result = custom_topological_sort(large_graph)
|
|
180
|
+
end_time = time.time()
|
|
181
|
+
assert len(result) == 1000
|
|
182
|
+
assert end_time - start_time < 5
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_graph_with_isolated_nodes():
|
|
186
|
+
A = MockNode(name="A")
|
|
187
|
+
B = MockNode(name="B")
|
|
188
|
+
C = MockNode(name="C")
|
|
189
|
+
D = MockNode(name="D")
|
|
190
|
+
E = MockNode(name="E")
|
|
191
|
+
graph = {A: [B], B: [], C: [], D: [E], E: []}
|
|
192
|
+
result = custom_topological_sort(graph)
|
|
193
|
+
assert set(result.keys()) == {A, B, C, D, E}
|
|
194
|
+
assert result[B] < result[A]
|
|
195
|
+
assert result[E] < result[D]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@pytest.mark.parametrize(
|
|
199
|
+
"graph, expected_order",
|
|
200
|
+
[
|
|
201
|
+
(
|
|
202
|
+
{
|
|
203
|
+
MockNode(name="C"): set(),
|
|
204
|
+
MockNode(name="B"): {MockNode(name="C")},
|
|
205
|
+
MockNode(name="A"): {MockNode(name="B")},
|
|
206
|
+
},
|
|
207
|
+
["C", "B", "A"],
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
{
|
|
211
|
+
MockNode(name="D"): set(),
|
|
212
|
+
MockNode(name="B"): {MockNode(name="D")},
|
|
213
|
+
MockNode(name="C"): {MockNode(name="D")},
|
|
214
|
+
MockNode(name="A"): {MockNode(name="B"), MockNode(name="C")},
|
|
215
|
+
},
|
|
216
|
+
["D", "B", "C", "A"],
|
|
217
|
+
),
|
|
218
|
+
],
|
|
219
|
+
)
|
|
220
|
+
def test_various_graph_structures(graph, expected_order):
|
|
221
|
+
result = custom_topological_sort(graph)
|
|
222
|
+
assert [node.name for node in result.keys()] == expected_order
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_consistent_results():
|
|
226
|
+
"""
|
|
227
|
+
Test for consistent results with same input
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
A = MockNode(name="A", table_name="table1")
|
|
231
|
+
B = MockNode(name="B", table_name="table1")
|
|
232
|
+
C = MockNode(name="C", table_name="table2")
|
|
233
|
+
D = MockNode(name="D", table_name="table2")
|
|
234
|
+
graph = {D: [], C: [D], B: [D], A: [B, C]}
|
|
235
|
+
result1 = custom_topological_sort(graph)
|
|
236
|
+
result2 = custom_topological_sort(graph)
|
|
237
|
+
assert list(result1.keys()) == list(result2.keys())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from iceaxe.migrations.generator import MigrationGenerator
|
|
9
|
+
from iceaxe.migrations.migration import MigrationRevisionBase
|
|
10
|
+
from iceaxe.schemas.actions import ColumnType, DatabaseActions, DryRunAction
|
|
11
|
+
from iceaxe.schemas.db_memory_serializer import DatabaseMemorySerializer
|
|
12
|
+
from iceaxe.schemas.db_stubs import DBTable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_new_migration():
|
|
17
|
+
migration_generator = MigrationGenerator()
|
|
18
|
+
|
|
19
|
+
code, up_revision = await migration_generator.new_migration(
|
|
20
|
+
down_objects_with_dependencies=[
|
|
21
|
+
(
|
|
22
|
+
DBTable(
|
|
23
|
+
table_name="test_table_a",
|
|
24
|
+
),
|
|
25
|
+
[],
|
|
26
|
+
)
|
|
27
|
+
],
|
|
28
|
+
up_objects_with_dependencies=[
|
|
29
|
+
(
|
|
30
|
+
DBTable(
|
|
31
|
+
table_name="test_table_b",
|
|
32
|
+
),
|
|
33
|
+
[],
|
|
34
|
+
)
|
|
35
|
+
],
|
|
36
|
+
down_revision="test_down_revision",
|
|
37
|
+
user_message="test_user_message",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Expected up
|
|
41
|
+
assert 'await migrator.actor.add_table(table_name="test_table_b")' in code
|
|
42
|
+
assert 'await migrator.actor.drop_table(table_name="test_table_a")' in code
|
|
43
|
+
|
|
44
|
+
# Expected down
|
|
45
|
+
assert 'await migrator.actor.drop_table(table_name="test_table_b")' in code
|
|
46
|
+
assert 'await migrator.actor.add_table(table_name="test_table_a")' in code
|
|
47
|
+
|
|
48
|
+
assert "Context: test_user_message" in code
|
|
49
|
+
assert 'down_revision: str | None = "test_down_revision"' in code
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_actions_to_code():
|
|
53
|
+
actor = DatabaseActions()
|
|
54
|
+
migration_generator = MigrationGenerator()
|
|
55
|
+
|
|
56
|
+
code = migration_generator.actions_to_code(
|
|
57
|
+
[
|
|
58
|
+
DryRunAction(
|
|
59
|
+
fn=actor.add_column,
|
|
60
|
+
kwargs={
|
|
61
|
+
"table_name": "test_table",
|
|
62
|
+
"column_name": "test_column",
|
|
63
|
+
"explicit_data_type": ColumnType.VARCHAR,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
assert code == [
|
|
69
|
+
'await migrator.actor.add_column(table_name="test_table", column_name="test_column", explicit_data_type=ColumnType.VARCHAR)'
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_actions_to_code_pass():
|
|
74
|
+
"""
|
|
75
|
+
We support generating migrations where there are no schema-level changes, so users can
|
|
76
|
+
write their own data migration logic. In these cases we should pass the code-block
|
|
77
|
+
so the resulting file is still legitimate.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
migration_generator = MigrationGenerator()
|
|
81
|
+
code = migration_generator.actions_to_code([])
|
|
82
|
+
assert code == ["pass"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ExampleEnum(Enum):
|
|
86
|
+
A = "a"
|
|
87
|
+
B = "b"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ExampleModel(BaseModel):
|
|
91
|
+
value: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ExampleDataclass:
|
|
96
|
+
value: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize(
|
|
100
|
+
"value, expected_value",
|
|
101
|
+
[
|
|
102
|
+
(ExampleEnum.A, "ExampleEnum.A"),
|
|
103
|
+
("example_arg", '"example_arg"'),
|
|
104
|
+
(1, "1"),
|
|
105
|
+
(
|
|
106
|
+
{"key": "value", "nested": {"key": "value2"}},
|
|
107
|
+
'{"key": "value", "nested": {"key": "value2"}}',
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
{
|
|
111
|
+
"key": ExampleModel(value="test"),
|
|
112
|
+
"enum": ExampleEnum.B,
|
|
113
|
+
},
|
|
114
|
+
'{"key": ExampleModel(value="test"), "enum": ExampleEnum.B}',
|
|
115
|
+
),
|
|
116
|
+
(ExampleModel(value="test"), 'ExampleModel(value="test")'),
|
|
117
|
+
(ExampleDataclass(value="test"), 'ExampleDataclass(value="test")'),
|
|
118
|
+
(True, "True"),
|
|
119
|
+
(False, "False"),
|
|
120
|
+
(frozenset({"A", "B"}), 'frozenset({"A", "B"})'),
|
|
121
|
+
({"A", "B"}, '{"A", "B"}'),
|
|
122
|
+
(("A",), '("A",)'),
|
|
123
|
+
(("A", "B"), '("A", "B")'),
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
def test_format_arg(value: Any, expected_value: str):
|
|
127
|
+
migration_generator = MigrationGenerator()
|
|
128
|
+
assert migration_generator.format_arg(value) == expected_value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_track_import():
|
|
132
|
+
migration_generator = MigrationGenerator()
|
|
133
|
+
|
|
134
|
+
migration_generator.track_import(DatabaseMemorySerializer)
|
|
135
|
+
migration_generator.track_import(MigrationRevisionBase)
|
|
136
|
+
|
|
137
|
+
assert dict(migration_generator.import_tracker) == {
|
|
138
|
+
"iceaxe.migrations.migration": {"MigrationRevisionBase"},
|
|
139
|
+
"iceaxe.schemas.db_memory_serializer": {"DatabaseMemorySerializer"},
|
|
140
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Type, Union
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iceaxe.generics import (
|
|
7
|
+
_is_type_compatible,
|
|
8
|
+
remove_null_type,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SomeEnum(Enum):
|
|
13
|
+
A = "A"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SomeSuperClass:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SomeSubClass(SomeSuperClass):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.parametrize(
|
|
25
|
+
"obj_type, target_type, expected",
|
|
26
|
+
[
|
|
27
|
+
# Basic types
|
|
28
|
+
(int, int, True),
|
|
29
|
+
(str, str, True),
|
|
30
|
+
(int, str, False),
|
|
31
|
+
(int, float, False),
|
|
32
|
+
# Subclasses
|
|
33
|
+
(bool, int, True),
|
|
34
|
+
(int, object, True),
|
|
35
|
+
(SomeSubClass, SomeSuperClass, True),
|
|
36
|
+
# Instance can match classes
|
|
37
|
+
(SomeSubClass(), SomeSuperClass, True),
|
|
38
|
+
([SomeSubClass], list[SomeSuperClass], True),
|
|
39
|
+
# Enums
|
|
40
|
+
(SomeEnum, Type[Enum], True),
|
|
41
|
+
# Unions with new syntax
|
|
42
|
+
(int, Union[int, str], True),
|
|
43
|
+
(str, Union[int, str], True),
|
|
44
|
+
(float, Union[int, str], False),
|
|
45
|
+
# Unions with old syntax using type hints
|
|
46
|
+
(int, Union[int, str], True),
|
|
47
|
+
(str, Union[int, str], True),
|
|
48
|
+
(float, Union[int, str], False),
|
|
49
|
+
# Complex types involving collections
|
|
50
|
+
(list[int], list[int], True),
|
|
51
|
+
(list[int], list[str], False),
|
|
52
|
+
(dict[str, int], dict[str, int], True),
|
|
53
|
+
(dict[str, int], dict[str, str], False),
|
|
54
|
+
# More complex union cases
|
|
55
|
+
(list[int], Union[list[int], dict[str, str]], True),
|
|
56
|
+
(dict[str, str], Union[list[int], dict[str, str]], True),
|
|
57
|
+
(dict[str, float], Union[list[int], dict[str, str]], False),
|
|
58
|
+
(dict[str, str], Union[list[int], dict[str, Any]], True),
|
|
59
|
+
# Pipe operator if Python >= 3.10
|
|
60
|
+
(int, int | str, True),
|
|
61
|
+
(int | str, int | str | float, True),
|
|
62
|
+
(str, int | str, True),
|
|
63
|
+
(float, int | str, False),
|
|
64
|
+
# Nested unions
|
|
65
|
+
(int, list[int | float | str] | int | float | str, True),
|
|
66
|
+
# Optional types
|
|
67
|
+
(None, int | str | None, True),
|
|
68
|
+
(int, int | str | None, True),
|
|
69
|
+
# Value evaluation for sequences
|
|
70
|
+
([1, 2, 3], list[int], True),
|
|
71
|
+
([1, 2, "3"], list[int], False),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
def test_is_type_compatible(obj_type: Any, target_type: Any, expected: bool):
|
|
75
|
+
raw_result = _is_type_compatible(obj_type, target_type)
|
|
76
|
+
bool_result = raw_result != float("inf")
|
|
77
|
+
assert bool_result == expected
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize(
|
|
81
|
+
"typehint, expected",
|
|
82
|
+
[
|
|
83
|
+
(int, int),
|
|
84
|
+
(str, str),
|
|
85
|
+
(int | None, int),
|
|
86
|
+
(str | None, str),
|
|
87
|
+
(Union[int, None], int),
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
def test_remove_null_type(typehint: Any, expected: bool):
|
|
91
|
+
assert remove_null_type(typehint) == expected
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import asyncpg
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iceaxe.mountaineer.config import DatabaseConfig
|
|
7
|
+
from iceaxe.mountaineer.dependencies.core import get_db_connection
|
|
8
|
+
from iceaxe.session import DBConnection
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def mock_db_connect(mock_connection: AsyncMock):
|
|
13
|
+
with patch("asyncpg.connect", new_callable=AsyncMock) as mock:
|
|
14
|
+
mock.return_value = mock_connection
|
|
15
|
+
|
|
16
|
+
yield mock
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_config():
|
|
21
|
+
return DatabaseConfig(
|
|
22
|
+
POSTGRES_HOST="test-host",
|
|
23
|
+
POSTGRES_PORT=5432,
|
|
24
|
+
POSTGRES_USER="test-user",
|
|
25
|
+
POSTGRES_PASSWORD="test-pass",
|
|
26
|
+
POSTGRES_DB="test-db",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def mock_connection():
|
|
32
|
+
conn = AsyncMock(spec=asyncpg.Connection)
|
|
33
|
+
conn.close = AsyncMock()
|
|
34
|
+
|
|
35
|
+
# We need to populate the internal dsn parameters like the real query
|
|
36
|
+
conn._addr = ("test-host", 5432)
|
|
37
|
+
conn._params.user = "test-user"
|
|
38
|
+
conn._params.password = "test-pass"
|
|
39
|
+
conn._params.database = "test-db"
|
|
40
|
+
|
|
41
|
+
conn._introspect_types.return_value = (MagicMock(), MagicMock())
|
|
42
|
+
|
|
43
|
+
return conn
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_get_db_connection_closes_after_yield(
|
|
48
|
+
mock_config: DatabaseConfig,
|
|
49
|
+
mock_connection: AsyncMock,
|
|
50
|
+
mock_db_connect: AsyncMock,
|
|
51
|
+
):
|
|
52
|
+
mock_db_connect.return_value = mock_connection
|
|
53
|
+
|
|
54
|
+
# Get the generator
|
|
55
|
+
db_gen = get_db_connection(mock_config)
|
|
56
|
+
|
|
57
|
+
# Get the connection
|
|
58
|
+
connection = await anext(db_gen) # noqa: F821
|
|
59
|
+
|
|
60
|
+
assert isinstance(connection, DBConnection)
|
|
61
|
+
assert connection.conn == mock_connection
|
|
62
|
+
mock_db_connect.assert_called_once_with(
|
|
63
|
+
host=mock_config.POSTGRES_HOST,
|
|
64
|
+
port=mock_config.POSTGRES_PORT,
|
|
65
|
+
user=mock_config.POSTGRES_USER,
|
|
66
|
+
password=mock_config.POSTGRES_PASSWORD,
|
|
67
|
+
database=mock_config.POSTGRES_DB,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Simulate the end of the generator's scope
|
|
71
|
+
try:
|
|
72
|
+
await db_gen.aclose()
|
|
73
|
+
except StopAsyncIteration:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
mock_connection.close.assert_called_once()
|
|
File without changes
|