diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 4d4a7f79..d989fa88 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -53,3 +53,7 @@ jobs: - name: Run black run: pipenv run black --check . working-directory: backend + + - name: Run ruff + run: pipenv run ruff check . + working-directory: backend diff --git a/backend/Pipfile b/backend/Pipfile index 66e02451..9ebd5de3 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -5,39 +5,40 @@ name = "pypi" [packages] aiopg = ">=1.4.0" +alembic = ">=1.9.1" +click = ">=8.1.3" +databases = {extras = ["asyncpg"], version = ">=0.7.0"} fastapi = ">=0.88.0" fastapi-cache2 = ">=0.2.0" +fastapi-sso = ">=0.6.4" gunicorn = ">=20.1.0" -uvicorn = ">=0.20.0" -starlette = ">=0.22.0" +heliclockter = ">=1.0.4" +parameterized = ">=0.8.1" +passlib = ">=1.7.4" +pydantic = "<2.0.0" +pyjwt = ">=2.6.0" +python-dotenv = ">=0.21.0" +python-multipart = ">=0.0.5" +sentry-sdk = ">=1.13.0" sqlalchemy = "<2.0" sqlalchemy-stubs = ">=0.4" -pydantic = "<2.0.0" -heliclockter = ">=1.0.4" -alembic = ">=1.9.1" -types-simplejson = ">=3.18.0" -python-dotenv = ">=0.21.0" -databases = {extras = ["asyncpg"], version = ">=0.7.0"} -passlib = ">=1.7.4" +starlette = ">=0.22.0" types-passlib = "*" -pyjwt = ">=2.6.0" -click = ">=8.1.3" -python-multipart = ">=0.0.5" -parameterized = ">=0.8.1" -sentry-sdk = ">=1.13.0" -fastapi-sso = ">=0.6.4" +types-simplejson = ">=3.18.0" +uvicorn = ">=0.20.0" [dev-packages] -mypy = ">=1.3.1" -black = ">=22.12.0" -isort = ">=5.11.4" -pylint = ">=2.15.10" -pytest = ">=7.2.0" -pytest-cov = ">=4.0.0" -pytest-asyncio = ">=0.20.3" -pytest-xdist = ">=3.2.1" aiohttp = ">=3.8.3" aioresponses = ">=0.7.4" +black = ">=22.12.0" +isort = ">=5.11.4" +mypy = ">=1.3.1" +pylint = ">=2.15.10" +pytest = ">=7.2.0" +pytest-asyncio = ">=0.20.3" +pytest-cov = ">=4.0.0" +pytest-xdist = ">=3.2.1" +ruff = ">=0.0.292" [requires] python_version = "3.10" diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 36e1b2af..cb213cc9 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -2,6 +2,7 @@ import logging import os import sys +# ruff: noqa: E402. We first need to insert the path from sqlalchemy import engine_from_config, pool from alembic import context @@ -9,8 +10,8 @@ from alembic import context parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, parent_dir) -from bracket.config import config # pylint: disable=wrong-import-position -from bracket.schema import Base # pylint: disable=wrong-import-position +from bracket.config import config +from bracket.schema import Base ALEMBIC_CONFIG = context.config logger = logging.getLogger('alembic') diff --git a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py index 0d79c6fc..75b2642a 100644 --- a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py +++ b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py @@ -1,7 +1,7 @@ """Add ON DELETE CASCADE to users_x_clubs Revision ID: 274385f2a757 -Revises: +Revises: Create Date: 2023-04-15 11:08:57.406407 """ diff --git a/backend/bracket/logic/scheduling/ladder_players_iter.py b/backend/bracket/logic/scheduling/ladder_players_iter.py index e5e410b3..8189fd3b 100644 --- a/backend/bracket/logic/scheduling/ladder_players_iter.py +++ b/backend/bracket/logic/scheduling/ladder_players_iter.py @@ -15,7 +15,7 @@ from bracket.utils.types import assert_some def player_already_scheduled(player: Player, draft_round: RoundWithMatches) -> bool: - return any((player.id in match.player_ids for match in draft_round.matches)) + return any(player.id in match.player_ids for match in draft_round.matches) async def get_possible_upcoming_matches_for_players( @@ -40,11 +40,9 @@ async def get_possible_upcoming_matches_for_players( @lru_cache def team_already_scheduled_before(player1: Player, player2: Player) -> bool: return any( - ( - player1 in match.team1.players and player2 in match.team2.players - for round_ in other_rounds - for match in round_.matches - ) + player1 in match.team1.players and player2 in match.team2.players + for round_ in other_rounds + for match in round_.matches ) team_already_scheduled_before.cache_clear() diff --git a/backend/bracket/logic/scheduling/ladder_teams.py b/backend/bracket/logic/scheduling/ladder_teams.py index 072a107a..22c21e6c 100644 --- a/backend/bracket/logic/scheduling/ladder_teams.py +++ b/backend/bracket/logic/scheduling/ladder_teams.py @@ -20,10 +20,8 @@ async def get_possible_upcoming_matches_for_teams( for i, team1 in enumerate(teams): for _, team2 in enumerate(teams[i + 1 :]): team_already_scheduled = any( - ( - team1.id in match.team_ids or team2.id in match.team_ids - for match in draft_round.matches - ) + team1.id in match.team_ids or team2.id in match.team_ids + for match in draft_round.matches ) if team_already_scheduled: continue diff --git a/backend/bracket/logic/scheduling/round_robin.py b/backend/bracket/logic/scheduling/round_robin.py index 067a86b2..091255a5 100644 --- a/backend/bracket/logic/scheduling/round_robin.py +++ b/backend/bracket/logic/scheduling/round_robin.py @@ -9,17 +9,15 @@ async def get_possible_upcoming_matches_round_robin( ) -> list[SuggestedMatch]: suggestions: list[SuggestedMatch] = [] rounds = await get_rounds_for_stage(tournament_id, stage_id) - draft_round = next((round_ for round_ in rounds if round_.id == round_id)) + draft_round = next(round_ for round_ in rounds if round_.id == round_id) teams = await get_teams_with_members(tournament_id, only_active_teams=True) for i, team1 in enumerate(teams): for _, team2 in enumerate(teams[i + 1 :]): team_already_scheduled = any( - ( - team1.id in match.team_ids or team2.id in match.team_ids - for match in draft_round.matches - ) + team1.id in match.team_ids or team2.id in match.team_ids + for match in draft_round.matches ) if team_already_scheduled: continue diff --git a/backend/bracket/sql/matches.py b/backend/bracket/sql/matches.py index bcb47711..f9553064 100644 --- a/backend/bracket/sql/matches.py +++ b/backend/bracket/sql/matches.py @@ -12,7 +12,15 @@ async def sql_delete_match(match_id: int) -> None: async def sql_create_match(match: MatchCreateBody) -> Match: query = ''' - INSERT INTO matches (round_id, team1_id, team2_id, team1_score, team2_score, court_id, created) + INSERT INTO matches ( + round_id, + team1_id, + team2_id, + team1_score, + team2_score, + court_id, + created + ) VALUES (:round_id, :team1_id, :team2_id, 0, 0, :court_id, NOW()) RETURNING * ''' diff --git a/backend/bracket/sql/rounds.py b/backend/bracket/sql/rounds.py index 06779ee7..7f95fd91 100644 --- a/backend/bracket/sql/rounds.py +++ b/backend/bracket/sql/rounds.py @@ -1,11 +1,9 @@ -from typing import List - from bracket.database import database from bracket.models.db.round import RoundWithMatches from bracket.sql.stages import get_stages_with_rounds_and_matches -async def get_rounds_for_stage(tournament_id: int, stage_id: int) -> List[RoundWithMatches]: +async def get_rounds_for_stage(tournament_id: int, stage_id: int) -> list[RoundWithMatches]: stages = await get_stages_with_rounds_and_matches(tournament_id) result_stage = next((stage for stage in stages if stage.id == stage_id), None) if result_stage is None: diff --git a/backend/bracket/sql/stages.py b/backend/bracket/sql/stages.py index b4ef34fc..2720e561 100644 --- a/backend/bracket/sql/stages.py +++ b/backend/bracket/sql/stages.py @@ -106,7 +106,7 @@ async def get_next_stage_in_tournament( SELECT id FROM stages AS t WHERE is_active IS TRUE AND stages.tournament_id = :tournament_id - ORDER BY id ASC + ORDER BY id ASC LIMIT 1 ), 10000000000000 @@ -118,7 +118,7 @@ async def get_next_stage_in_tournament( SELECT id FROM stages AS t WHERE is_active IS TRUE AND stages.tournament_id = :tournament_id - ORDER BY id DESC + ORDER BY id DESC LIMIT 1 ), -1 @@ -142,7 +142,7 @@ async def sql_activate_next_stage(new_active_stage_id: int, tournament_id: int) UPDATE stages SET is_active = (stages.id = :new_active_stage_id) WHERE stages.tournament_id = :tournament_id - + ''' await database.execute( query=update_query, diff --git a/backend/bracket/sql/users.py b/backend/bracket/sql/users.py index fe2771e1..00f82f13 100644 --- a/backend/bracket/sql/users.py +++ b/backend/bracket/sql/users.py @@ -12,7 +12,7 @@ async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> boo SELECT DISTINCT t.id FROM users_x_clubs JOIN tournaments t ON t.club_id = users_x_clubs.club_id - WHERE user_id = :user_id + WHERE user_id = :user_id ''' result = await database.fetch_all(query=query, values={'user_id': user_id}) return tournament_id in {tournament.id for tournament in result} # type: ignore[attr-defined] @@ -22,7 +22,7 @@ async def get_which_clubs_has_user_access_to(user_id: int) -> set[int]: query = ''' SELECT club_id FROM users_x_clubs - WHERE user_id = :user_id + WHERE user_id = :user_id ''' result = await database.fetch_all(query=query, values={'user_id': user_id}) return {club.club_id for club in result} # type: ignore[attr-defined] @@ -36,7 +36,7 @@ async def update_user(user_id: int, user: UserToUpdate) -> None: query = ''' UPDATE users SET name = :name, email = :email - WHERE id = :user_id + WHERE id = :user_id ''' await database.execute( query=query, values={'user_id': user_id, 'name': user.name, 'email': user.email} @@ -47,7 +47,7 @@ async def update_user_password(user_id: int, password_hash: str) -> None: query = ''' UPDATE users SET password_hash = :password_hash - WHERE id = :user_id + WHERE id = :user_id ''' await database.execute(query=query, values={'user_id': user_id, 'password_hash': password_hash}) @@ -56,7 +56,7 @@ async def get_user_by_id(user_id: int) -> UserPublic | None: query = ''' SELECT * FROM users - WHERE id = :user_id + WHERE id = :user_id ''' result = await database.fetch_one(query=query, values={'user_id': user_id}) return UserPublic.parse_obj(result._mapping) if result is not None else None diff --git a/backend/bracket/utils/conversion.py b/backend/bracket/utils/conversion.py index 293d08d2..7ba62b45 100644 --- a/backend/bracket/utils/conversion.py +++ b/backend/bracket/utils/conversion.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from pydantic import BaseModel diff --git a/backend/bracket/utils/db.py b/backend/bracket/utils/db.py index be5c1bf1..cbbebcbb 100644 --- a/backend/bracket/utils/db.py +++ b/backend/bracket/utils/db.py @@ -1,5 +1,3 @@ -from typing import Type - from databases import Database from sqlalchemy import Table from sqlalchemy.sql import Select @@ -10,27 +8,27 @@ from bracket.utils.types import BaseModelT, assert_some async def fetch_one_parsed( - database: Database, model: Type[BaseModelT], query: Select + database: Database, model: type[BaseModelT], query: Select ) -> BaseModelT | None: record = await database.fetch_one(query) return model.parse_obj(record._mapping) if record is not None else None async def fetch_one_parsed_certain( - database: Database, model: Type[BaseModelT], query: Select + database: Database, model: type[BaseModelT], query: Select ) -> BaseModelT: return assert_some(await fetch_one_parsed(database, model, query)) async def fetch_all_parsed( - database: Database, model: Type[BaseModelT], query: Select + database: Database, model: type[BaseModelT], query: Select ) -> list[BaseModelT]: records = await database.fetch_all(query) return [model.parse_obj(record._mapping) for record in records] async def insert_generic( - database: Database, data_model: BaseModelT, table: Table, return_type: Type[BaseModelT] + database: Database, data_model: BaseModelT, table: Table, return_type: type[BaseModelT] ) -> tuple[int, BaseModelT]: try: last_record_id: int = await database.execute( diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 70b7fc3c..c7719bdf 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -1,5 +1,6 @@ +from typing import TYPE_CHECKING + from heliclockter import datetime_utc -from sqlalchemy import Table from bracket.config import Environment, config, environment from bracket.database import database, engine @@ -65,6 +66,9 @@ from bracket.utils.logging import logger from bracket.utils.security import pwd_context from bracket.utils.types import BaseModelT +if TYPE_CHECKING: + from sqlalchemy import Table + async def create_admin_user() -> int: assert config.admin_email diff --git a/backend/bracket/utils/types.py b/backend/bracket/utils/types.py index 75a1c22d..56ec14cd 100644 --- a/backend/bracket/utils/types.py +++ b/backend/bracket/utils/types.py @@ -1,10 +1,13 @@ from __future__ import annotations from enum import Enum -from typing import Any, NewType, Sequence, TypeVar +from typing import TYPE_CHECKING, Any, NewType, TypeVar from pydantic import BaseModel +if TYPE_CHECKING: + from collections.abc import Sequence + BaseModelT = TypeVar('BaseModelT', bound=BaseModel) T = TypeVar('T') JsonDict = dict[str, Any] diff --git a/backend/bracket/uvicorn.py b/backend/bracket/uvicorn.py index ee4a9645..87c25fc8 100644 --- a/backend/bracket/uvicorn.py +++ b/backend/bracket/uvicorn.py @@ -2,7 +2,7 @@ import os import signal import threading import time -from typing import Any, Dict, List +from typing import Any from uvicorn.workers import UvicornWorker @@ -21,7 +21,7 @@ class ReloaderThread(threading.Thread): class RestartableUvicornWorker(UvicornWorker): - def __init__(self, *args: List[Any], **kwargs: Dict[str, Any]): + def __init__(self, *args: list[Any], **kwargs: dict[str, Any]): super().__init__(*args, **kwargs) self._reloader_thread = ReloaderThread(self) diff --git a/backend/precommit.sh b/backend/precommit.sh index f8f1f23a..264f32f6 100755 --- a/backend/precommit.sh +++ b/backend/precommit.sh @@ -2,7 +2,7 @@ set -evo pipefail black . +ruff --fix . dmypy run -- --follow-imports=normal --junit-xml= . ENVIRONMENT=CI pytest --cov --cov-report=xml . -vvv pylint alembic bracket tests -isort . diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9c0c508d..4aa20730 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -76,6 +76,7 @@ disable = [ 'logging-fstring-interpolation', 'too-many-arguments', 'unspecified-encoding', + 'wrong-import-position', ] [tool.bandit] @@ -84,3 +85,34 @@ skips = [ 'B106', 'B108' ] + + +[tool.ruff] +select = [ + "E", + "EXE", +# "ERA", TODO + "F", + "FA", + "FIX", + "I", + "ISC", + "PGH", + "PIE", + "PLE", + "PLW", + "RUF100", + "T20", + "TCH", + "TD", + "TID", + "UP", + "W", +] +ignore = [] +line-length = 100 +respect-gitignore = false +show-fixes = true +show-source = true +target-version = "py311" +exclude = [".mypy_cache", ".pytest_cache", "__pycache__"] \ No newline at end of file diff --git a/backend/tests/integration_tests/api/auth_test.py b/backend/tests/integration_tests/api/auth_test.py index 4af9d163..e3bbd88b 100644 --- a/backend/tests/integration_tests/api/auth_test.py +++ b/backend/tests/integration_tests/api/auth_test.py @@ -1,5 +1,5 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Generator from unittest.mock import Mock, patch import jwt diff --git a/backend/tests/integration_tests/api/conftest.py b/backend/tests/integration_tests/api/conftest.py index 7a195478..25bd4267 100644 --- a/backend/tests/integration_tests/api/conftest.py +++ b/backend/tests/integration_tests/api/conftest.py @@ -2,8 +2,8 @@ import asyncio import os from asyncio import AbstractEventLoop +from collections.abc import AsyncIterator from time import sleep -from typing import AsyncIterator import pytest from databases import Database diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index 710bb06f..29951a10 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -210,7 +210,6 @@ async def test_upcoming_matches_endpoint( json_response = await send_tournament_request( HTTPMethod.GET, f'rounds/{round_inserted.id}/upcoming_matches', auth_context, {} ) - print(json_response) assert json_response == { 'data': [ { diff --git a/backend/tests/integration_tests/api/shared.py b/backend/tests/integration_tests/api/shared.py index d4dbdc8f..93f6e449 100644 --- a/backend/tests/integration_tests/api/shared.py +++ b/backend/tests/integration_tests/api/shared.py @@ -1,7 +1,8 @@ import asyncio import socket +from collections.abc import Sequence from contextlib import closing -from typing import Final, Optional, Sequence +from typing import Final import aiohttp import uvicorn @@ -45,11 +46,11 @@ class UvicornTestServer(uvicorn.Server): def __init__(self, _app: FastAPI = app, host: str = TEST_HOST, port: int = TEST_PORT): self._startup_done = asyncio.Event() - self._serve_task: Optional[asyncio.Task[None]] = None + self._serve_task: asyncio.Task[None] | None = None self.should_exit: bool = False super().__init__(config=uvicorn.Config(_app, host=host, port=port)) - async def startup(self, sockets: Optional[Sequence[socket.socket]] = None) -> None: + async def startup(self, sockets: Sequence[socket.socket] | None = None) -> None: sockets_list = list(sockets) if sockets is not None else sockets await super().startup(sockets=sockets_list) self.config.setup_event_loop() diff --git a/backend/tests/integration_tests/api/users_test.py b/backend/tests/integration_tests/api/users_test.py index 47fa2752..c602e806 100644 --- a/backend/tests/integration_tests/api/users_test.py +++ b/backend/tests/integration_tests/api/users_test.py @@ -42,7 +42,6 @@ async def test_update_user( response = await send_auth_request( HTTPMethod.PATCH, f'users/{auth_context.user.id}', auth_context, None, body ) - print(response) patched_user = await fetch_one_parsed_certain( database, User, query=users.select().where(users.c.id == auth_context.user.id) ) diff --git a/backend/tests/integration_tests/sql.py b/backend/tests/integration_tests/sql.py index 409cd8fd..730b2092 100644 --- a/backend/tests/integration_tests/sql.py +++ b/backend/tests/integration_tests/sql.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from typing import AsyncIterator, Type, cast +from typing import cast from sqlalchemy import Table @@ -42,7 +43,7 @@ async def assert_row_count_and_clear(table: Table, expected_rows: int) -> None: @asynccontextmanager async def inserted_generic( - data_model: BaseModelT, table: Table, return_type: Type[BaseModelT] + data_model: BaseModelT, table: Table, return_type: type[BaseModelT] ) -> AsyncIterator[BaseModelT]: last_record_id, row_inserted = await insert_generic(database, data_model, table, return_type)