Fixing everything, making the project structure ready for real code.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -1,14 +1,22 @@
|
||||
"""Pytest configuration for automatic typeguard integration."""
|
||||
|
||||
from typeguard import install_import_hook
|
||||
|
||||
def pytest_configure(config):
|
||||
|
||||
def pytest_configure(config) -> None: # noqa: ARG001
|
||||
"""Configure pytest to use typeguard automatically."""
|
||||
# This enables typeguard for all functions with type hints
|
||||
# without requiring @typechecked decorators
|
||||
pass
|
||||
# Install typeguard import hook to automatically check all functions
|
||||
# with type hints in the backend package during test runs
|
||||
install_import_hook("backend")
|
||||
|
||||
# Also check any other packages in src/
|
||||
install_import_hook("src")
|
||||
|
||||
print("🛡️ Automatic typeguard hooks installed for test run")
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
"""Set up typeguard for each test run."""
|
||||
# Import hook is automatically enabled via --typeguard-packages
|
||||
# The import hook is automatically enabled via pytest_configure
|
||||
# All functions with type hints will be automatically checked
|
||||
pass
|
||||
|
||||
@@ -15,31 +15,6 @@ dev = [
|
||||
"pytest-mock>=3.12.0"
|
||||
]
|
||||
|
||||
[project]
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn[standard]>=0.24.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"pydantic>=2.5.0"
|
||||
]
|
||||
description = "Plex Playlist Management API"
|
||||
name = "plex-playlist-backend"
|
||||
requires-python = ">=3.12"
|
||||
version = "0.1.0"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff>=0.6.0",
|
||||
"pyright>=1.1.380",
|
||||
"darglint>=1.8.1",
|
||||
"pytest>=7.4.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"typeguard>=4.1.0",
|
||||
"httpx>=0.25.0", # For testing async HTTP calls
|
||||
"pytest-mock>=3.12.0"
|
||||
]
|
||||
|
||||
[tool.coverage]
|
||||
|
||||
[tool.coverage.report]
|
||||
@@ -78,8 +53,7 @@ addopts = [
|
||||
"--cov=backend",
|
||||
"--cov-report=term-missing:skip-covered",
|
||||
"--cov-report=html",
|
||||
"--cov-report=xml",
|
||||
"--typeguard-packages=backend"
|
||||
"--cov-report=xml"
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""
|
||||
Plex Playlist Backend Package.
|
||||
|
||||
This package provides the FastAPI backend for playlist management
|
||||
with automatic typeguard validation.
|
||||
"""
|
||||
|
||||
from .main import PlaylistManager, app, calculate_playlist_duration, process_user_data
|
||||
from .main import app
|
||||
|
||||
__all__ = ["app", "process_user_data", "calculate_playlist_duration", "PlaylistManager"]
|
||||
__all__ = ["app"]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""
|
||||
Hypermodern Python setup for automatic typeguard validation.
|
||||
|
||||
This approach enables runtime type checking without requiring decorators
|
||||
or explicit validation calls in your application code.
|
||||
Plex Playlist Backend API.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -10,13 +7,10 @@ from fastapi import FastAPI
|
||||
# Create FastAPI application instance
|
||||
app = FastAPI(
|
||||
title="Plex Playlist Backend",
|
||||
description="API for managing Plex playlists with automatic type validation",
|
||||
description="API for managing Plex playlists",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# No imports needed in application code - typeguard works automatically
|
||||
# when enabled via pytest configuration
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root() -> dict[str, str]:
|
||||
@@ -28,93 +22,3 @@ def read_root() -> dict[str, str]:
|
||||
def health_check() -> dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
def process_user_data(user_id: int, name: str, email: str) -> dict[str, str | int]:
|
||||
"""Process user data with automatic type validation.
|
||||
|
||||
This function has type hints, and when running under pytest with
|
||||
--typeguard-packages, all calls will be automatically validated
|
||||
without any decorators or explicit checks.
|
||||
|
||||
Args:
|
||||
user_id: Unique identifier for the user
|
||||
name: User's full name
|
||||
email: User's email address
|
||||
|
||||
Returns:
|
||||
Dictionary containing processed user information
|
||||
"""
|
||||
return {
|
||||
"id": user_id,
|
||||
"name": name.title(),
|
||||
"email": email.lower(),
|
||||
"display_name": f"{name} ({email})",
|
||||
}
|
||||
|
||||
|
||||
def calculate_playlist_duration(track_durations: list[float]) -> float:
|
||||
"""Calculate total playlist duration.
|
||||
|
||||
Automatic type validation ensures track_durations is actually a list
|
||||
of floats, without any explicit validation code.
|
||||
|
||||
Args:
|
||||
track_durations: List of track durations in seconds
|
||||
|
||||
Returns:
|
||||
Total duration in seconds
|
||||
"""
|
||||
return sum(track_durations)
|
||||
|
||||
|
||||
class PlaylistManager:
|
||||
"""Playlist management with automatic type validation."""
|
||||
|
||||
def __init__(self, name: str, max_size: int = 1000) -> None:
|
||||
"""Initialize playlist manager.
|
||||
|
||||
Args:
|
||||
name: Name of the playlist manager
|
||||
max_size: Maximum number of tracks allowed
|
||||
"""
|
||||
self.name = name
|
||||
self.max_size = max_size
|
||||
self.tracks: list[str] = []
|
||||
|
||||
def add_track(self, track_id: str) -> bool:
|
||||
"""Add a track to the playlist.
|
||||
|
||||
Args:
|
||||
track_id: Unique identifier for the track
|
||||
|
||||
Returns:
|
||||
True if track was added successfully
|
||||
"""
|
||||
if len(self.tracks) >= self.max_size:
|
||||
return False
|
||||
|
||||
self.tracks.append(track_id)
|
||||
return True
|
||||
|
||||
def get_track_count(self) -> int:
|
||||
"""Get the number of tracks in the playlist.
|
||||
|
||||
Returns:
|
||||
Number of tracks currently in the playlist
|
||||
"""
|
||||
return len(self.tracks)
|
||||
|
||||
|
||||
# API endpoints using the classes and functions with automatic validation
|
||||
@app.post("/api/users")
|
||||
def create_user(user_id: int, name: str, email: str) -> dict[str, str | int]:
|
||||
"""Create user endpoint with automatic type validation."""
|
||||
return process_user_data(user_id, name, email)
|
||||
|
||||
|
||||
@app.post("/api/playlists/{playlist_name}/duration")
|
||||
def calculate_duration(track_durations: list[float]) -> dict[str, float]:
|
||||
"""Calculate playlist duration endpoint."""
|
||||
total_duration = calculate_playlist_duration(track_durations)
|
||||
return {"total_duration": total_duration}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Example tests for the Plex Playlist backend with automatic typeguard."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers together.
|
||||
|
||||
Args:
|
||||
a: First number to add
|
||||
b: Second number to add
|
||||
|
||||
Returns:
|
||||
The sum of a and b
|
||||
"""
|
||||
return a + b
|
||||
|
||||
|
||||
def create_playlist_data(
|
||||
name: str, description: str | None = None
|
||||
) -> dict[str, str | None]:
|
||||
"""Create playlist data structure.
|
||||
|
||||
Args:
|
||||
name: Name of the playlist
|
||||
description: Optional description
|
||||
|
||||
Returns:
|
||||
Dictionary containing playlist data
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"id": f"playlist_{name.lower().replace(' ', '_')}",
|
||||
}
|
||||
|
||||
|
||||
class TestMathFunctions:
|
||||
"""Test mathematical functions with automatic type checking."""
|
||||
|
||||
def test_add_numbers_valid_types(self) -> None:
|
||||
"""Test adding numbers with correct types."""
|
||||
result = add_numbers(5, 3)
|
||||
assert result == 8
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_add_numbers_invalid_types(self) -> None:
|
||||
"""Test that typeguard catches invalid types automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# Typeguard will automatically catch this type violation
|
||||
add_numbers("5", 3) # type: ignore[arg-type]
|
||||
|
||||
def test_add_numbers_coverage_example(self) -> None:
|
||||
"""Test different number combinations for coverage."""
|
||||
assert add_numbers(0, 0) == 0
|
||||
assert add_numbers(-1, 1) == 0
|
||||
assert add_numbers(100, 200) == 300
|
||||
|
||||
|
||||
class TestPlaylistFunctions:
|
||||
"""Test playlist-related functions with automatic type checking."""
|
||||
|
||||
def test_create_playlist_data_with_description(self) -> None:
|
||||
"""Test creating playlist data with description."""
|
||||
result = create_playlist_data("My Playlist", "A great playlist")
|
||||
|
||||
expected = {
|
||||
"name": "My Playlist",
|
||||
"description": "A great playlist",
|
||||
"id": "playlist_my_playlist",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_create_playlist_data_without_description(self) -> None:
|
||||
"""Test creating playlist data without description."""
|
||||
result = create_playlist_data("Simple Playlist")
|
||||
|
||||
expected = {
|
||||
"name": "Simple Playlist",
|
||||
"description": None,
|
||||
"id": "playlist_simple_playlist",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_create_playlist_invalid_name_type(self) -> None:
|
||||
"""Test that typeguard automatically catches invalid name type."""
|
||||
with pytest.raises(TypeError):
|
||||
# No decorator needed - typeguard import hook handles this
|
||||
create_playlist_data(123, "description") # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestIntegrationExample:
|
||||
"""Example integration tests with automatic type validation."""
|
||||
|
||||
def test_integration_placeholder(self) -> None:
|
||||
"""Placeholder for real integration tests."""
|
||||
# All functions called here will have automatic type checking
|
||||
assert True
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Tests demonstrating hypermodern typeguard usage.
|
||||
No decorators needed - validation happens automatically.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from backend import PlaylistManager, calculate_playlist_duration, process_user_data
|
||||
|
||||
|
||||
class TestHypermodernTypeguard:
|
||||
"""Tests showing automatic type validation without decorators."""
|
||||
|
||||
def test_process_user_data_valid_types(self) -> None:
|
||||
"""Test with correct types - should work normally."""
|
||||
result = process_user_data(123, "John Doe", "JOHN@EXAMPLE.COM")
|
||||
|
||||
expected = {
|
||||
"id": 123,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"display_name": "John Doe (john@example.com)",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_process_user_data_invalid_user_id(self) -> None:
|
||||
"""Test with wrong user_id type - typeguard catches automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because "123" is str, not int
|
||||
process_user_data("123", "John", "john@example.com") # type: ignore[arg-type]
|
||||
|
||||
def test_process_user_data_invalid_name(self) -> None:
|
||||
"""Test with wrong name type - typeguard catches automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
process_user_data(123, 123, "john@example.com") # type: ignore[arg-type]
|
||||
|
||||
def test_calculate_duration_valid(self) -> None:
|
||||
"""Test duration calculation with valid types."""
|
||||
durations = [3.5, 4.2, 2.8, 5.1]
|
||||
total = calculate_playlist_duration(durations)
|
||||
|
||||
assert abs(total - 15.6) < 0.001 # Float comparison
|
||||
|
||||
def test_calculate_duration_invalid_list_type(self) -> None:
|
||||
"""Test with wrong parameter type - not a list."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because "not a list" is str, not list[float]
|
||||
calculate_playlist_duration("not a list") # type: ignore[arg-type]
|
||||
|
||||
def test_calculate_duration_invalid_element_types(self) -> None:
|
||||
"""Test with wrong element types in list."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because list contains strings, not floats
|
||||
calculate_playlist_duration(["3.5", "4.2"]) # type: ignore[list-item]
|
||||
|
||||
def test_playlist_manager_creation(self) -> None:
|
||||
"""Test playlist manager creation with valid types."""
|
||||
manager = PlaylistManager("My Playlist", 500)
|
||||
|
||||
assert manager.name == "My Playlist"
|
||||
assert manager.max_size == 500
|
||||
assert manager.get_track_count() == 0
|
||||
|
||||
def test_playlist_manager_invalid_name_type(self) -> None:
|
||||
"""Test playlist manager creation with invalid name type."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
PlaylistManager(123) # type: ignore[arg-type]
|
||||
|
||||
def test_playlist_manager_add_track_valid(self) -> None:
|
||||
"""Test adding tracks with valid types."""
|
||||
manager = PlaylistManager("Test Playlist", 2)
|
||||
|
||||
assert manager.add_track("track_1") is True
|
||||
assert manager.add_track("track_2") is True
|
||||
assert manager.add_track("track_3") is False # Exceeds max_size
|
||||
|
||||
assert manager.get_track_count() == 2
|
||||
|
||||
def test_playlist_manager_add_track_invalid_type(self) -> None:
|
||||
"""Test adding track with invalid type."""
|
||||
manager = PlaylistManager("Test Playlist")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
manager.add_track(123) # type: ignore[arg-type]
|
||||
Reference in New Issue
Block a user