Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
run: uv sync --locked
- name: Run tests
run: uv run --locked pytest
- name: Run pre-commit
- name: Run pre-commit checks
run: uv run --locked pre-commit run --verbose --all-files --show-diff-on-failure
# FIXME: mypy is failing due to missing types-* packages
# - name: Run mypy
Expand Down
10 changes: 3 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ repos:
additional_dependencies:
- tomli

- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.285
rev: v0.16.6
hooks:
- id: ruff
- id: ruff-check
- id: ruff-format
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# algorithms-keeper
[![CI](https://github.com/TheAlgorithms/algorithms-keeper/actions/workflows/main.yml/badge.svg)](https://github.com/TheAlgorithms/algorithms-keeper/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/TheAlgorithms/algorithms-keeper/branch/master/graph/badge.svg?token=QYAZ665UJL)](https://codecov.io/gh/TheAlgorithms/algorithms-keeper)
[![code style: black](https://img.shields.io/static/v1?label=code%20style&message=black&color=black)](https://github.com/psf/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/)
[![Checked with mypy](https://img.shields.io/static/v1?label=mypy&message=checked&color=2a6db2&labelColor=505050)](http://mypy-lang.org/)

</div>
Expand Down Expand Up @@ -57,8 +57,11 @@ the project and run tests from the repository root:
```shell
uv sync
uv run pytest
uv run pre-commit run --all-files
```

Ruff handles Python linting and formatting through the pre-commit hooks.

## Logging
Logging is done using the standard library logging module. All the API calls made by the bot are being logged at INFO level and `aiohttp.log.access_logger` is logging the POST requests made by GitHub for delivering the payload. Other minor events relevant to the repository is also being logged along with using the using [Sentry](https://sentry.io/). The logs can be viewed best using the following command ([_requires Heroku CLI_](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)):
```shell
Expand Down
17 changes: 10 additions & 7 deletions algorithms_keeper/__main__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import asyncio
import logging
import os
import sys
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, MutableMapping
from typing import TYPE_CHECKING, Any

from aiohttp import ClientSession, web
from cachetools import LRUCache
Expand All @@ -15,10 +16,12 @@
from algorithms_keeper.api import GitHubAPI
from algorithms_keeper.event import main_router

if TYPE_CHECKING:
from collections.abc import MutableMapping

# TODO(dhruvmanila): Remove this block when it's the default.
# https://github.com/Instagram/LibCST/issues/285#issuecomment-1011427731
if sys.version_info >= (3, 10):
os.environ["LIBCST_PARSER_TYPE"] = "native"
os.environ["LIBCST_PARSER_TYPE"] = "native"

cache: MutableMapping[Any, Any] = LRUCache(maxsize=500)

Expand Down Expand Up @@ -90,7 +93,7 @@ async def main(request: web.Request) -> web.Response:
logger.info(
"ratelimit=%s, time_remaining=%s",
f"{gh.rate_limit.remaining}/{gh.rate_limit.limit}",
gh.rate_limit.reset_datetime - datetime.now(timezone.utc),
gh.rate_limit.reset_datetime - datetime.now(UTC),
)
return web.Response(status=200)
except Exception as err:
Expand All @@ -103,4 +106,4 @@ async def main(request: web.Request) -> web.Response:
app.add_routes(routes)
# Heroku dynamically assigns the app a port, so we can't set the port to a fixed
# number. Heroku adds the port to the env, so we need to pull it from there.
web.run_app(app, port=int(os.environ.get("PORT", 5000)))
web.run_app(app, port=int(os.environ.get("PORT", "5000")))
10 changes: 8 additions & 2 deletions algorithms_keeper/api.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from __future__ import annotations

import logging
import os
from typing import Any, Mapping, MutableMapping
from typing import TYPE_CHECKING, Any

from aiohttp import ClientResponse
from cachetools import TTLCache
from gidgethub import apps
from gidgethub.abc import UTF_8_CHARSET
from gidgethub.aiohttp import GitHubAPI as BaseGitHubAPI

if TYPE_CHECKING:
from collections.abc import Mapping, MutableMapping

from aiohttp import ClientResponse

# Timed token_cache for installation access token (1 minute less than an hour)
token_cache: MutableMapping[int, str] = TTLCache(maxsize=10, ttl=1 * 59 * 60)

Expand Down
11 changes: 8 additions & 3 deletions algorithms_keeper/event/check_run.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations

import logging
from typing import Any
from typing import TYPE_CHECKING, Any

from gidgethub import routing
from gidgethub.sansio import Event

from algorithms_keeper import utils
from algorithms_keeper.api import GitHubAPI
from algorithms_keeper.constants import Label

if TYPE_CHECKING:
from gidgethub.sansio import Event

from algorithms_keeper.api import GitHubAPI

check_run_router = routing.Router()

logger = logging.getLogger(__package__)
Expand Down
13 changes: 10 additions & 3 deletions algorithms_keeper/event/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
including the modified files. As we cannot post review comments on lines not part of
the diff, this command only modify the labels accordingly.
"""

from __future__ import annotations

import logging
import re
from typing import Any, Pattern
from re import Pattern
from typing import TYPE_CHECKING, Any

from gidgethub import routing
from gidgethub.sansio import Event

from algorithms_keeper import utils
from algorithms_keeper.api import GitHubAPI
from algorithms_keeper.event.pull_request import check_pr_files

if TYPE_CHECKING:
from gidgethub.sansio import Event

from algorithms_keeper.api import GitHubAPI

commands_router = routing.Router()

COMMAND_RE: Pattern[str] = re.compile(r"@algorithms-keeper\s+([a-z\-]+)", re.IGNORECASE)
Expand Down
11 changes: 8 additions & 3 deletions algorithms_keeper/event/installation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from __future__ import annotations

import logging
from typing import Any
from typing import TYPE_CHECKING, Any

from gidgethub import routing
from gidgethub.sansio import Event

from algorithms_keeper.api import GitHubAPI
from algorithms_keeper.constants import GREETING_COMMENT

if TYPE_CHECKING:
from gidgethub.sansio import Event

from algorithms_keeper.api import GitHubAPI

installation_router = routing.Router()

logger = logging.getLogger(__package__)
Expand Down
19 changes: 12 additions & 7 deletions algorithms_keeper/event/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
"Awaiting changes" -> "Approved" [label="Review approves", color=green]
}
"""

from __future__ import annotations

import asyncio
import logging
import re
from typing import Any, Optional
from typing import TYPE_CHECKING, Any

from gidgethub import routing
from gidgethub.sansio import Event

from algorithms_keeper import utils
from algorithms_keeper.api import GitHubAPI
from algorithms_keeper.constants import (
CHECKBOX_NOT_TICKED_COMMENT,
EMPTY_PR_BODY_COMMENT,
Expand All @@ -37,8 +38,14 @@
PR_REVIEW_COMMENT,
Label,
)
from algorithms_keeper.event.check_run import check_ci_status_and_label
from algorithms_keeper.parser import PythonParser

if TYPE_CHECKING:
from gidgethub.sansio import Event

from algorithms_keeper.api import GitHubAPI

# To disable this check, set the constant to 0.
MAX_PR_PER_USER = 3
STAGE_PREFIX = "awaiting"
Expand All @@ -50,7 +57,7 @@


async def update_stage_label(
gh: GitHubAPI, *, pull_request: dict[str, Any], next_label: Optional[str] = None
gh: GitHubAPI, *, pull_request: dict[str, Any], next_label: str | None = None
) -> None:
"""Update the stage label of the given pull request.

Expand Down Expand Up @@ -266,8 +273,6 @@ async def check_ci_ready_for_review_pr(
removed if the checks are passing or failing. Thus, we need to manually check it
with respect to the latest commit on head.
"""
from algorithms_keeper.event.check_run import check_ci_status_and_label

await check_ci_status_and_label(event, gh, *args, **kwargs)


Expand Down Expand Up @@ -343,7 +348,7 @@ async def check_merge_status(
pull_request = event.data["pull_request"]

for retry_interval in range(MAX_RETRIES):
mergeable: Optional[bool] = pull_request["mergeable"]
mergeable: bool | None = pull_request["mergeable"]
if mergeable is None:
# We will use the iter value we get as our sleep period between the polls.
# In the webhook payload, the mergeable status will always be ``None``, so
Expand Down
10 changes: 8 additions & 2 deletions algorithms_keeper/parser/files_parser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from __future__ import annotations

import logging
from typing import Any, Collection, Iterable, Mapping
from typing import TYPE_CHECKING, Any

from algorithms_keeper.constants import Label
from algorithms_keeper.utils import File

if TYPE_CHECKING:
from collections.abc import Collection, Iterable, Mapping

from algorithms_keeper.utils import File

# These files are updated automatically by a GitHub action in almost every pull request.
IGNORE_FILES_FOR_TYPELABEL: set[str] = {"DIRECTORY.md"}
Expand Down
8 changes: 7 additions & 1 deletion algorithms_keeper/parser/lint_rule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from fixit import LintRule
from libcst import CSTNode

if TYPE_CHECKING:
from libcst import CSTNode


class ReviewLintRule(LintRule):
Expand Down
10 changes: 8 additions & 2 deletions algorithms_keeper/parser/python_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import logging
from typing import Any, Iterable, Iterator, Mapping
from typing import TYPE_CHECKING, Any

from fixit import Config, LintRule
from fixit.engine import LintRunner
Expand All @@ -14,7 +16,11 @@
RequireTypeHintRule,
UseFstringRule,
)
from algorithms_keeper.utils import File

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping

from algorithms_keeper.utils import File

# Select only the bot's review rules, independent of Fixit's built-in defaults.
DEFAULT_RULES: frozenset[type[LintRule]] = frozenset(
Expand Down
20 changes: 11 additions & 9 deletions algorithms_keeper/parser/record.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from dataclasses import asdict, dataclass, field
from typing import Any, Collection, Union
from __future__ import annotations

from fixit import LintViolation
from libcst import ParserSyntaxError
import traceback
from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING, Any

from algorithms_keeper.constants import Label

if TYPE_CHECKING:
from collections.abc import Collection

from fixit import LintViolation
from libcst import ParserSyntaxError

# Mapping of rule to the appropriate label.
RULE_TO_LABEL: dict[str, str] = {
"RequireDescriptiveName": Label.DESCRIPTIVE_NAME,
Expand Down Expand Up @@ -73,12 +79,8 @@ def add_comments(self, reports: Collection[LintViolation], filepath: str) -> Non
ReviewComment(report.message, filepath, report.range.start.line)
)

def add_error(
self, exc: Union[SyntaxError, ParserSyntaxError], filepath: str
) -> None:
def add_error(self, exc: SyntaxError | ParserSyntaxError, filepath: str) -> None:
"""Add any exception faced while parsing the source code."""
import traceback

message = traceback.format_exc(limit=1)
# It seems that ``ParserSyntaxError`` is not a subclass of ``SyntaxError``,
# the same information is stored under a different attribute. There is no
Expand Down
7 changes: 5 additions & 2 deletions algorithms_keeper/parser/rules/naming_convention.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Collection, Optional
from typing import TYPE_CHECKING

import libcst as cst
import libcst.matchers as m
Expand All @@ -8,6 +8,9 @@

from algorithms_keeper.parser import lint_rule

if TYPE_CHECKING:
from collections.abc import Collection

INVALID_CAMEL_CASE_NAME_COMMENT: str = (
"Class names should follow the [`CamelCase`]"
"(https://en.wikipedia.org/wiki/Camel_case) naming convention. "
Expand Down Expand Up @@ -116,7 +119,7 @@ def __init__(self) -> None:
self._assigntarget_counter: int = 0

def visit_Assign(self, node: cst.Assign) -> None:
metadata: Optional[Collection[QualifiedName]] = self.get_metadata(
metadata: Collection[QualifiedName] | None = self.get_metadata(
QualifiedNameProvider, node.value, None
)

Expand Down
10 changes: 7 additions & 3 deletions algorithms_keeper/parser/rules/require_descriptive_name.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import Union
from __future__ import annotations

from typing import TYPE_CHECKING

import libcst as cst
from fixit import Invalid, Valid

from algorithms_keeper.parser import lint_rule

if TYPE_CHECKING:
import libcst as cst

MESSAGE: str = "Please provide descriptive name for the {nodetype}: `{nodename}`"


Expand Down Expand Up @@ -75,7 +79,7 @@ def visit_Param(self, node: cst.Param) -> None:
self._validate_name_length(node, "parameter")

def _validate_name_length(
self, node: Union[cst.ClassDef, cst.FunctionDef, cst.Param], nodetype: str
self, node: cst.ClassDef | cst.FunctionDef | cst.Param, nodetype: str
) -> None:
nodename = node.name.value
if len(nodename) == 1:
Expand Down
Loading
Loading