Public Access
@@ -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
|
||||||
+20
-11
@@ -39,10 +39,27 @@ repos:
|
|||||||
files: ^backend/
|
files: ^backend/
|
||||||
additional_dependencies: [fastapi, uvicorn, sqlalchemy, pydantic]
|
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
|
# Frontend linting and formatting
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
# ESLint
|
# ESLint (basic linting only for now)
|
||||||
- id: eslint
|
- id: eslint
|
||||||
name: eslint
|
name: eslint
|
||||||
entry: bash -c 'cd frontend && npm run lint:fix'
|
entry: bash -c 'cd frontend && npm run lint:fix'
|
||||||
@@ -50,18 +67,10 @@ repos:
|
|||||||
files: ^frontend/.*\.(js|ts|vue)$
|
files: ^frontend/.*\.(js|ts|vue)$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
# Prettier
|
# Prettier (only if src directory exists)
|
||||||
- id: prettier
|
- id: prettier
|
||||||
name: 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
|
language: system
|
||||||
files: ^frontend/.*\.(js|ts|vue|json|css|scss|md)$
|
files: ^frontend/.*\.(js|ts|vue|json|css|scss|md)$
|
||||||
pass_filenames: false
|
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
|
|
||||||
|
|||||||
@@ -15,6 +15,33 @@ A full-stack application for managing Plex playlists with a FastAPI backend and
|
|||||||
|
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
- Git
|
- 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
|
### Running in Development Mode
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
[tool.ruff]
|
||||||
# Exclude common directories
|
# Exclude common directories
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user