Skip to content

Commit

Permalink
ci: add pre-commit hook to unasync, all working tests
Browse files Browse the repository at this point in the history
  • Loading branch information
spwoodcock committed Feb 8, 2025
1 parent edd4cd7 commit 1d4eb73
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 65 deletions.
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
repos:
# Unasync: Convert async --> sync
- repo: local
hooks:
- id: unasync
name: unasync
language: python
entry: uv run python build_sync.py
always_run: true
pass_filenames: false

# Versioning: Commit messages & changelog
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.1.0
Expand Down
56 changes: 40 additions & 16 deletions build_sync.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,79 @@
"""Convery async modules to sync equivalents using tokenisation."""

import subprocess
import sys
from pathlib import Path

import unasync
import os
import pathlib

OUTPUT_FILES = ["pg_nearest_city/nearest_city.py", "tests/test_nearest_city.py"]


def build_sync():
"""Transform async code to sync versions with proper import handling."""
source_files = [
"pg_nearest_city/async_nearest_city.py",
"tests/test_async_nearest_city.py"
"tests/test_async_nearest_city.py",
]

common_replacements = {
# Class and type replacements
"AsyncNearestCity": "NearestCity",
"async_nearest_city": "nearest_city",
"AsyncConnection": "Connection",
"AsyncCursor": "Cursor",

# Test-specific patterns
"import pytest_asyncio": "",
# Test-specific patterns (not working, but not required for now)
"pytest_asyncio": "pytest",
# "@pytest_asyncio": "@pytest",
# "@pytest_asyncio.fixture(loop_scope=\"function\")": "None",
# "@pytest.mark.asyncio(loop_scope=\"function\")": "None",
# "@pytest.mark.asyncio": "",
}

try:
unasync.unasync_files(
source_files,
rules=[
unasync.Rule(
"async_nearest_city.py",
"nearest_city.py",
additional_replacements=common_replacements
additional_replacements=common_replacements,
),
unasync.Rule(
"test_async_nearest_city.py",
"test_nearest_city.py",
additional_replacements=common_replacements
)
]
additional_replacements=common_replacements,
),
],
)

print("Transformation completed!")
# Verify with special focus on import statements
for output_file in ["pg_nearest_city/nearest_city.py", "tests/test_nearest_city.py"]:
if os.path.exists(output_file):
for output_file in OUTPUT_FILES:
if Path(output_file).exists():
print(f"\nSuccessfully created: {output_file}")
else:
print(f"Warning: Expected output file not found: {output_file}")


# Check if the output files were modified
result = subprocess.run(
["git", "diff", "--quiet", "--"] + OUTPUT_FILES, check=False
)

if result.returncode == 1:
print("Files were modified by unasync.")
sys.exit(0) # Allow pre-commit to continue

sys.exit(0) # No changes, allow pre-commit to continue

except Exception as e:
print(f"Error during transformation: {type(e).__name__}: {str(e)}")
import traceback

traceback.print_exc()
# Force failure of pre-commit hook
sys.exit(1)


if __name__ == "__main__":
build_sync()
6 changes: 4 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ services:
target: ci
container_name: pg-nearest-city
volumes:
# Mount project config
- ./pyproject.toml:/data/pyproject.toml:ro
# Mount local package
- ./pg_nearest_city:/opt/python/lib/python3.10/site-packages/pg_nearest_city
- ./pg_nearest_city:/opt/python/lib/python3.10/site-packages/pg_nearest_city:ro
# Mount local tests
- ./tests:/data/tests
- ./tests:/data/tests:ro
depends_on:
db:
condition: service_healthy
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ target-version = "py310"
exclude = [
".ruff_cache",
"pg_nearest_city/__version__.py",
# Generated files
"pg_nearest_city/nearest_city.py",
"tests/test_nearest_city.py",
]
[tool.ruff.lint]
select = ["I", "E", "W", "D", "B", "F", "N", "Q"]
Expand All @@ -69,6 +72,8 @@ addopts = "-ra -q"
testpaths = [
"tests",
]
asyncio_mode="auto"
asyncio_default_fixture_loop_scope="function"

