Skip to content

Commit 1d4eb73

Browse files
committed
ci: add pre-commit hook to unasync, all working tests
1 parent edd4cd7 commit 1d4eb73

File tree

6 files changed

+118
-65
lines changed

6 files changed

+118
-65
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
repos:
2+
# Unasync: Convert async --> sync
3+
- repo: local
4+
hooks:
5+
- id: unasync
6+
name: unasync
7+
language: python
8+
entry: uv run python build_sync.py
9+
always_run: true
10+
pass_filenames: false
11+
212
# Versioning: Commit messages & changelog
313
- repo: https://github.com/commitizen-tools/commitizen
414
rev: v4.1.0

build_sync.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,79 @@
1+
"""Convery async modules to sync equivalents using tokenisation."""
2+
3+
import subprocess
4+
import sys
5+
from pathlib import Path
6+
17
import unasync
2-
import os
3-
import pathlib
8+
9+
OUTPUT_FILES = ["pg_nearest_city/nearest_city.py", "tests/test_nearest_city.py"]
10+
411

512
def build_sync():
613
"""Transform async code to sync versions with proper import handling."""
714
source_files = [
815
"pg_nearest_city/async_nearest_city.py",
9-
"tests/test_async_nearest_city.py"
16+
"tests/test_async_nearest_city.py",
1017
]
11-
18+
1219
common_replacements = {
1320
# Class and type replacements
1421
"AsyncNearestCity": "NearestCity",
1522
"async_nearest_city": "nearest_city",
1623
"AsyncConnection": "Connection",
1724
"AsyncCursor": "Cursor",
18-
19-
# Test-specific patterns
20-
"import pytest_asyncio": "",
25+
# Test-specific patterns (not working, but not required for now)
2126
"pytest_asyncio": "pytest",
27+
# "@pytest_asyncio": "@pytest",
28+
# "@pytest_asyncio.fixture(loop_scope=\"function\")": "None",
29+
# "@pytest.mark.asyncio(loop_scope=\"function\")": "None",
30+
# "@pytest.mark.asyncio": "",
2231
}
23-
32+
2433
try:
2534
unasync.unasync_files(
2635
source_files,
2736
rules=[
2837
unasync.Rule(
2938
"async_nearest_city.py",
3039
"nearest_city.py",
31-
additional_replacements=common_replacements
40+
additional_replacements=common_replacements,
3241
),
3342
unasync.Rule(
3443
"test_async_nearest_city.py",
3544
"test_nearest_city.py",
36-
additional_replacements=common_replacements
37-
)
38-
]
45+
additional_replacements=common_replacements,
46+
),
47+
],
3948
)
40-
49+
4150
print("Transformation completed!")
4251
# Verify with special focus on import statements
43-
for output_file in ["pg_nearest_city/nearest_city.py", "tests/test_nearest_city.py"]:
44-
if os.path.exists(output_file):
52+
for output_file in OUTPUT_FILES:
53+
if Path(output_file).exists():
4554
print(f"\nSuccessfully created: {output_file}")
4655
else:
4756
print(f"Warning: Expected output file not found: {output_file}")
48-
57+
58+
# Check if the output files were modified
59+
result = subprocess.run(
60+
["git", "diff", "--quiet", "--"] + OUTPUT_FILES, check=False
61+
)
62+
63+
if result.returncode == 1:
64+
print("Files were modified by unasync.")
65+
sys.exit(0) # Allow pre-commit to continue
66+
67+
sys.exit(0) # No changes, allow pre-commit to continue
68+
4969
except Exception as e:
5070
print(f"Error during transformation: {type(e).__name__}: {str(e)}")
5171
import traceback
72+
5273
traceback.print_exc()
74+
# Force failure of pre-commit hook
75+
sys.exit(1)
76+
5377

5478
if __name__ == "__main__":
5579
build_sync()

compose.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ services:
2626
target: ci
2727
container_name: pg-nearest-city
2828
volumes:
29+
# Mount project config
30+
- ./pyproject.toml:/data/pyproject.toml:ro
2931
# Mount local package
30-
- ./pg_nearest_city:/opt/python/lib/python3.10/site-packages/pg_nearest_city
32+
- ./pg_nearest_city:/opt/python/lib/python3.10/site-packages/pg_nearest_city:ro
3133
# Mount local tests
32-
- ./tests:/data/tests
34+
- ./tests:/data/tests:ro
3335
depends_on:
3436
db:
3537
condition: service_healthy

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ target-version = "py310"
5757
exclude = [
5858
".ruff_cache",
5959
"pg_nearest_city/__version__.py",
60+
# Generated files
61+
"pg_nearest_city/nearest_city.py",
62+
"tests/test_nearest_city.py",
6063
]
6164
[tool.ruff.lint]
6265
select = ["I", "E", "W", "D", "B", "F", "N", "Q"]
@@ -69,6 +72,8 @@ addopts = "-ra -q"
6972
testpaths = [
7073
"tests",
7174
]
75+
asyncio_mode="auto"
76+
asyncio_default_fixture_loop_scope="function"
7277

7378
[tool.commitizen]
7479
name = "cz_conventional_commits"

