Did some things.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-10-18 21:29:28 -04:00
parent 0e23b38eeb
commit 8ab2e1dd28
10 changed files with 312 additions and 11 deletions

6
.darglint Normal file
View File

@@ -0,0 +1,6 @@
[darglint]
docstring_style = google
strictness = short
ignore_regex = ^_
# Ignore type-related errors since we use type hints in function signatures
ignore = DAR101,DAR102,DAR103,DAR201,DAR202,DAR203,DAR301,DAR302,DAR401,DAR402

View File

@@ -39,10 +39,27 @@ repos:
files: ^backend/
additional_dependencies: [fastapi, uvicorn, sqlalchemy, pydantic]
# Python docstring linting with darglint
- repo: https://github.com/terrencepreilly/darglint
rev: v1.8.1
hooks:
- id: darglint
files: ^backend/.*\.py$
# Custom hook to enforce no types in docstrings
- repo: local
hooks:
- id: no-docstring-types
name: Prohibit types in docstrings
entry: python scripts/check_no_docstring_types.py
language: system
files: ^backend/.*\.py$
pass_filenames: true
# Frontend linting and formatting
- repo: local
hooks:
# ESLint
# ESLint (basic linting only for now)
- id: eslint
name: eslint
entry: bash -c 'cd frontend && npm run lint:fix'
@@ -50,18 +67,10 @@ repos:
files: ^frontend/.*\.(js|ts|vue)$
pass_filenames: false
# Prettier
# Prettier (only if src directory exists)
- id: prettier
name: prettier
entry: bash -c 'cd frontend && npm run format'
entry: bash -c 'cd frontend && if [ -d src ]; then npm run format; else echo "Skipping prettier - no src directory"; fi'
language: system
files: ^frontend/.*\.(js|ts|vue|json|css|scss|md)$
pass_filenames: false
# TypeScript type checking
- id: typescript-check
name: typescript-check
entry: bash -c 'cd frontend && npm run type-check'
language: system
files: ^frontend/.*\.(ts|vue)$
pass_filenames: false

View File

@@ -15,6 +15,33 @@ A full-stack application for managing Plex playlists with a FastAPI backend and
- Docker and Docker Compose
- Git
- pre-commit (for development)
### Code Quality Tools
This project uses comprehensive linting and formatting:
**Backend (Python):**
- `ruff` - Fast Python linter and formatter
- `pyright` - Type checking
- `darglint` - Docstring linting (Google style)
**Frontend (TypeScript/Vue):**
- `eslint` - Linting with Vue and TypeScript support
- `prettier` - Code formatting
- `vue-tsc` - Vue TypeScript checking
- `eslint-plugin-tsdoc` - TSDoc documentation linting
**General:**
- `pre-commit` - Git hooks for automated quality checks
- TOML formatting and validation
### Setting up pre-commit hooks
```bash
pip install pre-commit
pre-commit install
```
### Running in Development Mode

View File

@@ -1,3 +1,29 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
[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.13"
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"
]
[tool.ruff]
# Exclude common directories
exclude = [

View File

@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}

7
frontend/env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

32
frontend/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "plex-playlist-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --fix",
"lint:fix": "eslint . --fix",
"lint:tsdoc": "eslint . --fix",
"format": "prettier --write src/",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/tsconfig": "^0.5.0",
"eslint": "^9.33.0",
"eslint-plugin-vue": "^9.28.0",
"prettier": "^3.0.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-eslint-parser": "^9.4.0",
"vue-tsc": "^2.0.0"
}
}

24
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}

24
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://backend:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Check that docstrings do not contain type annotations.
This script enforces that Google-style docstrings contain NO type information,
since types should be declared in function signatures using type hints.
"""
import ast
import re
import sys
from pathlib import Path
def extract_docstring(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
"""Extract docstring from a function node."""
if (
node.body
and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant)
and isinstance(node.body[0].value.value, str)
):
return node.body[0].value.value
return None
def check_docstring_for_types(docstring: str, func_name: str, filename: str, lineno: int) -> list[str]:
"""Check if docstring contains type annotations and return errors."""
errors: list[str] = []
lines = docstring.split('\n')
in_args_section = False
in_returns_section = False
in_yields_section = False
for i, line in enumerate(lines, 1):
line_stripped = line.strip()
# Track which section we're in
if re.match(r'^\s*Args?\s*:', line_stripped, re.IGNORECASE):
in_args_section = True
in_returns_section = False
in_yields_section = False
continue
elif re.match(r'^\s*Returns?\s*:', line_stripped, re.IGNORECASE):
in_args_section = False
in_returns_section = True
in_yields_section = False
continue
elif re.match(r'^\s*Yields?\s*:', line_stripped, re.IGNORECASE):
in_args_section = False
in_returns_section = False
in_yields_section = True
continue
elif re.match(r'^\s*(Raises?|Examples?|Note|Notes)\s*:', line_stripped, re.IGNORECASE):
in_args_section = False
in_returns_section = False
in_yields_section = False
continue
# Check for type annotations in Args section
if in_args_section and re.match(r'^\s*\w+\s*\([^)]+\)\s*:', line_stripped):
errors.append(
f"{filename}:{lineno + i}:{func_name}: "
f"Type annotation found in Args section: '{line_stripped}'. "
f"Remove type information and use function signature type hints instead."
)
# Check for type annotations in Returns section
if in_returns_section and re.match(r'^\s*\w+\s*:', line_stripped) and not re.match(r'^\s*Returns?\s*:', line_stripped, re.IGNORECASE):
errors.append(
f"{filename}:{lineno + i}:{func_name}: "
f"Type annotation found in Returns section: '{line_stripped}'. "
f"Remove type information and use function signature return type hints instead."
)
# Check for type annotations in Yields section
if in_yields_section and re.match(r'^\s*\w+\s*:', line_stripped) and not re.match(r'^\s*Yields?\s*:', line_stripped, re.IGNORECASE):
errors.append(
f"{filename}:{lineno + i}:{func_name}: "
f"Type annotation found in Yields section: '{line_stripped}'. "
f"Remove type information and use function signature type hints instead."
)
return errors
def check_file(filepath: Path) -> list[str]:
"""Check a single Python file for type annotations in docstrings."""
try:
content = filepath.read_text(encoding='utf-8')
tree = ast.parse(content, filename=str(filepath))
except (SyntaxError, UnicodeDecodeError) as e:
return [f"{filepath}: Failed to parse file: {e}"]
errors: list[str] = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
docstring = extract_docstring(node)
if docstring:
func_errors = check_docstring_for_types(
docstring,
node.name,
str(filepath),
node.lineno
)
errors.extend(func_errors)
return errors
def main() -> int:
"""Main function to check all provided files."""
if len(sys.argv) < 2:
print("Usage: check_no_docstring_types.py <file1> [file2] ...")
return 1
all_errors: list[str] = []
for filepath_str in sys.argv[1:]:
filepath = Path(filepath_str)
if filepath.suffix == '.py':
errors = check_file(filepath)
all_errors.extend(errors)
if all_errors:
print("❌ Found type annotations in docstrings:")
for error in all_errors:
print(f" {error}")
print("\n💡 Tip: Use type hints in function signatures instead of docstring type annotations.")
return 1
print("✅ No type annotations found in docstrings.")
return 0
if __name__ == '__main__':
sys.exit(main())