[tool.commitizen]
name = "cz_conventional_commits"
Expand Down
57 changes: 29 additions & 28 deletions tests/test_async_nearest_city.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Test async geocoder initialization and data file loading."""

import os

import psycopg
import pytest
import pytest_asyncio

from dataclasses import dataclass
from pg_nearest_city.async_nearest_city import AsyncNearestCity
from pg_nearest_city.base_nearest_city import Location, InitializationStatus, DbConfig
from pg_nearest_city.base_nearest_city import DbConfig, Location


def get_test_config():
Expand All @@ -20,24 +20,25 @@ def get_test_config():
port=int(os.getenv("PGNEAREST_TEST_PORT", "5432")),
)

@pytest_asyncio.fixture(loop_scope="function")

@pytest_asyncio.fixture()
async def test_db():
"""Provide a clean database connection for each test."""
config = get_test_config()

# Create a single connection for the test
conn = await psycopg.AsyncConnection.connect(config.get_connection_string())

# Clean up any existing state
async with conn.cursor() as cur:
await cur.execute("DROP TABLE IF EXISTS pg_nearest_city_geocoding;")
await conn.commit()

yield conn

await conn.close()

@pytest.mark.asyncio(loop_scope="function")

async def test_full_initialization_connect():
"""Test completet database initialization and basic query through connect method."""
async with AsyncNearestCity.connect(get_test_config()) as geocoder:
Expand All @@ -47,7 +48,7 @@ async def test_full_initialization_connect():
assert location.city == "New York City"
assert isinstance(location, Location)

@pytest.mark.asyncio

async def test_full_initialization(test_db):
"""Test complete database initialization and basic query."""
geocoder = AsyncNearestCity(test_db)
Expand All @@ -59,17 +60,17 @@ async def test_full_initialization(test_db):
assert location.city == "New York City"
assert isinstance(location, Location)

@pytest.mark.asyncio

async def test_check_initialization_fresh_database(test_db):
"""Test initialization check on a fresh database with no tables."""
geocoder = AsyncNearestCity(test_db)
async with test_db.cursor() as cur:
status = await geocoder._check_initialization_status(cur)

assert not status.is_fully_initialized
assert not status.has_table

@pytest.mark.asyncio

async def test_check_initialization_incomplete_table(test_db):
"""Test initialization check with a table that's missing columns."""
geocoder = AsyncNearestCity(test_db)
Expand All @@ -82,30 +83,30 @@ async def test_check_initialization_incomplete_table(test_db):
);
""")
await test_db.commit()

status = await geocoder._check_initialization_status(cur)

assert not status.is_fully_initialized
assert status.has_table
assert not status.has_valid_structure

@pytest.mark.asyncio

async def test_check_initialization_empty_table(test_db):
"""Test initialization check with properly structured but empty table."""
geocoder = AsyncNearestCity(test_db)

async with test_db.cursor() as cur:
await geocoder._create_geocoding_table(cur)
await test_db.commit()

status = await geocoder._check_initialization_status(cur)

assert not status.is_fully_initialized
assert status.has_table
assert status.has_valid_structure
assert not status.has_data

@pytest.mark.asyncio

async def test_check_initialization_missing_voronoi(test_db):
"""Test initialization check when Voronoi polygons are missing."""
geocoder = AsyncNearestCity(test_db)
Expand All @@ -114,14 +115,14 @@ async def test_check_initialization_missing_voronoi(test_db):
await geocoder._create_geocoding_table(cur)
await geocoder._import_cities(cur)
await test_db.commit()

status = await geocoder._check_initialization_status(cur)

assert not status.is_fully_initialized
assert status.has_data
assert not status.has_complete_voronoi

@pytest.mark.asyncio

async def test_check_initialization_missing_index(test_db):
"""Test initialization check when spatial index is missing."""
geocoder = AsyncNearestCity(test_db)
Expand All @@ -131,36 +132,36 @@ async def test_check_initialization_missing_index(test_db):
await geocoder._import_cities(cur)
await geocoder._import_voronoi_polygons(cur)
await test_db.commit()

status = await geocoder._check_initialization_status(cur)

assert not status.is_fully_initialized
assert status.has_data
assert status.has_complete_voronoi
assert not status.has_spatial_index

@pytest.mark.asyncio

async def test_check_initialization_complete(test_db):
"""Test initialization check with a properly initialized database."""
geocoder = AsyncNearestCity(test_db)
await geocoder.initialize()

async with test_db.cursor() as cur:
status = await geocoder._check_initialization_status(cur)

assert status.is_fully_initialized
assert status.has_spatial_index
assert status.has_complete_voronoi
assert status.has_data

@pytest.mark.asyncio

async def test_invalid_coordinates(test_db):
"""Test that invalid coordinates are properly handled."""
geocoder = AsyncNearestCity(test_db)
await geocoder.initialize()

with pytest.raises(ValueError):
await geocoder.query(91, 0) # Invalid latitude

with pytest.raises(ValueError):
await geocoder.query(0, 181) # Invalid longitude
Loading

0 comments on commit 1d4eb73

Please sign in to comment.