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
3 changes: 1 addition & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
# fixit 0.1.4 imports the stdlib ``distutils`` module, which was removed
# in Python 3.12. Pin to 3.11 until the fixit pin is upgraded.
# Keep a fixed runtime for the full dependency set.
python-version: "3.11"
- uses: actions/cache@v3
with:
Expand Down
10 changes: 10 additions & 0 deletions algorithms_keeper/parser/lint_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fixit import LintRule
from libcst import CSTNode


class ReviewLintRule(LintRule):
"""Review every violation, including code with lint suppression comments."""

def ignore_lint(self, node: CSTNode) -> bool:
# Preserve the bot's use_ignore_comments=False policy from Fixit 0.1.
return False
71 changes: 28 additions & 43 deletions algorithms_keeper/parser/python_parser.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,35 @@
import importlib
import inspect
import logging
from typing import Any, Iterable, Iterator, Mapping

from fixit import CstLintRule, LintConfig
from fixit.common.utils import LintRuleCollectionT
from fixit.rule_lint_engine import lint_file
from fixit import Config, LintRule
from fixit.engine import LintRunner
from libcst import ParserSyntaxError

from algorithms_keeper.parser.files_parser import BaseFilesParser
from algorithms_keeper.parser.record import PullRequestReviewRecord
from algorithms_keeper.parser.rules import RequireDoctestRule
from algorithms_keeper.parser.rules import (
NamingConventionRule,
RequireDescriptiveNameRule,
RequireDoctestRule,
RequireTypeHintRule,
UseFstringRule,
)
from algorithms_keeper.utils import File

RULES_DOTPATH: str = "algorithms_keeper.parser.rules"

DEFAULT_CONFIG: LintConfig = LintConfig(packages=[RULES_DOTPATH])
# Select only the bot's review rules, independent of Fixit's built-in defaults.
DEFAULT_RULES: frozenset[type[LintRule]] = frozenset(
{
NamingConventionRule,
RequireDescriptiveNameRule,
RequireDoctestRule,
RequireTypeHintRule,
UseFstringRule,
}
)

logger = logging.getLogger(__package__)


def get_rules_from_config(config: LintConfig = DEFAULT_CONFIG) -> LintRuleCollectionT:
"""Get rules from the packages specified in the lint config file, omitting
block-listed rules.

Custom rules should be imported in the ``__init__`` file of the rules package
along with all the rules used from ``fixit``. Also, make sure all the rule classes
have the suffix `Rule`.
"""
rules: LintRuleCollectionT = set()
block_list_rules = config.block_list_rules
for package in config.packages:
pkg = importlib.import_module(package)
for name in dir(pkg):
if name.endswith("Rule"):
obj = getattr(pkg, name)
if (
obj is not CstLintRule
and issubclass(obj, CstLintRule)
and not inspect.isabstract(obj)
and name not in block_list_rules
):
rules.add(obj)
return rules


class PythonParser(BaseFilesParser):
"""Parser for all the Python files in the pull request.

Expand All @@ -57,7 +42,7 @@ class PythonParser(BaseFilesParser):
"""

_pr_report: PullRequestReviewRecord
_rules: LintRuleCollectionT
_rules: set[type[LintRule]]

DOCS_EXTENSIONS: tuple[str, ...] = (".md", ".rst")

