diff --git a/Makefile b/Makefile index 2e174aca..7b8a64d9 100644 --- a/Makefile +++ b/Makefile @@ -190,9 +190,13 @@ lint-strict: @echo "Type checking with 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 @echo "Quality assurance checks completed!" @@ -333,7 +337,8 @@ help: @echo " format - Format code with ruff" @echo " lint - Lint and auto-fix with ruff + 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 " qa - Quality assurance" @echo "" diff --git a/app/api/deps.py b/app/api/deps.py index 3b2413fc..4036294b 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -367,9 +367,7 @@ def get_current_customer_from_cookie_or_header( # Verify token hasn't expired exp = payload.get("exp") - if exp and datetime.fromtimestamp(exp, tz=UTC) < datetime.now( - UTC - ): + if exp and datetime.fromtimestamp(exp, tz=UTC) < datetime.now(UTC): logger.warning(f"Expired customer token for customer_id={customer_id}") raise InvalidTokenException("Token has expired") diff --git a/app/api/v1/admin/content_pages.py b/app/api/v1/admin/content_pages.py index 653c8397..6305a7b7 100644 --- a/app/api/v1/admin/content_pages.py +++ b/app/api/v1/admin/content_pages.py @@ -48,9 +48,7 @@ class ContentPageCreate(BaseModel): meta_description: str | None = Field( None, max_length=300, description="SEO meta description" ) - meta_keywords: str | None = Field( - None, max_length=300, description="SEO keywords" - ) + meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords") is_published: bool = Field(default=False, description="Publish immediately") show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_header: bool = Field(default=False, description="Show in header navigation") diff --git a/app/api/v1/admin/notifications.py b/app/api/v1/admin/notifications.py index 090b2205..6322cbde 100644 --- a/app/api/v1/admin/notifications.py +++ b/app/api/v1/admin/notifications.py @@ -88,9 +88,7 @@ def mark_all_as_read( @router.get("/alerts", response_model=PlatformAlertListResponse) def get_platform_alerts( severity: str | None = Query(None, description="Filter by severity"), - is_resolved: bool | None = Query( - None, description="Filter by resolution status" - ), + is_resolved: bool | None = Query(None, description="Filter by resolution status"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), db: Session = Depends(get_db), diff --git a/app/api/v1/shop/products.py b/app/api/v1/shop/products.py index cf6f76e7..f7d834b6 100644 --- a/app/api/v1/shop/products.py +++ b/app/api/v1/shop/products.py @@ -30,9 +30,7 @@ def get_product_catalog( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), search: str | None = Query(None, description="Search products by name"), - is_featured: bool | None = Query( - None, description="Filter by featured products" - ), + is_featured: bool | None = Query(None, description="Filter by featured products"), db: Session = Depends(get_db), ): """ diff --git a/app/api/v1/vendor/content_pages.py b/app/api/v1/vendor/content_pages.py index 4f50d8c0..02b0297a 100644 --- a/app/api/v1/vendor/content_pages.py +++ b/app/api/v1/vendor/content_pages.py @@ -43,9 +43,7 @@ class VendorContentPageCreate(BaseModel): meta_description: str | None = Field( None, max_length=300, description="SEO meta description" ) - meta_keywords: str | None = Field( - None, max_length=300, description="SEO keywords" - ) + meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords") is_published: bool = Field(default=False, description="Publish immediately") show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_header: bool = Field(default=False, description="Show in header navigation") diff --git a/app/core/config.py b/app/core/config.py index 915bee47..86221ce6 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -13,7 +13,6 @@ Note: Environment detection is handled by app.core.environment module. This module focuses purely on configuration storage and validation. """ - from pydantic_settings import BaseSettings diff --git a/app/exceptions/auth.py b/app/exceptions/auth.py index 8af58f40..f92ab3a5 100644 --- a/app/exceptions/auth.py +++ b/app/exceptions/auth.py @@ -3,7 +3,6 @@ Authentication and authorization specific exceptions. """ - from .base import AuthenticationException, AuthorizationException, ConflictException diff --git a/app/exceptions/cart.py b/app/exceptions/cart.py index fd62b890..502a9e40 100644 --- a/app/exceptions/cart.py +++ b/app/exceptions/cart.py @@ -3,7 +3,6 @@ Shopping cart specific exceptions. """ - from .base import BusinessLogicException, ResourceNotFoundException, ValidationException diff --git a/app/exceptions/order.py b/app/exceptions/order.py index 8a72de98..7d733829 100644 --- a/app/exceptions/order.py +++ b/app/exceptions/order.py @@ -3,7 +3,6 @@ Order management specific exceptions. """ - from .base import BusinessLogicException, ResourceNotFoundException, ValidationException diff --git a/app/exceptions/product.py b/app/exceptions/product.py index 72e8d377..3e4c3c59 100644 --- a/app/exceptions/product.py +++ b/app/exceptions/product.py @@ -3,7 +3,6 @@ Product (vendor catalog) specific exceptions. """ - from .base import ( BusinessLogicException, ConflictException, diff --git a/app/exceptions/vendor_domain.py b/app/exceptions/vendor_domain.py index 147cc26d..36f895bd 100644 --- a/app/exceptions/vendor_domain.py +++ b/app/exceptions/vendor_domain.py @@ -3,7 +3,6 @@ Vendor domain management specific exceptions. """ - from .base import ( BusinessLogicException, ConflictException, diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 4cb6f26e..45169c9c 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -30,7 +30,6 @@ Routes: - GET /code-quality/violations/{violation_id} → Violation details (auth required) """ - from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates diff --git a/app/services/product_service.py b/app/services/product_service.py index f0af551a..b7eb6e1f 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -107,9 +107,7 @@ class ProductService: ) if existing: - raise ProductAlreadyExistsException( - "Product already exists in catalog" - ) + raise ProductAlreadyExistsException("Product already exists in catalog") # Create product product = Product( diff --git a/app/utils/data_processing.py b/app/utils/data_processing.py index a5508f1a..893c563d 100644 --- a/app/utils/data_processing.py +++ b/app/utils/data_processing.py @@ -108,9 +108,7 @@ class PriceProcessor: r"([A-Z]{3})\s*([0-9.,]+)": lambda m: (m.group(2), m.group(1)), } - def parse_price_currency( - self, price_str: any - ) -> tuple[str | None, str | None]: + def parse_price_currency(self, price_str: any) -> tuple[str | None, str | None]: """ Parse a price string to extract the numeric value and currency. diff --git a/docs/development/code-quality.md b/docs/development/code-quality.md index 3564f540..776d1152 100644 --- a/docs/development/code-quality.md +++ b/docs/development/code-quality.md @@ -190,7 +190,7 @@ show_missing = true Before committing code: ```bash -# 1. Format and lint your code +# 1. Format, lint, and verify critical imports make check # 2. Run relevant tests @@ -201,6 +201,25 @@ git add . 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 For continuous integration: diff --git a/middleware/auth.py b/middleware/auth.py index 521b573e..b09163bb 100644 --- a/middleware/auth.py +++ b/middleware/auth.py @@ -204,9 +204,7 @@ class AuthManager: raise InvalidTokenException("Token missing expiration") # Check if token has expired (additional check beyond jwt.decode) - if datetime.now(UTC) > datetime.fromtimestamp( - exp, tz=UTC - ): + if datetime.now(UTC) > datetime.fromtimestamp(exp, tz=UTC): raise TokenExpiredException() # Validate user identifier claim exists diff --git a/middleware/vendor_context.py b/middleware/vendor_context.py index 64690216..5315e3ed 100644 --- a/middleware/vendor_context.py +++ b/middleware/vendor_context.py @@ -141,9 +141,7 @@ class VendorContextManager: f"[OK] Vendor found via custom domain: {domain} → {vendor.name}" ) return vendor - logger.warning( - f"No active vendor found for custom domain: {domain}" - ) + logger.warning(f"No active vendor found for custom domain: {domain}") return None # Method 2 & 3: Subdomain or path-based lookup diff --git a/models/database/cart.py b/models/database/cart.py index ee538243..8752dfc9 100644 --- a/models/database/cart.py +++ b/models/database/cart.py @@ -1,7 +1,6 @@ # models/database/cart.py """Cart item database model.""" - from sqlalchemy import ( Column, Float, diff --git a/models/database/customer.py b/models/database/customer.py index f8c947c8..a74b8d20 100644 --- a/models/database/customer.py +++ b/models/database/customer.py @@ -1,4 +1,3 @@ - from sqlalchemy import ( JSON, Boolean, diff --git a/models/database/marketplace_import_job.py b/models/database/marketplace_import_job.py index 85dedd69..b929a426 100644 --- a/models/database/marketplace_import_job.py +++ b/models/database/marketplace_import_job.py @@ -1,4 +1,3 @@ - from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import relationship diff --git a/models/database/marketplace_product.py b/models/database/marketplace_product.py index 3dbdf9ab..e5910dd4 100644 --- a/models/database/marketplace_product.py +++ b/models/database/marketplace_product.py @@ -1,4 +1,3 @@ - from sqlalchemy import ( Column, Index, diff --git a/models/database/vendor_domain.py b/models/database/vendor_domain.py index 619416df..4e4798e1 100644 --- a/models/database/vendor_domain.py +++ b/models/database/vendor_domain.py @@ -3,7 +3,6 @@ Vendor Domain Model - Maps custom domains to vendors """ - from sqlalchemy import ( Boolean, Column, diff --git a/models/schema/cart.py b/models/schema/cart.py index 8a97b36b..d59ca68b 100644 --- a/models/schema/cart.py +++ b/models/schema/cart.py @@ -3,7 +3,6 @@ Pydantic schemas for shopping cart operations. """ - from pydantic import BaseModel, ConfigDict, Field # ============================================================================ diff --git a/models/schema/product.py b/models/schema/product.py index 0f87dce2..270ee4cf 100644 --- a/models/schema/product.py +++ b/models/schema/product.py @@ -11,9 +11,7 @@ class ProductCreate(BaseModel): marketplace_product_id: int = Field( ..., description="MarketplaceProduct ID to add to vendor catalog" ) - product_id: str | None = Field( - None, description="Vendor's internal SKU/product ID" - ) + product_id: str | None = Field(None, description="Vendor's internal SKU/product ID") price: float | None = Field(None, ge=0) sale_price: float | None = Field(None, ge=0) currency: str | None = None diff --git a/models/schema/team.py b/models/schema/team.py index 78aef465..eed771ce 100644 --- a/models/schema/team.py +++ b/models/schema/team.py @@ -31,7 +31,6 @@ class RoleCreate(RoleBase): """Schema for creating a role.""" - class RoleUpdate(BaseModel): """Schema for updating a role.""" diff --git a/models/schema/vendor.py b/models/schema/vendor.py index a0049c95..4e43a11e 100644 --- a/models/schema/vendor.py +++ b/models/schema/vendor.py @@ -100,9 +100,7 @@ class VendorUpdate(BaseModel): subdomain: str | None = Field(None, min_length=2, max_length=100) # Business Contact Information (Vendor Fields) - contact_email: str | None = Field( - None, description="Public business contact email" - ) + contact_email: str | None = Field(None, description="Public business contact email") contact_phone: str | None = None website: str | None = None diff --git a/models/schema/vendor_theme.py b/models/schema/vendor_theme.py index 90b6943d..76d823d1 100644 --- a/models/schema/vendor_theme.py +++ b/models/schema/vendor_theme.py @@ -3,7 +3,6 @@ Pydantic schemas for vendor theme operations. """ - from pydantic import BaseModel, Field @@ -54,14 +53,10 @@ class VendorThemeUpdate(BaseModel): theme_name: str | None = Field(None, description="Theme preset name") colors: dict[str, str] | None = Field(None, description="Color scheme") fonts: dict[str, str] | None = Field(None, description="Font settings") - branding: dict[str, str | None] | None = Field( - None, description="Branding assets" - ) + branding: dict[str, str | None] | None = Field(None, description="Branding assets") layout: dict[str, str] | None = Field(None, description="Layout settings") custom_css: str | None = Field(None, description="Custom CSS rules") - social_links: dict[str, str] | None = Field( - None, description="Social media links" - ) + social_links: dict[str, str] | None = Field(None, description="Social media links") class VendorThemeResponse(BaseModel): diff --git a/pyproject.toml b/pyproject.toml index 5e3f25c3..b2e2c1ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,8 +60,10 @@ unfixable = [] # 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"] +# Base files that re-export (re-export Base from core.database) +"models/database/base.py" = ["F401"] # Ignore specific rules in test files "tests/**/*.py" = ["S101", "PLR2004"] # Alembic migrations can have longer lines and specific patterns diff --git a/scripts/route_diagnostics.py b/scripts/route_diagnostics.py index ad809ef2..6f5d499d 100644 --- a/scripts/route_diagnostics.py +++ b/scripts/route_diagnostics.py @@ -7,7 +7,6 @@ Usage: python route_diagnostics.py """ - def check_route_order(): """Check if routes are registered in the correct order.""" print("šŸ” Checking FastAPI Route Configuration...\n") diff --git a/scripts/test_auth_complete.py b/scripts/test_auth_complete.py index 6c1f7a9d..170d52ab 100644 --- a/scripts/test_auth_complete.py +++ b/scripts/test_auth_complete.py @@ -20,7 +20,6 @@ Requirements: * Customer: username=customer, password=customer123, vendor_id=1 """ - import requests BASE_URL = "http://localhost:8000" diff --git a/scripts/test_vendor_management.py b/scripts/test_vendor_management.py index 153c18be..2bef707a 100644 --- a/scripts/test_vendor_management.py +++ b/scripts/test_vendor_management.py @@ -9,7 +9,6 @@ Tests: - Transfer ownership """ - import requests BASE_URL = "http://localhost:8000" diff --git a/scripts/verify_critical_imports.py b/scripts/verify_critical_imports.py new file mode 100755 index 00000000..1d839fa1 --- /dev/null +++ b/scripts/verify_critical_imports.py @@ -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() diff --git a/tests/system/test_error_handling.py b/tests/system/test_error_handling.py index e30ae012..9850f4d5 100644 --- a/tests/system/test_error_handling.py +++ b/tests/system/test_error_handling.py @@ -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. """ - import pytest diff --git a/tests/unit/middleware/test_decorators.py b/tests/unit/middleware/test_decorators.py index 22ebe684..e67bb61b 100644 --- a/tests/unit/middleware/test_decorators.py +++ b/tests/unit/middleware/test_decorators.py @@ -12,7 +12,6 @@ Tests cover: - Edge cases and isolation """ - import pytest from app.exceptions.base import RateLimitException