From 8ab2e1dd28ff5018494357c968a7b151c72ee65f Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Sat, 18 Oct 2025 21:29:28 -0400 Subject: [PATCH] Did some things. Signed-off-by: Cliff Hill --- .darglint | 6 ++ .pre-commit-config.yaml | 31 ++++--- README.md | 27 ++++++ backend/pyproject.toml | 26 ++++++ frontend/.prettierrc.json | 8 ++ frontend/env.d.ts | 7 ++ frontend/package.json | 32 +++++++ frontend/tsconfig.json | 24 +++++ frontend/vite.config.ts | 24 +++++ scripts/check_no_docstring_types.py | 138 ++++++++++++++++++++++++++++ 10 files changed, 312 insertions(+), 11 deletions(-) create mode 100644 .darglint create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/env.d.ts create mode 100644 frontend/package.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 scripts/check_no_docstring_types.py diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..1bf6baa --- /dev/null +++ b/.darglint @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cd6ee1..451e2e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/README.md b/README.md index 8e79293..c1a4679 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3b0e879..93dc18c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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 = [ diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..29b9d1f --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6abaefa --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..105feaf --- /dev/null +++ b/frontend/tsconfig.json @@ -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 + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..0068b89 --- /dev/null +++ b/frontend/vite.config.ts @@ -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/, '') + } + } + } +}) diff --git a/scripts/check_no_docstring_types.py b/scripts/check_no_docstring_types.py new file mode 100644 index 0000000..9464431 --- /dev/null +++ b/scripts/check_no_docstring_types.py @@ -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 [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())