tests/test_async_nearest_city.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Test async geocoder initialization and data file loading."""
22

33
import os
4+
45
import psycopg
56
import pytest
67
import pytest_asyncio
78

8-
from dataclasses import dataclass
99
from pg_nearest_city.async_nearest_city import AsyncNearestCity
10-
from pg_nearest_city.base_nearest_city import Location, InitializationStatus, DbConfig
10+
from pg_nearest_city.base_nearest_city import DbConfig, Location
1111

1212

1313
def get_test_config():
@@ -20,24 +20,25 @@ def get_test_config():
2020
port=int(os.getenv("PGNEAREST_TEST_PORT", "5432")),
2121
)
2222

23-
@pytest_asyncio.fixture(loop_scope="function")
23+
24+
@pytest_asyncio.fixture()
2425
async def test_db():
2526
"""Provide a clean database connection for each test."""
2627
config = get_test_config()
27-
28+
2829
# Create a single connection for the test
2930
conn = await psycopg.AsyncConnection.connect(config.get_connection_string())
30-
31+
3132
# Clean up any existing state
3233
async with conn.cursor() as cur:
3334
await cur.execute("DROP TABLE IF EXISTS pg_nearest_city_geocoding;")
3435
await conn.commit()
35-
36+
3637
yield conn
37-
38+
3839
await conn.close()
3940

40-
@pytest.mark.asyncio(loop_scope="function")
41+
4142
async def test_full_initialization_connect():
4243
"""Test completet database initialization and basic query through connect method."""
4344
async with AsyncNearestCity.connect(get_test_config()) as geocoder:
@@ -47,7 +48,7 @@ async def test_full_initialization_connect():
4748
assert location.city == "New York City"
4849
assert isinstance(location, Location)
4950

50-
@pytest.mark.asyncio
51+
5152
async def test_full_initialization(test_db):
5253
"""Test complete database initialization and basic query."""
5354
geocoder = AsyncNearestCity(test_db)
@@ -59,17 +60,17 @@ async def test_full_initialization(test_db):
5960
assert location.city == "New York City"
6061
assert isinstance(location, Location)
6162

62-
@pytest.mark.asyncio
63+
6364
async def test_check_initialization_fresh_database(test_db):
6465
"""Test initialization check on a fresh database with no tables."""
6566
geocoder = AsyncNearestCity(test_db)
6667
async with test_db.cursor() as cur:
6768
status = await geocoder._check_initialization_status(cur)
68-
69+
6970
assert not status.is_fully_initialized
7071
assert not status.has_table
7172

72-
@pytest.mark.asyncio
73+
7374
async def test_check_initialization_incomplete_table(test_db):
7475
"""Test initialization check with a table that's missing columns."""
7576
geocoder = AsyncNearestCity(test_db)
@@ -82,30 +83,30 @@ async def test_check_initialization_incomplete_table(test_db):
8283
);
8384
""")
8485
await test_db.commit()
85-
86+
8687
status = await geocoder._check_initialization_status(cur)
87-
88+
8889
assert not status.is_fully_initialized
8990
assert status.has_table
9091
assert not status.has_valid_structure
9192

92-
@pytest.mark.asyncio
93+
9394
async def test_check_initialization_empty_table(test_db):
9495
"""Test initialization check with properly structured but empty table."""
9596
geocoder = AsyncNearestCity(test_db)
9697

9798
async with test_db.cursor() as cur:
9899
await geocoder._create_geocoding_table(cur)
99100
await test_db.commit()
100-
101+
101102
status = await geocoder._check_initialization_status(cur)
102-
103+
103104
assert not status.is_fully_initialized
104105
assert status.has_table
105106
assert status.has_valid_structure
106107
assert not status.has_data
107108

108-
@pytest.mark.asyncio
109+
109110
async def test_check_initialization_missing_voronoi(test_db):
110111
"""Test initialization check when Voronoi polygons are missing."""
111112
geocoder = AsyncNearestCity(test_db)
@@ -114,14 +115,14 @@ async def test_check_initialization_missing_voronoi(test_db):
114115
await geocoder._create_geocoding_table(cur)
115116
await geocoder._import_cities(cur)
116117
await test_db.commit()
117-
118+
118119
status = await geocoder._check_initialization_status(cur)
119-
120+
120121
assert not status.is_fully_initialized
121122
assert status.has_data
122123
assert not status.has_complete_voronoi
123124

124-
@pytest.mark.asyncio
125+
125126
async def test_check_initialization_missing_index(test_db):
126127
"""Test initialization check when spatial index is missing."""
127128
geocoder = AsyncNearestCity(test_db)
@@ -131,36 +132,36 @@ async def test_check_initialization_missing_index(test_db):
131132
await geocoder._import_cities(cur)
132133
await geocoder._import_voronoi_polygons(cur)
133134
await test_db.commit()
134-
135+
135136
status = await geocoder._check_initialization_status(cur)
136-
137+
137138
assert not status.is_fully_initialized
138139
assert status.has_data
139140
assert status.has_complete_voronoi
140141
assert not status.has_spatial_index
141142

142-
@pytest.mark.asyncio
143+
143144
async def test_check_initialization_complete(test_db):
144145
"""Test initialization check with a properly initialized database."""
145146
geocoder = AsyncNearestCity(test_db)
146147
await geocoder.initialize()
147148

148149
async with test_db.cursor() as cur:
149150
status = await geocoder._check_initialization_status(cur)
150-
151+
151152
assert status.is_fully_initialized
152153
assert status.has_spatial_index
153154
assert status.has_complete_voronoi
154155
assert status.has_data
155156

156-
@pytest.mark.asyncio
157+
157158
async def test_invalid_coordinates(test_db):
158159
"""Test that invalid coordinates are properly handled."""
159160
geocoder = AsyncNearestCity(test_db)
160161
await geocoder.initialize()
161-
162+
162163
with pytest.raises(ValueError):
163164
await geocoder.query(91, 0) # Invalid latitude
164-
165+
165166
with pytest.raises(ValueError):
166167
await geocoder.query(0, 181) # Invalid longitude

0 commit comments

Comments
 (0)