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,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker helper utilities for testing.
|
|
3
|
+
|
|
4
|
+
This module provides classes and functions to manage Docker containers for testing,
|
|
5
|
+
particularly focusing on PostgreSQL database containers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Any, Dict, Optional, cast
|
|
13
|
+
|
|
14
|
+
import docker
|
|
15
|
+
from docker.errors import APIError
|
|
16
|
+
|
|
17
|
+
# Configure logging
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_free_port() -> int:
|
|
22
|
+
"""Find a free port on the host machine."""
|
|
23
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
24
|
+
s.bind(("", 0))
|
|
25
|
+
return s.getsockname()[1]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PostgresContainer:
|
|
29
|
+
"""
|
|
30
|
+
A class that manages a PostgreSQL Docker container for testing.
|
|
31
|
+
|
|
32
|
+
This class handles the lifecycle of a PostgreSQL container, including:
|
|
33
|
+
- Starting the container with appropriate configuration
|
|
34
|
+
- Finding available ports
|
|
35
|
+
- Waiting for the container to be ready
|
|
36
|
+
- Providing connection information
|
|
37
|
+
- Cleaning up after tests
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
pg_user: str = "iceaxe",
|
|
43
|
+
pg_password: str = "mysecretpassword",
|
|
44
|
+
pg_db: str = "iceaxe_test_db",
|
|
45
|
+
postgres_version: str = "16",
|
|
46
|
+
):
|
|
47
|
+
self.pg_user = pg_user
|
|
48
|
+
self.pg_password = pg_password
|
|
49
|
+
self.pg_db = pg_db
|
|
50
|
+
self.postgres_version = postgres_version
|
|
51
|
+
self.port = get_free_port()
|
|
52
|
+
self.container: Optional[Any] = None
|
|
53
|
+
self.client = docker.from_env()
|
|
54
|
+
self.container_name = f"iceaxe-postgres-test-{uuid.uuid4().hex[:8]}"
|
|
55
|
+
|
|
56
|
+
def start(self) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Start the PostgreSQL container.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dict[str, Any]: Connection information for the PostgreSQL container
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
RuntimeError: If the container fails to start or become ready
|
|
65
|
+
"""
|
|
66
|
+
logger.info(f"Starting PostgreSQL container on port {self.port}")
|
|
67
|
+
|
|
68
|
+
max_attempts = 3
|
|
69
|
+
attempt = 0
|
|
70
|
+
|
|
71
|
+
while attempt < max_attempts:
|
|
72
|
+
attempt += 1
|
|
73
|
+
try:
|
|
74
|
+
self.container = self._run_container(self.port)
|
|
75
|
+
break
|
|
76
|
+
except APIError as e:
|
|
77
|
+
if "port is already allocated" in str(e) and attempt < max_attempts:
|
|
78
|
+
logger.warning(
|
|
79
|
+
f"Port {self.port} is still in use. Trying with a new port (attempt {attempt}/{max_attempts})."
|
|
80
|
+
)
|
|
81
|
+
self.port = get_free_port()
|
|
82
|
+
else:
|
|
83
|
+
raise RuntimeError(f"Failed to start PostgreSQL container: {e}")
|
|
84
|
+
|
|
85
|
+
# Wait for PostgreSQL to be ready
|
|
86
|
+
if not self._wait_for_container_ready():
|
|
87
|
+
self.stop()
|
|
88
|
+
raise RuntimeError("Failed to connect to PostgreSQL container")
|
|
89
|
+
|
|
90
|
+
return self.get_connection_info()
|
|
91
|
+
|
|
92
|
+
def _run_container(
|
|
93
|
+
self, port: int
|
|
94
|
+
) -> Any: # Type as Any since docker.models.containers.Container isn't imported
|
|
95
|
+
"""
|
|
96
|
+
Run the Docker container with the specified port.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
port: The port to map PostgreSQL to on the host
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The Docker container object
|
|
103
|
+
"""
|
|
104
|
+
return self.client.containers.run(
|
|
105
|
+
f"postgres:{self.postgres_version}",
|
|
106
|
+
name=self.container_name,
|
|
107
|
+
detach=True,
|
|
108
|
+
environment={
|
|
109
|
+
"POSTGRES_USER": self.pg_user,
|
|
110
|
+
"POSTGRES_PASSWORD": self.pg_password,
|
|
111
|
+
"POSTGRES_DB": self.pg_db,
|
|
112
|
+
# Additional settings for faster startup in testing
|
|
113
|
+
"POSTGRES_HOST_AUTH_METHOD": "trust",
|
|
114
|
+
},
|
|
115
|
+
ports={"5432/tcp": port},
|
|
116
|
+
remove=True, # Auto-remove container when stopped
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _wait_for_container_ready(self) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Wait for the PostgreSQL container to be ready.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
bool: True if the container is ready, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
max_retries = 30
|
|
127
|
+
retry_interval = 1
|
|
128
|
+
|
|
129
|
+
for i in range(max_retries):
|
|
130
|
+
try:
|
|
131
|
+
if self.container is None:
|
|
132
|
+
logger.warning("Container is None, cannot proceed")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# We've already checked that self.container is not None
|
|
136
|
+
container = cast(Any, self.container)
|
|
137
|
+
container.reload() # Refresh container status
|
|
138
|
+
if container.status != "running":
|
|
139
|
+
logger.warning(f"Container status: {container.status}")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
# Try to connect to PostgreSQL
|
|
143
|
+
conn = socket.create_connection(("localhost", self.port), timeout=1)
|
|
144
|
+
conn.close()
|
|
145
|
+
# Wait a bit more to ensure PostgreSQL is fully initialized
|
|
146
|
+
time.sleep(2)
|
|
147
|
+
logger.info(f"PostgreSQL container is ready after {i + 1} attempt(s)")
|
|
148
|
+
return True
|
|
149
|
+
except (socket.error, ConnectionRefusedError) as e:
|
|
150
|
+
if i == max_retries - 1:
|
|
151
|
+
logger.warning(
|
|
152
|
+
f"Failed to connect after {max_retries} attempts: {e}"
|
|
153
|
+
)
|
|
154
|
+
return False
|
|
155
|
+
time.sleep(retry_interval)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning(f"Unexpected error checking container readiness: {e}")
|
|
158
|
+
if i == max_retries - 1:
|
|
159
|
+
return False
|
|
160
|
+
time.sleep(retry_interval)
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def stop(self) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Stop the PostgreSQL container.
|
|
167
|
+
|
|
168
|
+
This method ensures the container is properly stopped and removed.
|
|
169
|
+
"""
|
|
170
|
+
if self.container is not None:
|
|
171
|
+
try:
|
|
172
|
+
logger.info(f"Stopping PostgreSQL container {self.container_name}")
|
|
173
|
+
# We've already checked that self.container is not None
|
|
174
|
+
container = cast(Any, self.container)
|
|
175
|
+
container.stop(timeout=10) # Allow 10 seconds for graceful shutdown
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning(f"Failed to stop container: {e}")
|
|
178
|
+
try:
|
|
179
|
+
# Force remove as a fallback
|
|
180
|
+
if self.container is not None:
|
|
181
|
+
self.container.remove(force=True)
|
|
182
|
+
logger.info("Forced container removal")
|
|
183
|
+
except Exception as e2:
|
|
184
|
+
logger.warning(f"Failed to force remove container: {e2}")
|
|
185
|
+
|
|
186
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
|
187
|
+
"""
|
|
188
|
+
Get the connection information for the PostgreSQL container.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict[str, Any]: A dictionary containing connection parameters
|
|
192
|
+
"""
|
|
193
|
+
return {
|
|
194
|
+
"host": "localhost",
|
|
195
|
+
"port": self.port,
|
|
196
|
+
"user": self.pg_user,
|
|
197
|
+
"password": self.pg_password,
|
|
198
|
+
"database": self.pg_db,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def get_connection_string(self) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Get a PostgreSQL connection string.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
str: A connection string in the format 'postgresql://user:password@host:port/database'
|
|
207
|
+
"""
|
|
208
|
+
return f"postgresql://{self.pg_user}:{self.pg_password}@localhost:{self.port}/{self.pg_db}"
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from json import JSONDecodeError, dump as json_dump, loads as json_loads
|
|
7
|
+
from re import Pattern
|
|
8
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
9
|
+
from textwrap import dedent
|
|
10
|
+
|
|
11
|
+
from pyright import run
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PyrightDiagnostic:
|
|
16
|
+
file: str
|
|
17
|
+
severity: str
|
|
18
|
+
message: str
|
|
19
|
+
rule: str | None
|
|
20
|
+
line: int
|
|
21
|
+
column: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExpectedPyrightError(Exception):
|
|
25
|
+
"""
|
|
26
|
+
Exception raised when Pyright doesn't produce the expected error
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_imports_from_module(module_source: str) -> set[str]:
|
|
34
|
+
"""
|
|
35
|
+
Extract all import statements from module source
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
tree = ast.parse(module_source)
|
|
39
|
+
imports: set[str] = set()
|
|
40
|
+
|
|
41
|
+
for node in ast.walk(tree):
|
|
42
|
+
if isinstance(node, ast.Import):
|
|
43
|
+
for name in node.names:
|
|
44
|
+
imports.add(f"import {name.name}")
|
|
45
|
+
elif isinstance(node, ast.ImportFrom):
|
|
46
|
+
names = ", ".join(name.name for name in node.names)
|
|
47
|
+
if node.module is None:
|
|
48
|
+
# Handle "from . import x" case
|
|
49
|
+
imports.add(f"from . import {names}")
|
|
50
|
+
else:
|
|
51
|
+
imports.add(f"from {node.module} import {names}")
|
|
52
|
+
|
|
53
|
+
return imports
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def strip_type_ignore(line: str) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Strip type: ignore comments from a line while preserving the line content
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
if "#" not in line:
|
|
62
|
+
return line
|
|
63
|
+
|
|
64
|
+
# Split only on the first #
|
|
65
|
+
code_part, *comment_parts = line.split("#", 1)
|
|
66
|
+
if not comment_parts:
|
|
67
|
+
return line
|
|
68
|
+
|
|
69
|
+
comment = comment_parts[0]
|
|
70
|
+
# If this is a type: ignore comment, return just the code
|
|
71
|
+
if "type:" in comment and "ignore" in comment:
|
|
72
|
+
return code_part.rstrip()
|
|
73
|
+
|
|
74
|
+
# Otherwise return the full line
|
|
75
|
+
return line
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_current_function_code():
|
|
79
|
+
"""
|
|
80
|
+
Extracts the source code of the function calling this utility,
|
|
81
|
+
along with any necessary imports at the module level. This only works for
|
|
82
|
+
functions in a pytest testing context that are prefixed with `test_`.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
# Get the frame of the calling function
|
|
86
|
+
frame = inspect.currentframe()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Go up until we find the test function; workaround to not
|
|
90
|
+
# knowing the entrypoint of our contextmanager at runtime
|
|
91
|
+
while frame is not None:
|
|
92
|
+
func_name = frame.f_code.co_name
|
|
93
|
+
if func_name.startswith("test_"):
|
|
94
|
+
test_frame = frame
|
|
95
|
+
break
|
|
96
|
+
frame = frame.f_back
|
|
97
|
+
else:
|
|
98
|
+
raise RuntimeError("Could not find test function frame")
|
|
99
|
+
|
|
100
|
+
# Source code of the function
|
|
101
|
+
func_source = inspect.getsource(test_frame.f_code)
|
|
102
|
+
|
|
103
|
+
# Source code of the larger test file, which contains the test function
|
|
104
|
+
# All the imports used by the test function should be within this file
|
|
105
|
+
module = inspect.getmodule(test_frame)
|
|
106
|
+
if not module:
|
|
107
|
+
raise RuntimeError("Could not find module for test function")
|
|
108
|
+
|
|
109
|
+
module_source = inspect.getsource(module)
|
|
110
|
+
|
|
111
|
+
# Postprocess the source code to build into a valid new module
|
|
112
|
+
imports = get_imports_from_module(module_source)
|
|
113
|
+
filtered_lines = [strip_type_ignore(line) for line in func_source.split("\n")]
|
|
114
|
+
return "\n".join(sorted(imports)) + "\n\n" + dedent("\n".join(filtered_lines))
|
|
115
|
+
|
|
116
|
+
finally:
|
|
117
|
+
del frame # Avoid reference cycles
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_pyright_config():
|
|
121
|
+
"""
|
|
122
|
+
Creates a new pyright configuration that ignores unused imports or other
|
|
123
|
+
issues that are not related to context-manager wrapped type checking.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
return {
|
|
127
|
+
"include": ["."],
|
|
128
|
+
"exclude": [],
|
|
129
|
+
"ignore": [],
|
|
130
|
+
"strict": [],
|
|
131
|
+
"typeCheckingMode": "strict",
|
|
132
|
+
"reportUnusedImport": False,
|
|
133
|
+
"reportUnusedVariable": False,
|
|
134
|
+
# Focus only on type checking
|
|
135
|
+
"reportOptionalMemberAccess": True,
|
|
136
|
+
"reportGeneralTypeIssues": True,
|
|
137
|
+
"reportPropertyTypeMismatch": True,
|
|
138
|
+
"reportFunctionMemberAccess": True,
|
|
139
|
+
"reportTypeCommentUsage": True,
|
|
140
|
+
"reportMissingTypeStubs": False,
|
|
141
|
+
# Only typehint intentional typehints, not inferred values
|
|
142
|
+
"reportUnknownParameterType": False,
|
|
143
|
+
"reportUnknownVariableType": False,
|
|
144
|
+
"reportUnknownMemberType": False,
|
|
145
|
+
"reportUnknownArgumentType": False,
|
|
146
|
+
"reportMissingParameterType": False,
|
|
147
|
+
"extraPaths": [
|
|
148
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
|
149
|
+
],
|
|
150
|
+
"reportAttributeAccessIssue": True,
|
|
151
|
+
"reportArgumentType": True,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_pyright(file_path: str) -> list[PyrightDiagnostic]:
|
|
156
|
+
"""
|
|
157
|
+
Run pyright on a file and return the diagnostics
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
with TemporaryDirectory() as temp_dir:
|
|
162
|
+
# Create pyright config
|
|
163
|
+
config_path = os.path.join(temp_dir, "pyrightconfig.json")
|
|
164
|
+
with open(config_path, "w") as f:
|
|
165
|
+
json_dump(create_pyright_config(), f)
|
|
166
|
+
|
|
167
|
+
# Copy the file to analyze into the project directory
|
|
168
|
+
test_file = os.path.join(temp_dir, "test.py")
|
|
169
|
+
with open(file_path, "r") as src, open(test_file, "w") as dst:
|
|
170
|
+
dst.write(src.read())
|
|
171
|
+
|
|
172
|
+
# Run pyright with the config
|
|
173
|
+
result = run(
|
|
174
|
+
"--project",
|
|
175
|
+
temp_dir,
|
|
176
|
+
"--outputjson",
|
|
177
|
+
test_file,
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
output = json_loads(result.stdout)
|
|
184
|
+
except JSONDecodeError:
|
|
185
|
+
print(f"Failed to parse pyright output: {result.stdout}") # noqa: T201
|
|
186
|
+
print(f"Stderr: {result.stderr}") # noqa: T201
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
if "generalDiagnostics" not in output:
|
|
190
|
+
raise RuntimeError(
|
|
191
|
+
f"Unknown pyright output, missing generalDiagnostics: {output}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
diagnostics: list[PyrightDiagnostic] = []
|
|
195
|
+
for diag in output["generalDiagnostics"]:
|
|
196
|
+
diagnostics.append(
|
|
197
|
+
PyrightDiagnostic(
|
|
198
|
+
file=diag["file"],
|
|
199
|
+
severity=diag["severity"],
|
|
200
|
+
message=diag["message"],
|
|
201
|
+
rule=diag.get("rule"),
|
|
202
|
+
line=diag["range"]["start"]["line"] + 1, # Convert to 1-based
|
|
203
|
+
column=(
|
|
204
|
+
diag["range"]["start"]["character"]
|
|
205
|
+
+ 1 # Convert to 1-based
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return diagnostics
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise RuntimeError(f"Failed to run pyright: {str(e)}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@contextmanager
|
|
217
|
+
def pyright_raises(
|
|
218
|
+
expected_rule: str,
|
|
219
|
+
expected_line: int | None = None,
|
|
220
|
+
matches: Pattern | None = None,
|
|
221
|
+
):
|
|
222
|
+
"""
|
|
223
|
+
Context manager that verifies code produces a specific Pyright error.
|
|
224
|
+
|
|
225
|
+
:params expected_rule: The Pyright rule that should be violated
|
|
226
|
+
:params expected_line: Optional line number where the error should occur
|
|
227
|
+
|
|
228
|
+
:raises ExpectedPyrightError: If Pyright doesn't produce the expected error
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
# Create a temporary file to store the code
|
|
232
|
+
with NamedTemporaryFile(mode="w", suffix=".py") as temp_file:
|
|
233
|
+
temp_path = temp_file.name
|
|
234
|
+
|
|
235
|
+
# Extract the source code of the calling function
|
|
236
|
+
source_code = extract_current_function_code()
|
|
237
|
+
print(f"Running Pyright on:\n{source_code}") # noqa: T201
|
|
238
|
+
|
|
239
|
+
# Write the source code to the temporary file
|
|
240
|
+
temp_file.write(source_code)
|
|
241
|
+
temp_file.flush()
|
|
242
|
+
|
|
243
|
+
# At runtime, our actual code is probably a no-op but we still let it run
|
|
244
|
+
# inside the scope of the contextmanager
|
|
245
|
+
yield
|
|
246
|
+
|
|
247
|
+
# Run Pyright on the temporary file
|
|
248
|
+
diagnostics = run_pyright(temp_path)
|
|
249
|
+
|
|
250
|
+
# Check if any of the diagnostics match our expected error
|
|
251
|
+
for diagnostic in diagnostics:
|
|
252
|
+
if diagnostic.rule == expected_rule:
|
|
253
|
+
if expected_line is not None and diagnostic.line != expected_line:
|
|
254
|
+
continue
|
|
255
|
+
if matches and not matches.search(diagnostic.message):
|
|
256
|
+
continue
|
|
257
|
+
# Found matching error
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# If we get here, we didn't find the expected error
|
|
261
|
+
actual_errors = [
|
|
262
|
+
f"{d.rule or 'unknown'} on line {d.line}: {d.message}" for d in diagnostics
|
|
263
|
+
]
|
|
264
|
+
raise ExpectedPyrightError(
|
|
265
|
+
f"Expected Pyright error {expected_rule}"
|
|
266
|
+
f"{f' on line {expected_line}' if expected_line else ''}"
|
|
267
|
+
f" but got: {', '.join(actual_errors) if actual_errors else 'no errors'}"
|
|
268
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pytest_asyncio
|
|
2
|
+
|
|
3
|
+
from iceaxe.session import DBConnection
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest_asyncio.fixture
|
|
7
|
+
async def clear_all_database_objects(db_connection: DBConnection):
|
|
8
|
+
"""
|
|
9
|
+
Clear all database objects.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
# Step 1: Drop all tables in the public schema
|
|
13
|
+
await db_connection.conn.execute(
|
|
14
|
+
"""
|
|
15
|
+
DO $$ DECLARE
|
|
16
|
+
r RECORD;
|
|
17
|
+
BEGIN
|
|
18
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
|
|
19
|
+
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
20
|
+
END LOOP;
|
|
21
|
+
END $$;
|
|
22
|
+
"""
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Step 2: Drop all custom types in the public schema
|
|
26
|
+
await db_connection.conn.execute(
|
|
27
|
+
"""
|
|
28
|
+
DO $$ DECLARE
|
|
29
|
+
r RECORD;
|
|
30
|
+
BEGIN
|
|
31
|
+
FOR r IN (SELECT typname FROM pg_type WHERE typtype = 'e' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')) LOOP
|
|
32
|
+
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
|
|
33
|
+
END LOOP;
|
|
34
|
+
END $$;
|
|
35
|
+
"""
|
|
36
|
+
)
|