Add ruff check (#294)

This commit is contained in:
Erik Vroon
2023-10-07 17:02:05 +02:00
committed by GitHub
parent 6c741fd821
commit f4d7aae2ea
24 changed files with 118 additions and 74 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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')

View File

@@ -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
"""

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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 *
'''

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -1,4 +1,5 @@
from typing import Any, Mapping
from collections.abc import Mapping
from typing import Any
from pydantic import BaseModel

View File

@@ -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(

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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 .

View File

@@ -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__"]

View File

@@ -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

View File

@@ -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

View File

@@ -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': [
{

View File

@@ -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()

View File

@@ -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)
)

View File

@@ -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)