fix: protect critical re-export imports from linter removal

Problem:
- Ruff removed 'from app.core.database import Base' from models/database/base.py
- Import appeared "unused" (F401) but was actually a critical re-export
- Caused ImportError: cannot import name 'Base' at runtime
- Re-export pattern: import in one file to export from package

Solution:
1. Added F401 ignore for models/database/base.py in pyproject.toml
2. Created scripts/verify_critical_imports.py verification script
3. Integrated verification into make check and CI pipeline
4. Updated documentation with explanation

New Verification Script:
- Checks all critical re-export imports exist
- Detects import variations (parentheses, 'as' clauses)
- Handles SQLAlchemy declarative_base alternatives
- Runs as part of make check automatically

Protected Files:
- models/database/base.py - Re-exports Base for all models
- models/__init__.py - Exports Base for Alembic
- models/database/__init__.py - Exports Base from package
- All __init__.py files (already protected)

Makefile Changes:
- make verify-imports - Run import verification
- make check - Now includes verify-imports
- make ci - Includes verify-imports in pipeline

Documentation Updated:
- Code quality guide explains re-export protection
- Pre-commit workflow includes verification
- Examples of why re-exports matter

This prevents future issues where linters remove seemingly
"unused" imports that are actually critical for application structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 20:10:22 +01:00
parent 4279c458c4
commit b8a46e1746
35 changed files with 200 additions and 64 deletions

View File

@@ -190,9 +190,13 @@ lint-strict:
@echo "Type checking with mypy..." @echo "Type checking with mypy..."
$(PYTHON) -m mypy . $(PYTHON) -m mypy .
check: format lint check: format lint verify-imports
ci: lint-strict test-coverage ci: lint-strict verify-imports test-coverage
verify-imports:
@echo "Verifying critical imports..."
$(PYTHON) scripts/verify_critical_imports.py
qa: format lint test-coverage docs-check qa: format lint test-coverage docs-check
@echo "Quality assurance checks completed!" @echo "Quality assurance checks completed!"
@@ -333,7 +337,8 @@ help:
@echo " format - Format code with ruff" @echo " format - Format code with ruff"
@echo " lint - Lint and auto-fix with ruff + mypy" @echo " lint - Lint and auto-fix with ruff + mypy"
@echo " lint-strict - Lint without auto-fix + mypy" @echo " lint-strict - Lint without auto-fix + mypy"
@echo " check - Format + lint" @echo " verify-imports - Verify critical imports haven't been removed"
@echo " check - Format + lint + verify imports"
@echo " ci - Full CI pipeline (strict)" @echo " ci - Full CI pipeline (strict)"
@echo " qa - Quality assurance" @echo " qa - Quality assurance"
@echo "" @echo ""

View File