Expand Down Expand Up @@ -92,7 +77,7 @@ def __init__(
self._pr_record = PullRequestReviewRecord()
# Collection of rules are going to be static for a pull request, so let's
# extract it out and store it.
self._rules = get_rules_from_config()
self._rules = set(DEFAULT_RULES)
# If the pull request contains a test file as per the naming convention, there's
# no need to run ``RequireDoctestRule``.
if self._contains_testfile():
Expand Down Expand Up @@ -149,13 +134,13 @@ def files_to_check(self, ignore_modified: bool) -> Iterator[File]:
def parse(self, file: File, source: bytes) -> None:
"""Run the lint engine on the given *source* for the *file*."""
try:
reports = lint_file(
file.path,
source,
use_ignore_byte_markers=False,
use_ignore_comments=False,
config=DEFAULT_CONFIG,
rules=self._rules,
runner = LintRunner(file.path, source)
# Rules retain visitor state and violations; create them for each file.
reports = list(
runner.collect_violations(
[rule() for rule in self._rules],
Config(path=file.path, enable=[]),
)
)
self._pr_record.add_comments(reports, file.name)
except (SyntaxError, ParserSyntaxError) as exc:
Expand Down
20 changes: 10 additions & 10 deletions algorithms_keeper/parser/record.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from dataclasses import asdict, dataclass, field
from typing import Any, Collection, Union

from fixit.common.report import BaseLintRuleReport
from fixit import LintViolation
from libcst import ParserSyntaxError

from algorithms_keeper.constants import Label

# Mapping of rule to the appropriate label.
RULE_TO_LABEL: dict[str, str] = {
"RequireDescriptiveNameRule": Label.DESCRIPTIVE_NAME,
"RequireDoctestRule": Label.REQUIRE_TEST,
"RequireTypeHintRule": Label.TYPE_HINT,
"RequireDescriptiveName": Label.DESCRIPTIVE_NAME,
"RequireDoctest": Label.REQUIRE_TEST,
"RequireTypeHint": Label.TYPE_HINT,
}

MULTIPLE_COMMENT_SEPARATOR: str = "\n\n"
Expand Down Expand Up @@ -58,20 +58,20 @@ class PullRequestReviewRecord:
# duplication.
_violated_rules: set[str] = field(default_factory=set, init=False, repr=False)

def add_comments(
self, reports: Collection[BaseLintRuleReport], filepath: str
) -> None:
def add_comments(self, reports: Collection[LintViolation], filepath: str) -> None:
"""Construct and add comments from the reports.

If the line on which the comment is to be posted already exists, then the
*body* is simply added to the respective comment's body provided it is in the
same file. This is done to avoid adding multiple comments on the same line.
"""
for report in reports:
self._violated_rules.add(report.code)
if self._lineno_exist(report.message, filepath, report.line):
self._violated_rules.add(report.rule_name)
if self._lineno_exist(report.message, filepath, report.range.start.line):
continue
self._comments.append(ReviewComment(report.message, filepath, report.line))
self._comments.append(
ReviewComment(report.message, filepath, report.range.start.line)
)

def add_error(
self, exc: Union[SyntaxError, ParserSyntaxError], filepath: str
Expand Down
12 changes: 6 additions & 6 deletions algorithms_keeper/parser/rules/naming_convention.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

import libcst as cst
import libcst.matchers as m
from fixit import CstContext, CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
from fixit import Invalid, Valid
from libcst.metadata import QualifiedName, QualifiedNameProvider

from algorithms_keeper.parser import lint_rule

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 @@ -40,7 +40,7 @@ def valid(self, name: str) -> bool:
return True


class NamingConventionRule(CstLintRule):
class NamingConventionRule(lint_rule.ReviewLintRule):
METADATA_DEPENDENCIES = (QualifiedNameProvider,) # type: ignore

VALID = [
Expand Down Expand Up @@ -111,8 +111,8 @@ def __init__(self, foo, bar):
),
]

def __init__(self, context: CstContext) -> None:
super().__init__(context)
def __init__(self) -> None:
super().__init__()
self._assigntarget_counter: int = 0

def visit_Assign(self, node: cst.Assign) -> None:
Expand Down
8 changes: 4 additions & 4 deletions algorithms_keeper/parser/rules/require_descriptive_name.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Union

import libcst as cst
from fixit import CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
from fixit import Invalid, Valid

from algorithms_keeper.parser import lint_rule

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


class RequireDescriptiveNameRule(CstLintRule):
class RequireDescriptiveNameRule(lint_rule.ReviewLintRule):
VALID = [
Valid(
"""
Expand Down
41 changes: 20 additions & 21 deletions algorithms_keeper/parser/rules/require_doctest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from pathlib import Path
from typing import Union

import libcst as cst
import libcst.matchers as m
from fixit import CstContext, CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
from fixit import Invalid, Valid
from libcst.metadata import FilePathProvider

from algorithms_keeper.parser import lint_rule

MISSING_DOCTEST: str = (
"As there is no test file in this pull request nor any test function or class in "
Expand All @@ -14,7 +16,9 @@
INIT: str = "__init__"


class RequireDoctestRule(CstLintRule):
class RequireDoctestRule(lint_rule.ReviewLintRule):
METADATA_DEPENDENCIES = (FilePathProvider,)

VALID = [
# Module-level docstring contains doctest.
Valid(
Expand Down Expand Up @@ -150,14 +154,6 @@ def bar(self):
pass
"""
),
# No doctest required in the ``web_programming`` directory.
Valid(
"""
def foo():
pass
""",
filename="web_programming/foo.py",
),
]

INVALID = [
Expand Down Expand Up @@ -206,16 +202,21 @@ def egg():
),
]

def __init__(self, context: CstContext) -> None:
super().__init__(context)
def __init__(self) -> None:
super().__init__()
self._skip_doctest: bool = False
self._temporary: bool = False

def should_skip_file(self) -> bool:
return self.context.file_path.match("web_programming/*")

def visit_Module(self, node: cst.Module) -> None:
self._skip_doctest = self._has_testnode(node) or self._has_doctest(node)
# LibCST supplies an absolute path; GitHub reviews use relative paths.
self._file_path = self.get_metadata(FilePathProvider, node).relative_to(
Path.cwd()
)
self._skip_doctest = (
self._file_path.match("web_programming/*")
or self._has_testnode(node)
or self._has_doctest(node)
)

def visit_ClassDef(self, node: cst.ClassDef) -> None:
# Temporary storage of the ``skip_doctest`` value only during the class visit.
Expand All @@ -234,9 +235,7 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> None:
if nodename != INIT and not self._has_doctest(node):
self.report(
node,
MISSING_DOCTEST.format(
filepath=self.context.file_path, nodename=nodename
),
MISSING_DOCTEST.format(filepath=self._file_path, nodename=nodename),
)

def _has_doctest(
Expand Down
12 changes: 6 additions & 6 deletions algorithms_keeper/parser/rules/require_type_hint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import libcst as cst
from fixit import CstContext, CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
from fixit import Invalid, Valid

from algorithms_keeper.parser import lint_rule

MISSING_TYPE_HINT: str = "Please provide type hint for the parameter: `{nodename}`"

Expand All @@ -14,7 +14,7 @@
IGNORE_PARAM: set[str] = {"self", "cls"}


class RequireTypeHintRule(CstLintRule):
class RequireTypeHintRule(lint_rule.ReviewLintRule):
VALID = [
Valid(
"""
Expand Down Expand Up @@ -107,8 +107,8 @@ def wrapper(call) -> None:
),
]

def __init__(self, context: CstContext) -> None:
super().__init__(context)
def __init__(self) -> None:
super().__init__()
self._lambda_counter: int = 0

def visit_Lambda(self, node: cst.Lambda) -> None:
Expand Down
8 changes: 4 additions & 4 deletions algorithms_keeper/parser/rules/use_fstring.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import libcst as cst
import libcst.matchers as m
from fixit import CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
from fixit import Invalid, Valid

from algorithms_keeper.parser import lint_rule

class UseFstringRule(CstLintRule):

class UseFstringRule(lint_rule.ReviewLintRule):
MESSAGE: str = (
"As mentioned in the [Contributing Guidelines]"
"(https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md), "
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
aiohttp==3.9.0
cachetools==5.3.2
fixit==0.1.4
fixit==2.2.1
gidgethub==5.3.0
libcst==1.1.0
sentry-sdk==1.32.0
Loading
Loading