@@ -367,9 +367,7 @@ def get_current_customer_from_cookie_or_header(
# Verify token hasn't expired # Verify token hasn't expired
exp = payload.get("exp") exp = payload.get("exp")
if exp and datetime.fromtimestamp(exp, tz=UTC) < datetime.now( if exp and datetime.fromtimestamp(exp, tz=UTC) < datetime.now(UTC):
UTC
):
logger.warning(f"Expired customer token for customer_id={customer_id}") logger.warning(f"Expired customer token for customer_id={customer_id}")
raise InvalidTokenException("Token has expired") raise InvalidTokenException("Token has expired")

View File

@@ -48,9 +48,7 @@ class ContentPageCreate(BaseModel):
meta_description: str | None = Field( meta_description: str | None = Field(
None, max_length=300, description="SEO meta description" None, max_length=300, description="SEO meta description"
) )
meta_keywords: str | None = Field( meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords")
None, max_length=300, description="SEO keywords"
)
is_published: bool = Field(default=False, description="Publish immediately") is_published: bool = Field(default=False, description="Publish immediately")
show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_footer: bool = Field(default=True, description="Show in footer navigation")
show_in_header: bool = Field(default=False, description="Show in header navigation") show_in_header: bool = Field(default=False, description="Show in header navigation")

View File

@@ -88,9 +88,7 @@ def mark_all_as_read(
@router.get("/alerts", response_model=PlatformAlertListResponse) @router.get("/alerts", response_model=PlatformAlertListResponse)
def get_platform_alerts( def get_platform_alerts(
severity: str | None = Query(None, description="Filter by severity"), severity: str | None = Query(None, description="Filter by severity"),
is_resolved: bool | None = Query( is_resolved: bool | None = Query(None, description="Filter by resolution status"),
None, description="Filter by resolution status"
),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),

View File

@@ -30,9 +30,7 @@ def get_product_catalog(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
search: str | None = Query(None, description="Search products by name"), search: str | None = Query(None, description="Search products by name"),
is_featured: bool | None = Query( is_featured: bool | None = Query(None, description="Filter by featured products"),
None, description="Filter by featured products"
),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """

View File

@@ -43,9 +43,7 @@ class VendorContentPageCreate(BaseModel):
meta_description: str | None = Field( meta_description: str | None = Field(
None, max_length=300, description="SEO meta description" None, max_length=300, description="SEO meta description"
) )
meta_keywords: str | None = Field( meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords")
None, max_length=300, description="SEO keywords"
)
is_published: bool = Field(default=False, description="Publish immediately") is_published: bool = Field(default=False, description="Publish immediately")
show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_footer: bool = Field(default=True, description="Show in footer navigation")
show_in_header: bool = Field(default=False, description="Show in header navigation") show_in_header: bool = Field(default=False, description="Show in header navigation")

View File

@@ -13,7 +13,6 @@ Note: Environment detection is handled by app.core.environment module.
This module focuses purely on configuration storage and validation. This module focuses purely on configuration storage and validation.
""" """
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings

View File

@@ -3,7 +3,6 @@
Authentication and authorization specific exceptions. Authentication and authorization specific exceptions.
""" """
from .base import AuthenticationException, AuthorizationException, ConflictException from .base import AuthenticationException, AuthorizationException, ConflictException

View File

@@ -3,7 +3,6 @@
Shopping cart specific exceptions. Shopping cart specific exceptions.
""" """
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException from .base import BusinessLogicException, ResourceNotFoundException, ValidationException

View File

@@ -3,7 +3,6 @@
Order management specific exceptions. Order management specific exceptions.
""" """
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException from .base import BusinessLogicException, ResourceNotFoundException, ValidationException

View File

@@ -3,7 +3,6 @@
Product (vendor catalog) specific exceptions. Product (vendor catalog) specific exceptions.
""" """
from .base import ( from .base import (
BusinessLogicException, BusinessLogicException,
ConflictException, ConflictException,

View File

@@ -3,7 +3,6 @@
Vendor domain management specific exceptions. Vendor domain management specific exceptions.
""" """
from .base import ( from .base import (
BusinessLogicException, BusinessLogicException,
ConflictException, ConflictException,

View File

@@ -30,7 +30,6 @@ Routes:
- GET /code-quality/violations/{violation_id} → Violation details (auth required) - GET /code-quality/violations/{violation_id} → Violation details (auth required)
""" """
from fastapi import APIRouter, Depends, Path, Request from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates

View File

@@ -107,9 +107,7 @@ class ProductService:
) )
if existing: if existing:
raise ProductAlreadyExistsException( raise ProductAlreadyExistsException("Product already exists in catalog")
"Product already exists in catalog"
)
# Create product # Create product
product = Product( product = Product(

View File

@@ -108,9 +108,7 @@ class PriceProcessor:
r"([A-Z]{3})\s*([0-9.,]+)": lambda m: (m.group(2), m.group(1)), r"([A-Z]{3})\s*([0-9.,]+)": lambda m: (m.group(2), m.group(1)),
} }
def parse_price_currency( def parse_price_currency(self, price_str: any) -> tuple[str | None, str | None]:
self, price_str: any
) -> tuple[str | None, str | None]:
""" """
Parse a price string to extract the numeric value and currency. Parse a price string to extract the numeric value and currency.

View File

@@ -190,7 +190,7 @@ show_missing = true
Before committing code: Before committing code:
```bash ```bash
# 1. Format and lint your code # 1. Format, lint, and verify critical imports
make check make check
# 2. Run relevant tests # 2. Run relevant tests
@@ -201,6 +201,25 @@ git add .
git commit -m "your message" git commit -m "your message"
``` ```
### Critical Import Verification
The `make check` command includes a critical import verification step that ensures re-export imports haven't been removed by linters.
**What it checks:**
- `models/database/base.py` - Re-exports Base from app.core.database
- `models/__init__.py` - Exports Base for Alembic
- `models/database/__init__.py` - Exports Base from database package
**Why it matters:**
Linters like Ruff may see these imports as "unused" (F401) because they're re-exported, not directly used. Removing them breaks the application.
**Manual verification:**
```bash
make verify-imports
```
If this fails, imports have been removed and must be restored.
## CI/CD Integration ## CI/CD Integration
For continuous integration: For continuous integration:

View File

@@ -204,9 +204,7 @@ class AuthManager:
raise InvalidTokenException("Token missing expiration") raise InvalidTokenException("Token missing expiration")
# Check if token has expired (additional check beyond jwt.decode) # Check if token has expired (additional check beyond jwt.decode)
if datetime.now(UTC) > datetime.fromtimestamp( if datetime.now(UTC) > datetime.fromtimestamp(exp, tz=UTC):
exp, tz=UTC
):
raise TokenExpiredException() raise TokenExpiredException()
# Validate user identifier claim exists # Validate user identifier claim exists

View File

@@ -141,9 +141,7 @@ class VendorContextManager:
f"[OK] Vendor found via custom domain: {domain}{vendor.name}" f"[OK] Vendor found via custom domain: {domain}{vendor.name}"
) )
return vendor return vendor
logger.warning( logger.warning(f"No active vendor found for custom domain: {domain}")
f"No active vendor found for custom domain: {domain}"
)
return None return None
# Method 2 & 3: Subdomain or path-based lookup # Method 2 & 3: Subdomain or path-based lookup

View File

@@ -1,7 +1,6 @@
# models/database/cart.py # models/database/cart.py
"""Cart item database model.""" """Cart item database model."""
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Float, Float,

View File

@@ -1,4 +1,3 @@
from sqlalchemy import ( from sqlalchemy import (
JSON, JSON,
Boolean, Boolean,

View File

@@ -1,4 +1,3 @@
from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,4 +1,3 @@
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Index, Index,

View File

@@ -3,7 +3,6 @@
Vendor Domain Model - Maps custom domains to vendors Vendor Domain Model - Maps custom domains to vendors
""" """
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
Column, Column,

View File

@@ -3,7 +3,6 @@
Pydantic schemas for shopping cart operations. Pydantic schemas for shopping cart operations.
""" """
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
# ============================================================================ # ============================================================================

View File

@@ -11,9 +11,7 @@ class ProductCreate(BaseModel):
marketplace_product_id: int = Field( marketplace_product_id: int = Field(
..., description="MarketplaceProduct ID to add to vendor catalog" ..., description="MarketplaceProduct ID to add to vendor catalog"
) )
product_id: str | None = Field( product_id: str | None = Field(None, description="Vendor's internal SKU/product ID")
None, description="Vendor's internal SKU/product ID"
)
price: float | None = Field(None, ge=0) price: float | None = Field(None, ge=0)
sale_price: float | None = Field(None, ge=0) sale_price: float | None = Field(None, ge=0)
currency: str | None = None currency: str | None = None

View File

@@ -31,7 +31,6 @@ class RoleCreate(RoleBase):
"""Schema for creating a role.""" """Schema for creating a role."""
class RoleUpdate(BaseModel): class RoleUpdate(BaseModel):
"""Schema for updating a role.""" """Schema for updating a role."""

View File

@@ -100,9 +100,7 @@ class VendorUpdate(BaseModel):
subdomain: str | None = Field(None, min_length=2, max_length=100) subdomain: str | None = Field(None, min_length=2, max_length=100)
# Business Contact Information (Vendor Fields) # Business Contact Information (Vendor Fields)
contact_email: str | None = Field( contact_email: str | None = Field(None, description="Public business contact email")
None, description="Public business contact email"
)
contact_phone: str | None = None contact_phone: str | None = None
website: str | None = None website: str | None = None

View File

@@ -3,7 +3,6 @@
Pydantic schemas for vendor theme operations. Pydantic schemas for vendor theme operations.
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -54,14 +53,10 @@ class VendorThemeUpdate(BaseModel):
theme_name: str | None = Field(None, description="Theme preset name") theme_name: str | None = Field(None, description="Theme preset name")
colors: dict[str, str] | None = Field(None, description="Color scheme") colors: dict[str, str] | None = Field(None, description="Color scheme")
fonts: dict[str, str] | None = Field(None, description="Font settings") fonts: dict[str, str] | None = Field(None, description="Font settings")
branding: dict[str, str | None] | None = Field( branding: dict[str, str | None] | None = Field(None, description="Branding assets")
None, description="Branding assets"
)
layout: dict[str, str] | None = Field(None, description="Layout settings") layout: dict[str, str] | None = Field(None, description="Layout settings")
custom_css: str | None = Field(None, description="Custom CSS rules") custom_css: str | None = Field(None, description="Custom CSS rules")
social_links: dict[str, str] | None = Field( social_links: dict[str, str] | None = Field(None, description="Social media links")
None, description="Social media links"
)
class VendorThemeResponse(BaseModel): class VendorThemeResponse(BaseModel):

View File

@@ -60,8 +60,10 @@ unfixable = []
# Per-file ignores # Per-file ignores
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
# Ignore import violations in __init__.py files # Ignore import violations in __init__.py files (re-exports)
"__init__.py" = ["F401", "F403"] "__init__.py" = ["F401", "F403"]
# Base files that re-export (re-export Base from core.database)
"models/database/base.py" = ["F401"]
# Ignore specific rules in test files # Ignore specific rules in test files
"tests/**/*.py" = ["S101", "PLR2004"] "tests/**/*.py" = ["S101", "PLR2004"]
# Alembic migrations can have longer lines and specific patterns # Alembic migrations can have longer lines and specific patterns

View File

@@ -7,7 +7,6 @@ Usage: python route_diagnostics.py
""" """
def check_route_order(): def check_route_order():
"""Check if routes are registered in the correct order.""" """Check if routes are registered in the correct order."""
print("🔍 Checking FastAPI Route Configuration...\n") print("🔍 Checking FastAPI Route Configuration...\n")

View File

@@ -20,7 +20,6 @@ Requirements:
* Customer: username=customer, password=customer123, vendor_id=1 * Customer: username=customer, password=customer123, vendor_id=1
""" """
import requests import requests
BASE_URL = "http://localhost:8000" BASE_URL = "http://localhost:8000"

View File

@@ -9,7 +9,6 @@ Tests:
- Transfer ownership - Transfer ownership
""" """
import requests import requests
BASE_URL = "http://localhost:8000" BASE_URL = "http://localhost:8000"

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
Verify Critical Imports
========================
Checks that critical imports (re-exports) haven't been removed by linters.
This script verifies that essential import statements exist in key files,
preventing issues where tools like Ruff might remove imports that appear
unused but are actually critical for the application structure.
"""
import sys
from pathlib import Path
# Define critical imports that must exist
# Format: {file_path: [(import_line, description)]}
CRITICAL_IMPORTS: dict[str, list[tuple[str, str]]] = {
"models/database/base.py": [
("from app.core.database import Base", "Re-export Base for all models"),
],
"models/__init__.py": [
("from .database.base import Base", "Export Base for Alembic and models"),
],
"models/database/__init__.py": [
("from .base import Base", "Export Base from database package"),
],
"app/core/database.py": [
(
"from sqlalchemy.ext.declarative import declarative_base",
"SQLAlchemy Base declaration",
),
# Note: Might also use sqlalchemy.orm declarative_base in newer versions
],
}
class ImportVerifier:
"""Verifies critical imports exist in codebase"""
def __init__(self, project_root: Path):
self.project_root = project_root
self.issues: list[str] = []
def verify_all(self) -> bool:
"""Verify all critical imports"""
print("🔍 Verifying critical imports...\n")
all_good = True
for file_path, imports in CRITICAL_IMPORTS.items():
if not self.verify_file(file_path, imports):
all_good = False
return all_good
def verify_file(
self, file_path: str, required_imports: list[tuple[str, str]]
) -> bool:
"""Verify imports in a single file"""
full_path = self.project_root / file_path
if not full_path.exists():
self.issues.append(f"❌ File not found: {file_path}")
print(f"{file_path}: File not found")
return False
content = full_path.read_text()
file_ok = True
for import_line, description in required_imports:
# Check for exact import or variations
if import_line in content:
print(f"{file_path}: {import_line}")
else:
# Check for alternative import formats
alternatives = self._get_import_alternatives(import_line)
found = any(alt in content for alt in alternatives)
if found:
print(f"{file_path}: {import_line} (alternative format)")
else:
self.issues.append(
f"{file_path}: Missing critical import\n"
f" Expected: {import_line}\n"
f" Purpose: {description}"
)
print(f"{file_path}: Missing {import_line}")
file_ok = False
print()
return file_ok
def _get_import_alternatives(self, import_line: str) -> list[str]:
"""Get alternative formats for an import"""
alternatives = [import_line]
# Handle 'from x import y' vs 'from x import (y)'
if "from" in import_line and "import" in import_line:
parts = import_line.split("import")
if len(parts) == 2:
from_part = parts[0].strip()
import_part = parts[1].strip()
# Add parenthesized version
alternatives.append(f"{from_part} import ({import_part})")
# Add version with 'as' clause
alternatives.append(f"{import_line} as")
# Handle declarative_base alternatives (sqlalchemy changes)
if "declarative_base" in import_line:
# Old style
alternatives.append(
"from sqlalchemy.ext.declarative import declarative_base"
)
# New style (SQLAlchemy 1.4+)
alternatives.append("from sqlalchemy.orm import declarative_base")
return alternatives
def print_summary(self):
"""Print summary of verification"""
print("\n" + "=" * 80)
print("📊 CRITICAL IMPORTS VERIFICATION SUMMARY")
print("=" * 80)
if not self.issues:
print("\n✅ All critical imports verified successfully!")
print("\nAll re-export patterns are intact.")
else:
print(f"\n❌ Found {len(self.issues)} issue(s):\n")
for issue in self.issues:
print(issue)
print()
print("💡 RESOLUTION:")
print(" 1. Check if imports were removed by linter (Ruff)")
print(" 2. Add missing imports back to the files")
print(" 3. Update pyproject.toml to ignore F401 for these files")
print(" 4. Run this script again to verify")
print("=" * 80)
def main():
"""Main entry point"""
project_root = Path(__file__).parent.parent
verifier = ImportVerifier(project_root)
success = verifier.verify_all()
verifier.print_summary()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@@ -6,7 +6,6 @@ Tests the complete error handling flow from FastAPI through custom exception han
to ensure proper HTTP status codes, error structures, and client-friendly responses. to ensure proper HTTP status codes, error structures, and client-friendly responses.
""" """
import pytest import pytest

View File

@@ -12,7 +12,6 @@ Tests cover:
- Edge cases and isolation - Edge cases and isolation
""" """
import pytest import pytest
from app.exceptions.base import RateLimitException from app.exceptions.base import RateLimitException