From d07c9f27e053e173afd5006b59b6159ebff943c7 Mon Sep 17 00:00:00 2001 From: meanmail Date: Thu, 26 Sep 2024 13:34:23 +0200 Subject: [PATCH] Add type hints, assertions, and minor refactorings This commit introduces type hints, assertions, and minor code refactorings across multiple modules to enhance type safety and code readability. It updates type annotations, adds necessary assertions, improves list initializations, and removes redundant code. Additionally, the commit upgrades dependencies in the `pyproject.toml` file. --- .github/workflows/build.yml | 16 +++++ .../src/python/common/tool_arguments.py | 12 ++-- .../src/python/review/application_config.py | 2 +- .../src/python/review/common/file_system.py | 4 -- .../python/review/common/parallel_runner.py | 12 ++-- .../common/inspector/base_inspector.py | 4 -- .../inspectors/common/inspector/ij_client.py | 6 +- .../common/issue/base_issue_converter.py | 4 +- .../review/inspectors/common/issue/issue.py | 34 +++++----- .../inspectors/common/issue/issue_configs.py | 6 +- .../review/inspectors/common/xml_parser.py | 4 +- .../python/review/inspectors/flake8/flake8.py | 1 + .../review/inspectors/ij_java/ij_java.py | 2 +- .../review/inspectors/ij_java/issue_types.py | 9 ++- .../review/inspectors/ij_kotlin/ij_kotlin.py | 2 +- .../inspectors/ij_kotlin/issue_types.py | 9 ++- .../review/inspectors/ij_python/ij_python.py | 2 +- .../inspectors/ij_python/issue_types.py | 4 +- .../review/inspectors/pyast/python_ast.py | 2 +- .../python/review/inspectors/pylint/pylint.py | 4 +- .../python/review/inspectors/radon/radon.py | 4 +- .../src/python/review/logging_config.py | 6 +- hyperstyle/src/python/review/quality/model.py | 11 +++- .../src/python/review/quality/penalty.py | 24 ++++--- .../quality/rules/best_practices_scoring.py | 5 +- .../quality/rules/boolean_length_scoring.py | 5 +- .../quality/rules/class_response_scoring.py | 5 +- .../quality/rules/code_style_scoring.py | 9 ++- .../review/quality/rules/cohesion_scoring.py | 5 +- .../quality/rules/complexity_scoring.py | 5 +- .../review/quality/rules/coupling_scoring.py | 5 +- .../rules/cyclomatic_complexity_scoring.py | 5 +- .../quality/rules/error_prone_scoring.py | 5 +- .../quality/rules/function_length_scoring.py | 5 +- .../rules/inheritance_depth_scoring.py | 5 +- .../review/quality/rules/line_len_scoring.py | 3 +- .../quality/rules/maintainability_scoring.py | 5 +- .../quality/rules/method_number_scoring.py | 5 +- .../quality/rules/weighted_methods_scoring.py | 5 +- .../src/python/review/reviewers/common.py | 37 ++++++----- .../review/reviewers/utils/code_statistics.py | 8 ++- .../review/reviewers/utils/issues_filter.py | 4 +- .../reviewers/utils/metadata_exploration.py | 4 +- .../review/reviewers/utils/print_review.py | 64 +++++++------------ hyperstyle/src/python/review/run_tool.py | 12 ++-- poetry.lock | 57 ++++++++++------- pyproject.toml | 3 +- .../test_filter_duplicate_issues.py | 13 ++-- test/python/inspectors/test_issue_configs.py | 2 +- 49 files changed, 276 insertions(+), 189 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b5f788b..ea731ce5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,22 @@ jobs: cd /review /hyperstyle/bin/pytest + type-checking: + name: Type checking + runs-on: [ self-hosted, small ] + needs: + - build_image + container: + image: hyperskill.azurecr.io/hyperstyle:${{ github.sha }} + credentials: + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + steps: + - name: Lint + run: | + cd /review + /hyperstyle/bin/mypy . + build: runs-on: [ self-hosted, small ] needs: diff --git a/hyperstyle/src/python/common/tool_arguments.py b/hyperstyle/src/python/common/tool_arguments.py index d4376cc0..785671d9 100644 --- a/hyperstyle/src/python/common/tool_arguments.py +++ b/hyperstyle/src/python/common/tool_arguments.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from enum import Enum, unique -from typing import ClassVar from hyperstyle.src.python.review.common.language import Language from hyperstyle.src.python.review.common.language_version import LanguageVersion @@ -30,6 +29,10 @@ class ArgumentsInfo: description: str +INSPECTORS = [inspector.lower() for inspector in InspectorType.available_values()] +DISABLED_INSPECTORS_EXAMPLE = f"-d {INSPECTORS[0].lower()},{INSPECTORS[1].lower()}" + + @unique class RunToolArgument(Enum): VERBOSITY = ArgumentsInfo( @@ -43,15 +46,12 @@ class RunToolArgument(Enum): "default is 0", ) - inspectors: ClassVar = [inspector.lower() for inspector in InspectorType.available_values()] - disabled_inspectors_example = f"-d {inspectors[0].lower()},{inspectors[1].lower()}" - DISABLE = ArgumentsInfo( "-d", "--disable", 'Disable inspectors. ' - f'Available values: {", ".join(inspectors)}. ' - f'Example: {disabled_inspectors_example}', + f'Available values: {", ".join(INSPECTORS)}. ' + f'Example: {DISABLED_INSPECTORS_EXAMPLE}', ) DUPLICATES = ArgumentsInfo( diff --git a/hyperstyle/src/python/review/application_config.py b/hyperstyle/src/python/review/application_config.py index dc177612..56ee6daa 100644 --- a/hyperstyle/src/python/review/application_config.py +++ b/hyperstyle/src/python/review/application_config.py @@ -13,7 +13,7 @@ class ApplicationConfig: disabled_inspectors: set[InspectorType] allow_duplicates: bool n_cpu: int - inspectors_config: dict + inspectors_config: dict[str, object] with_all_categories: bool start_line: int = 1 language: Language | None = None diff --git a/hyperstyle/src/python/review/common/file_system.py b/hyperstyle/src/python/review/common/file_system.py index aa39cc5c..7d785769 100644 --- a/hyperstyle/src/python/review/common/file_system.py +++ b/hyperstyle/src/python/review/common/file_system.py @@ -88,10 +88,6 @@ def new_temp_dir() -> Iterator[Path]: yield Path(temp_dir) -def new_temp_file(suffix: Extension = Extension.EMPTY) -> Iterator[tuple[str, str]]: - yield tempfile.mkstemp(suffix=suffix.value) - - def get_file_line(path: Path, line_number: int) -> str: return linecache.getline( str(path), diff --git a/hyperstyle/src/python/review/common/parallel_runner.py b/hyperstyle/src/python/review/common/parallel_runner.py index c2013fc0..b6380b37 100644 --- a/hyperstyle/src/python/review/common/parallel_runner.py +++ b/hyperstyle/src/python/review/common/parallel_runner.py @@ -56,9 +56,11 @@ def inspect_in_parallel( return issues with multiprocessing.Pool(config.n_cpu) as pool: - issues = pool.map( - functools.partial(inspector_runner, data, config), - inspectors_to_run, + return list( + itertools.chain( + *pool.map( + functools.partial(inspector_runner, data, config), + inspectors_to_run, + ) + ) ) - - return list(itertools.chain(*issues)) diff --git a/hyperstyle/src/python/review/inspectors/common/inspector/base_inspector.py b/hyperstyle/src/python/review/inspectors/common/inspector/base_inspector.py index 8ad519cc..12bc8122 100644 --- a/hyperstyle/src/python/review/inspectors/common/inspector/base_inspector.py +++ b/hyperstyle/src/python/review/inspectors/common/inspector/base_inspector.py @@ -138,10 +138,6 @@ def convert_to_base_issues( return base_issues def _get_inspection_result(self, code_text: str, file_path: Path) -> list[BaseIssue]: - if self.host is None or self.port is None: - msg = "Connection parameters is not set up." - raise Exception(msg) - client = IJClient(self.host, self.port) code = model_pb2.Code() diff --git a/hyperstyle/src/python/review/inspectors/common/inspector/ij_client.py b/hyperstyle/src/python/review/inspectors/common/inspector/ij_client.py index f06e42c8..4c651609 100644 --- a/hyperstyle/src/python/review/inspectors/common/inspector/ij_client.py +++ b/hyperstyle/src/python/review/inspectors/common/inspector/ij_client.py @@ -8,6 +8,8 @@ class IJClient: + stub: model_pb2_grpc.CodeInspectionServiceStub + def __init__(self, host: str = "localhost", port: int = 8080) -> None: self.host = host self.port = port @@ -22,10 +24,10 @@ def __init__(self, host: str = "localhost", port: int = 8080) -> None: msg = "Failed to connect to ij code server" raise Exception(msg) from e else: - self.stub = model_pb2_grpc.CodeInspectionServiceStub(self.channel) + self.stub = model_pb2_grpc.CodeInspectionServiceStub(self.channel) # type: ignore[no-untyped-call] def inspect(self, code: model_pb2.Code) -> model_pb2.InspectionResult: return self.stub.inspect(code, timeout=TIMEOUT) def init(self, service: model_pb2.Service) -> model_pb2.InitResult: - return self.stub.init(service, timeout=TIMEOUT) + return self.stub.init(service, timeout=TIMEOUT) # type: ignore[attr-defined] diff --git a/hyperstyle/src/python/review/inspectors/common/issue/base_issue_converter.py b/hyperstyle/src/python/review/inspectors/common/issue/base_issue_converter.py index ca3ce617..5739a91c 100644 --- a/hyperstyle/src/python/review/inspectors/common/issue/base_issue_converter.py +++ b/hyperstyle/src/python/review/inspectors/common/issue/base_issue_converter.py @@ -8,7 +8,7 @@ get_issue_class_by_issue_type, get_measure_name_by_measurable_issue_type, IssueData, - Measurable, + MeasurableIssue, ) if TYPE_CHECKING: @@ -36,7 +36,7 @@ def convert_base_issue(base_issue: BaseIssue, issue_configs_handler: IssueConfig issue_data[IssueData.DESCRIPTION.value] = issue_configs_handler.get_description(origin_class, description) issue_class = get_issue_class_by_issue_type(issue_type) - if issubclass(issue_class, Measurable): + if issubclass(issue_class, MeasurableIssue): measure = issue_configs_handler.parse_measure(origin_class, description) if measure is None: logger.error(f"{inspector_type.value}: Unable to parse measure.") diff --git a/hyperstyle/src/python/review/inspectors/common/issue/issue.py b/hyperstyle/src/python/review/inspectors/common/issue/issue.py index 8de9d492..7bfe76f1 100644 --- a/hyperstyle/src/python/review/inspectors/common/issue/issue.py +++ b/hyperstyle/src/python/review/inspectors/common/issue/issue.py @@ -1,11 +1,10 @@ from __future__ import annotations -import abc import logging from collections import defaultdict from dataclasses import dataclass from enum import Enum, unique -from typing import Any, TYPE_CHECKING +from typing import Any, Protocol, runtime_checkable, TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path @@ -206,10 +205,9 @@ class BaseIssue(ShortIssue): difficulty: IssueDifficulty -class Measurable(abc.ABC): - @abc.abstractmethod - def measure(self) -> int: - pass +@runtime_checkable +class MeasurableIssue(Protocol): + def measure(self) -> int: ... @dataclass(frozen=True) @@ -218,7 +216,7 @@ class CodeIssue(BaseIssue): @dataclass(frozen=True) -class BoolExprLenIssue(BaseIssue, Measurable): +class BoolExprLenIssue(BaseIssue, MeasurableIssue): bool_expr_len: int type = IssueType.BOOL_EXPR_LEN @@ -227,7 +225,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class FuncLenIssue(BaseIssue, Measurable): +class FuncLenIssue(BaseIssue, MeasurableIssue): func_len: int type = IssueType.FUNC_LEN @@ -236,7 +234,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class LineLenIssue(BaseIssue, Measurable): +class LineLenIssue(BaseIssue, MeasurableIssue): line_len: int type = IssueType.LINE_LEN @@ -245,7 +243,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class CyclomaticComplexityIssue(BaseIssue, Measurable): +class CyclomaticComplexityIssue(BaseIssue, MeasurableIssue): cc_value: int type = IssueType.CYCLOMATIC_COMPLEXITY @@ -254,7 +252,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class InheritanceIssue(BaseIssue, Measurable): +class InheritanceIssue(BaseIssue, MeasurableIssue): inheritance_tree_depth: int type = IssueType.INHERITANCE_DEPTH @@ -263,7 +261,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class ChildrenNumberIssue(BaseIssue, Measurable): +class ChildrenNumberIssue(BaseIssue, MeasurableIssue): children_number: int type = IssueType.CHILDREN_NUMBER @@ -272,7 +270,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class WeightedMethodIssue(BaseIssue, Measurable): +class WeightedMethodIssue(BaseIssue, MeasurableIssue): weighted_method: int type = IssueType.WEIGHTED_METHOD @@ -281,7 +279,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class CouplingIssue(BaseIssue, Measurable): +class CouplingIssue(BaseIssue, MeasurableIssue): class_objects_coupling: int type = IssueType.COUPLING @@ -290,7 +288,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class CohesionIssue(BaseIssue, Measurable): +class CohesionIssue(BaseIssue, MeasurableIssue): cohesion_lack: int type = IssueType.COHESION @@ -299,7 +297,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class ClassResponseIssue(BaseIssue, Measurable): +class ClassResponseIssue(BaseIssue, MeasurableIssue): class_response: int type = IssueType.CLASS_RESPONSE @@ -308,7 +306,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class MethodNumberIssue(BaseIssue, Measurable): +class MethodNumberIssue(BaseIssue, MeasurableIssue): method_number: int type = IssueType.METHOD_NUMBER @@ -317,7 +315,7 @@ def measure(self) -> int: @dataclass(frozen=True) -class MaintainabilityLackIssue(BaseIssue, Measurable): +class MaintainabilityLackIssue(BaseIssue, MeasurableIssue): maintainability_lack: int type = IssueType.MAINTAINABILITY diff --git a/hyperstyle/src/python/review/inspectors/common/issue/issue_configs.py b/hyperstyle/src/python/review/inspectors/common/issue/issue_configs.py index 431909cd..d5c7866e 100644 --- a/hyperstyle/src/python/review/inspectors/common/issue/issue_configs.py +++ b/hyperstyle/src/python/review/inspectors/common/issue/issue_configs.py @@ -2,7 +2,7 @@ import logging from dataclasses import dataclass, field -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from hyperstyle.src.python.review.inspectors.common.utils import ( contains_format_fields, @@ -136,7 +136,7 @@ def __init__(self, *issue_configs: IssueConfig) -> None: issue_config.origin_class: issue_config for issue_config in issue_configs } - def _parse_description(self, origin_class: str, description: str) -> tuple | None: + def _parse_description(self, origin_class: str, description: str) -> tuple[object, ...] | None: """Parse a description. :param origin_class: An origin class of issue. @@ -158,7 +158,7 @@ def _parse_description(self, origin_class: str, description: str) -> tuple | Non return args - def parse_measure(self, origin_class: str, description: str) -> Optional: + def parse_measure(self, origin_class: str, description: str) -> object | None: """Parse a measure from a description. :param origin_class: An origin class of issue. diff --git a/hyperstyle/src/python/review/inspectors/common/xml_parser.py b/hyperstyle/src/python/review/inspectors/common/xml_parser.py index 0ab99557..bf4c68d4 100644 --- a/hyperstyle/src/python/review/inspectors/common/xml_parser.py +++ b/hyperstyle/src/python/review/inspectors/common/xml_parser.py @@ -20,12 +20,12 @@ logger = logging.getLogger(__name__) -def __should_handle_element(element: ET) -> bool: +def __should_handle_element(element: Element) -> bool: """Checks if a tree element is a file.""" return element.tag == "file" -def __is_error(element: ET) -> bool: +def __is_error(element: Element) -> bool: """Checks if a tree element is an error.""" return element.tag == "error" diff --git a/hyperstyle/src/python/review/inspectors/flake8/flake8.py b/hyperstyle/src/python/review/inspectors/flake8/flake8.py index ca71bb20..d06a7d92 100644 --- a/hyperstyle/src/python/review/inspectors/flake8/flake8.py +++ b/hyperstyle/src/python/review/inspectors/flake8/flake8.py @@ -91,6 +91,7 @@ def choose_issue_type(code: str) -> IssueType: return CODE_TO_ISSUE_TYPE[code] regex_match = re.match(r"^([A-Z]+)(\d)\d*$", code, re.IGNORECASE) + assert regex_match is not None, f"flake8: {code} - unexpected error code format" code_prefix = regex_match.group(1) first_code_number = regex_match.group(2) diff --git a/hyperstyle/src/python/review/inspectors/ij_java/ij_java.py b/hyperstyle/src/python/review/inspectors/ij_java/ij_java.py index 5b3c43aa..d72ebe28 100644 --- a/hyperstyle/src/python/review/inspectors/ij_java/ij_java.py +++ b/hyperstyle/src/python/review/inspectors/ij_java/ij_java.py @@ -12,7 +12,7 @@ class JavaIJInspector(BaseIJInspector): inspector_type = InspectorType.IJ_JAVA - language_id = model_pb2.LanguageId.Java + language_id = model_pb2.LanguageId.Java # type: ignore[attr-defined] issue_configs = ISSUE_CONFIGS ij_inspection_to_issue_type = IJ_INSPECTION_TO_ISSUE_TYPE ij_message_to_issue_type = IJ_MESSAGE_TO_ISSUE_TYPE diff --git a/hyperstyle/src/python/review/inspectors/ij_java/issue_types.py b/hyperstyle/src/python/review/inspectors/ij_java/issue_types.py index 5c0fe456..17f7a93b 100644 --- a/hyperstyle/src/python/review/inspectors/ij_java/issue_types.py +++ b/hyperstyle/src/python/review/inspectors/ij_java/issue_types.py @@ -1,5 +1,10 @@ from __future__ import annotations -IJ_INSPECTION_TO_ISSUE_TYPE = {} +from typing import TYPE_CHECKING -IJ_MESSAGE_TO_ISSUE_TYPE = {} +if TYPE_CHECKING: + from hyperstyle.src.python.review.inspectors.common.issue.issue import IssueType + +IJ_INSPECTION_TO_ISSUE_TYPE: dict[str, IssueType] = {} + +IJ_MESSAGE_TO_ISSUE_TYPE: dict[str, dict[str, IssueType]] = {} diff --git a/hyperstyle/src/python/review/inspectors/ij_kotlin/ij_kotlin.py b/hyperstyle/src/python/review/inspectors/ij_kotlin/ij_kotlin.py index bc78323d..bbf661b7 100644 --- a/hyperstyle/src/python/review/inspectors/ij_kotlin/ij_kotlin.py +++ b/hyperstyle/src/python/review/inspectors/ij_kotlin/ij_kotlin.py @@ -12,7 +12,7 @@ class KotlinIJInspector(BaseIJInspector): inspector_type = InspectorType.IJ_KOTLIN - language_id = model_pb2.LanguageId.kotlin + language_id = model_pb2.LanguageId.kotlin # type: ignore[attr-defined] issue_configs = ISSUE_CONFIGS ij_inspection_to_issue_type = IJ_INSPECTION_TO_ISSUE_TYPE ij_message_to_issue_type = IJ_MESSAGE_TO_ISSUE_TYPE diff --git a/hyperstyle/src/python/review/inspectors/ij_kotlin/issue_types.py b/hyperstyle/src/python/review/inspectors/ij_kotlin/issue_types.py index 5c0fe456..17f7a93b 100644 --- a/hyperstyle/src/python/review/inspectors/ij_kotlin/issue_types.py +++ b/hyperstyle/src/python/review/inspectors/ij_kotlin/issue_types.py @@ -1,5 +1,10 @@ from __future__ import annotations -IJ_INSPECTION_TO_ISSUE_TYPE = {} +from typing import TYPE_CHECKING -IJ_MESSAGE_TO_ISSUE_TYPE = {} +if TYPE_CHECKING: + from hyperstyle.src.python.review.inspectors.common.issue.issue import IssueType + +IJ_INSPECTION_TO_ISSUE_TYPE: dict[str, IssueType] = {} + +IJ_MESSAGE_TO_ISSUE_TYPE: dict[str, dict[str, IssueType]] = {} diff --git a/hyperstyle/src/python/review/inspectors/ij_python/ij_python.py b/hyperstyle/src/python/review/inspectors/ij_python/ij_python.py index 10016dab..3e864fa0 100644 --- a/hyperstyle/src/python/review/inspectors/ij_python/ij_python.py +++ b/hyperstyle/src/python/review/inspectors/ij_python/ij_python.py @@ -12,7 +12,7 @@ class PythonIJInspector(BaseIJInspector): inspector_type = InspectorType.IJ_PYTHON - language_id = model_pb2.LanguageId.Python + language_id = model_pb2.LanguageId.Python # type: ignore[attr-defined] issue_configs = ISSUE_CONFIGS ij_inspection_to_issue_type = IJ_INSPECTION_TO_ISSUE_TYPE ij_message_to_issue_type = IJ_MESSAGE_TO_ISSUE_TYPE diff --git a/hyperstyle/src/python/review/inspectors/ij_python/issue_types.py b/hyperstyle/src/python/review/inspectors/ij_python/issue_types.py index aa715272..6f14d8ae 100644 --- a/hyperstyle/src/python/review/inspectors/ij_python/issue_types.py +++ b/hyperstyle/src/python/review/inspectors/ij_python/issue_types.py @@ -3,7 +3,7 @@ from hyperstyle.src.python.review.inspectors.common.issue.issue import IssueType # Synchronized with https://github.com/JetBrains-Research/code-quality-ij-server/tree/master/docs/inspections/python -IJ_INSPECTION_TO_ISSUE_TYPE = { +IJ_INSPECTION_TO_ISSUE_TYPE: dict[str, IssueType] = { # BEST_PRACTICES "PyUnusedLocalInspection": IssueType.BEST_PRACTICES, "PySimplifyBooleanCheckInspection": IssueType.BEST_PRACTICES, @@ -63,7 +63,7 @@ "PyUnresolvedReferencesInspection": IssueType.ERROR_PRONE, } -IJ_MESSAGE_TO_ISSUE_TYPE = { +IJ_MESSAGE_TO_ISSUE_TYPE: dict[str, dict[str, IssueType]] = { "PyDataclassInspection": { "is useless until " "__post_init__" " is declared": IssueType.BEST_PRACTICES, "should take all init-only variables (incl. inherited) in the same order as they are defined": IssueType.BEST_PRACTICES, diff --git a/hyperstyle/src/python/review/inspectors/pyast/python_ast.py b/hyperstyle/src/python/review/inspectors/pyast/python_ast.py index a7ebd6ab..aa61d5e0 100644 --- a/hyperstyle/src/python/review/inspectors/pyast/python_ast.py +++ b/hyperstyle/src/python/review/inspectors/pyast/python_ast.py @@ -145,7 +145,7 @@ def inspect(cls, path: Path, config: dict[str, Any]) -> list[BaseIssue]: path_to_files = language.filter_paths_by_language(path_to_files, Language.PYTHON) - metrics = [] + metrics: list[BaseIssue] = [] for path_to_file in path_to_files: file_content = path_to_file.read_text() tree = ast.parse(file_content, path_to_file.name) diff --git a/hyperstyle/src/python/review/inspectors/pylint/pylint.py b/hyperstyle/src/python/review/inspectors/pylint/pylint.py index f94c18dc..149981b9 100644 --- a/hyperstyle/src/python/review/inspectors/pylint/pylint.py +++ b/hyperstyle/src/python/review/inspectors/pylint/pylint.py @@ -64,7 +64,7 @@ def parse(cls, output: str) -> list[BaseIssue]: row_re = re.compile(r"^(.*):(\d+):(\d+):([IRCWEF]\d+):(.*)$", re.MULTILINE) issue_configs_handler = IssueConfigsHandler(*ISSUE_CONFIGS) - issues = [] + issues: list[BaseIssue] = [] for groups in row_re.findall(output): if groups[1] == INFO_CATEGORY: continue @@ -76,7 +76,7 @@ def parse(cls, output: str) -> list[BaseIssue]: origin_class = groups[3] issue_type = cls.choose_issue_type(origin_class) if issue_type not in cls.supported_issue_types: - logger.error("pylint: unsupported issue type %s", issue_type.__name__) + logger.error("pylint: unsupported issue type %s", issue_type.__name__) # type: ignore[attr-defined] continue base_issue = BaseIssue( diff --git a/hyperstyle/src/python/review/inspectors/radon/radon.py b/hyperstyle/src/python/review/inspectors/radon/radon.py index dd6ea992..1bff57bb 100644 --- a/hyperstyle/src/python/review/inspectors/radon/radon.py +++ b/hyperstyle/src/python/review/inspectors/radon/radon.py @@ -31,7 +31,7 @@ def inspect_in_memory(cls, code: str, config: dict[str, Any]) -> list[BaseIssue] @classmethod def inspect(cls, path: Path, config: dict[str, Any]) -> list[BaseIssue]: - mi_command = [ + mi_command: list[str] = [ sys.executable, "-m", "radon", @@ -39,7 +39,7 @@ def inspect(cls, path: Path, config: dict[str, Any]) -> list[BaseIssue]: "--max", "F", # set the maximum MI rank to display "--show", # actual MI value is shown in results, alongside the rank - path, + str(path), ] mi_output = run_in_subprocess(mi_command) diff --git a/hyperstyle/src/python/review/logging_config.py b/hyperstyle/src/python/review/logging_config.py index ab7ef702..ff50d915 100644 --- a/hyperstyle/src/python/review/logging_config.py +++ b/hyperstyle/src/python/review/logging_config.py @@ -1,8 +1,12 @@ from __future__ import annotations import sys +from typing import TYPE_CHECKING -logging_config: dict[str, object] = { +if TYPE_CHECKING: + from logging.config import _DictConfigArgs + +logging_config: _DictConfigArgs = { "version": 1, "formatters": { "common": { diff --git a/hyperstyle/src/python/review/quality/model.py b/hyperstyle/src/python/review/quality/model.py index cb72606d..f178146e 100644 --- a/hyperstyle/src/python/review/quality/model.py +++ b/hyperstyle/src/python/review/quality/model.py @@ -34,7 +34,7 @@ def __le__(self, other: QualityType) -> bool: class Rule(abc.ABC): rule_type: IssueType - quality_type: QualityType + quality_type: QualityType | None next_level_type: QualityType next_level_delta: float value: int @@ -43,6 +43,10 @@ class Rule(abc.ABC): def apply(self, *args, **kwargs) -> None: pass + @abc.abstractmethod + def merge(self, other: Rule) -> Rule: + raise NotImplementedError + class Quality: def __init__(self, rules: list[Rule]) -> None: @@ -50,7 +54,10 @@ def __init__(self, rules: list[Rule]) -> None: @property def quality_type(self) -> QualityType: - return min((rule.quality_type for rule in self.rules), default=QualityType.EXCELLENT) + return min( + (rule.quality_type for rule in self.rules if rule.quality_type is not None), + default=QualityType.EXCELLENT, + ) @property def next_quality_type(self) -> QualityType: diff --git a/hyperstyle/src/python/review/quality/penalty.py b/hyperstyle/src/python/review/quality/penalty.py index 300a1e28..6ed1b675 100644 --- a/hyperstyle/src/python/review/quality/penalty.py +++ b/hyperstyle/src/python/review/quality/penalty.py @@ -59,7 +59,7 @@ class PenaltyConfig: class PreviousIssue: origin_class: str number: int - category: IssueType = None + category: IssueType | None = None def get_previous_issues_by_language(lang_to_history: str | None, language: Language) -> list[PreviousIssue]: @@ -78,12 +78,10 @@ def get_previous_issues_by_language(lang_to_history: str | None, language: Langu def categorize(previous_issues: list[PreviousIssue], current_issues: list[BaseIssue]) -> None: """For each previously made issue determines its category, with the help of current issues.""" - origin_class_to_category = {} - for issue in current_issues: - origin_class_to_category[issue.origin_class] = issue.type + origin_class_to_category = {issue.origin_class: issue.type for issue in current_issues} for issue in previous_issues: - issue.category = origin_class_to_category.get(issue.origin_class, None) + issue.category = origin_class_to_category.get(issue.origin_class) class Punisher: @@ -154,9 +152,13 @@ def _get_penalty_coefficient( filter(lambda issue: issue.origin_class in penalizing_classes, previous_issues) ) - coefficient = 0 + coefficient: float = 0 for issue in penalizing_issues: - coefficient += ISSUE_TYPE_TO_PENALTY_COEFFICIENT.get(issue.category, 1) * issue.number + coefficient += ( + ISSUE_TYPE_TO_PENALTY_COEFFICIENT.get(issue.category, 1) * issue.number + if issue.category + else issue.number + ) return coefficient @@ -164,7 +166,7 @@ def _get_normalized_penalty_coefficient(self, current_issues: list[BaseIssue]) - """The penalty coefficient is normalized by the formula: k / (k + n), where k is the penalty coefficient, n is the number of current issues. """ - coefficient = 0 + coefficient: float = 0 if current_issues: coefficient = self._penalty_coefficient / (self._penalty_coefficient + len(current_issues)) @@ -185,7 +187,11 @@ def _get_issue_class_to_influence( result = {} for issue in penalizing_issues: - issue_coefficient = ISSUE_TYPE_TO_PENALTY_COEFFICIENT.get(issue.category, 1) * issue.number + issue_coefficient = ( + ISSUE_TYPE_TO_PENALTY_COEFFICIENT.get(issue.category, 1) * issue.number + if issue.category + else issue.number + ) normalized_issue_coefficient = issue_coefficient / ( self._penalty_coefficient + len(current_issues) ) diff --git a/hyperstyle/src/python/review/quality/rules/best_practices_scoring.py b/hyperstyle/src/python/review/quality/rules/best_practices_scoring.py index 9d5e4ab5..5ba01148 100644 --- a/hyperstyle/src/python/review/quality/rules/best_practices_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/best_practices_scoring.py @@ -57,13 +57,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: BestPracticesRule) -> BestPracticesRule: + def merge(self, other: Rule) -> BestPracticesRule: + assert isinstance(other, BestPracticesRule) config = BestPracticesRuleConfig( min(self.config.n_best_practices_moderate, other.config.n_best_practices_moderate), min(self.config.n_best_practices_good, other.config.n_best_practices_good), n_files=self.config.n_files + other.config.n_files, ) result_rule = BestPracticesRule(config) + assert self.n_best_practices is not None + assert other.n_best_practices is not None result_rule.apply(self.n_best_practices + other.n_best_practices) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/boolean_length_scoring.py b/hyperstyle/src/python/review/quality/rules/boolean_length_scoring.py index 6c2bc8b4..74b56c2e 100644 --- a/hyperstyle/src/python/review/quality/rules/boolean_length_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/boolean_length_scoring.py @@ -64,13 +64,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: BooleanExpressionRule) -> BooleanExpressionRule: + def merge(self, other: Rule) -> BooleanExpressionRule: + assert isinstance(other, BooleanExpressionRule) config = BooleanExpressionRuleConfig( min(self.config.bool_expr_len_bad, other.config.bool_expr_len_bad), min(self.config.bool_expr_len_moderate, other.config.bool_expr_len_moderate), min(self.config.bool_expr_len_good, other.config.bool_expr_len_good), ) result_rule = BooleanExpressionRule(config) + assert self.bool_expr_len is not None + assert other.bool_expr_len is not None result_rule.apply(max(self.bool_expr_len, other.bool_expr_len)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/class_response_scoring.py b/hyperstyle/src/python/review/quality/rules/class_response_scoring.py index 6eeefcc4..a57dcf93 100644 --- a/hyperstyle/src/python/review/quality/rules/class_response_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/class_response_scoring.py @@ -51,12 +51,15 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: ResponseRule) -> ResponseRule: + def merge(self, other: Rule) -> ResponseRule: + assert isinstance(other, ResponseRule) config = ResponseRuleConfig( min(self.config.response_moderate, other.config.response_moderate), min(self.config.response_good, other.config.response_good), ) result_rule = ResponseRule(config) + assert self.response is not None + assert other.response is not None result_rule.apply(max(self.response, other.response)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/code_style_scoring.py b/hyperstyle/src/python/review/quality/rules/code_style_scoring.py index 94837c35..038b27f1 100644 --- a/hyperstyle/src/python/review/quality/rules/code_style_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/code_style_scoring.py @@ -72,8 +72,8 @@ def __init__(self, config: CodeStyleRuleConfig) -> None: self.rule_type = IssueType.CODE_STYLE self.total_lines = 0 self.n_code_style_lines = 0 - self.ratio = 0 - self.quality_type = None + self.ratio: float = 0 + self.quality_type: QualityType | None = None self.next_level_delta = 0 # TODO: refactor @@ -119,7 +119,10 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: CodeStyleRule) -> CodeStyleRule: + def merge(self, other: Rule) -> CodeStyleRule: + assert isinstance(other, CodeStyleRule) + assert self.quality_type is not None + assert other.quality_type is not None if self.quality_type > other.quality_type: return other return self diff --git a/hyperstyle/src/python/review/quality/rules/cohesion_scoring.py b/hyperstyle/src/python/review/quality/rules/cohesion_scoring.py index 79c20423..064d2420 100644 --- a/hyperstyle/src/python/review/quality/rules/cohesion_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/cohesion_scoring.py @@ -61,13 +61,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: CohesionRule) -> CohesionRule: + def merge(self, other: Rule) -> CohesionRule: + assert isinstance(other, CohesionRule) config = CohesionRuleConfig( min(self.config.cohesion_lack_bad, other.config.cohesion_lack_bad), min(self.config.cohesion_lack_moderate, other.config.cohesion_lack_moderate), min(self.config.cohesion_lack_good, other.config.cohesion_lack_good), ) result_rule = CohesionRule(config) + assert self.cohesion_lack is not None + assert other.cohesion_lack is not None result_rule.apply(max(self.cohesion_lack, other.cohesion_lack)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/complexity_scoring.py b/hyperstyle/src/python/review/quality/rules/complexity_scoring.py index f9685fb7..20da2e87 100644 --- a/hyperstyle/src/python/review/quality/rules/complexity_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/complexity_scoring.py @@ -58,13 +58,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: ComplexityRule) -> ComplexityRule: + def merge(self, other: Rule) -> ComplexityRule: + assert isinstance(other, ComplexityRule) config = ComplexityRuleConfig( min(self.config.complexity_bad, other.config.complexity_bad), min(self.config.complexity_moderate, other.config.complexity_moderate), min(self.config.complexity_good, other.config.complexity_good), ) result_rule = ComplexityRule(config) + assert self.complexity is not None + assert other.complexity is not None result_rule.apply(max(self.complexity, other.complexity)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/coupling_scoring.py b/hyperstyle/src/python/review/quality/rules/coupling_scoring.py index 85831061..f778929b 100644 --- a/hyperstyle/src/python/review/quality/rules/coupling_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/coupling_scoring.py @@ -51,12 +51,15 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.MODERATE return QualityType.EXCELLENT - def merge(self, other: CouplingRule) -> CouplingRule: + def merge(self, other: Rule) -> CouplingRule: + assert isinstance(other, CouplingRule) config = CouplingRuleConfig( min(self.config.coupling_bad, other.config.coupling_bad), min(self.config.coupling_moderate, other.config.coupling_moderate), ) result_rule = CouplingRule(config) + assert self.coupling is not None + assert other.coupling is not None result_rule.apply(max(self.coupling, other.coupling)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/cyclomatic_complexity_scoring.py b/hyperstyle/src/python/review/quality/rules/cyclomatic_complexity_scoring.py index 73859e1e..c90c95e2 100644 --- a/hyperstyle/src/python/review/quality/rules/cyclomatic_complexity_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/cyclomatic_complexity_scoring.py @@ -62,12 +62,15 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.MODERATE return QualityType.EXCELLENT - def merge(self, other: CyclomaticComplexityRule) -> CyclomaticComplexityRule: + def merge(self, other: Rule) -> CyclomaticComplexityRule: + assert isinstance(other, CyclomaticComplexityRule) config = CyclomaticComplexityRuleConfig( min(self.config.cc_value_bad, other.config.cc_value_bad), min(self.config.cc_value_moderate, other.config.cc_value_moderate), ) result_rule = CyclomaticComplexityRule(config) + assert self.cc_value is not None + assert other.cc_value is not None result_rule.apply(max(self.cc_value, other.cc_value)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/error_prone_scoring.py b/hyperstyle/src/python/review/quality/rules/error_prone_scoring.py index 8879906e..a5227292 100644 --- a/hyperstyle/src/python/review/quality/rules/error_prone_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/error_prone_scoring.py @@ -48,9 +48,12 @@ def apply(self, n_error_prone: int) -> None: def __get_next_quality_type(self) -> QualityType: return QualityType.EXCELLENT - def merge(self, other: ErrorProneRule) -> ErrorProneRule: + def merge(self, other: Rule) -> ErrorProneRule: + assert isinstance(other, ErrorProneRule) config = ErrorProneRuleConfig(min(self.config.n_error_prone_bad, other.config.n_error_prone_bad)) result_rule = ErrorProneRule(config) + assert self.n_error_prone is not None + assert other.n_error_prone is not None result_rule.apply(self.n_error_prone + other.n_error_prone) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/function_length_scoring.py b/hyperstyle/src/python/review/quality/rules/function_length_scoring.py index 14be729f..c1449986 100644 --- a/hyperstyle/src/python/review/quality/rules/function_length_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/function_length_scoring.py @@ -51,11 +51,14 @@ def apply(self, func_len: int) -> None: def __get_next_quality_type(self) -> QualityType: return QualityType.EXCELLENT - def merge(self, other: FunctionLengthRule) -> FunctionLengthRule: + def merge(self, other: Rule) -> FunctionLengthRule: + assert isinstance(other, FunctionLengthRule) config = FunctionLengthRuleConfig( min(self.config.func_len_bad, other.config.func_len_bad), ) result_rule = FunctionLengthRule(config) + assert self.func_len is not None + assert other.func_len is not None result_rule.apply(max(self.func_len, other.func_len)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/inheritance_depth_scoring.py b/hyperstyle/src/python/review/quality/rules/inheritance_depth_scoring.py index 79a87a74..6e0191a9 100644 --- a/hyperstyle/src/python/review/quality/rules/inheritance_depth_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/inheritance_depth_scoring.py @@ -44,9 +44,12 @@ def apply(self, depth: int) -> None: def __get_next_quality_type(self) -> QualityType: return QualityType.EXCELLENT - def merge(self, other: InheritanceDepthRule) -> InheritanceDepthRule: + def merge(self, other: Rule) -> InheritanceDepthRule: + assert isinstance(other, InheritanceDepthRule) config = InheritanceDepthRuleConfig(min(self.config.depth_bad, other.config.depth_bad)) result_rule = InheritanceDepthRule(config) + assert self.depth is not None + assert other.depth is not None result_rule.apply(max(self.depth, other.depth)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/line_len_scoring.py b/hyperstyle/src/python/review/quality/rules/line_len_scoring.py index 0c0d290b..4db860e0 100644 --- a/hyperstyle/src/python/review/quality/rules/line_len_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/line_len_scoring.py @@ -59,7 +59,8 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: LineLengthRule) -> LineLengthRule: + def merge(self, other: Rule) -> LineLengthRule: + assert isinstance(other, LineLengthRule) config = LineLengthRuleConfig( min(self.config.n_line_len_bad, other.config.n_line_len_bad), min(self.config.n_line_len_moderate, other.config.n_line_len_moderate), diff --git a/hyperstyle/src/python/review/quality/rules/maintainability_scoring.py b/hyperstyle/src/python/review/quality/rules/maintainability_scoring.py index b08860c0..bec14770 100644 --- a/hyperstyle/src/python/review/quality/rules/maintainability_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/maintainability_scoring.py @@ -75,13 +75,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: MaintainabilityRule) -> MaintainabilityRule: + def merge(self, other: Rule) -> MaintainabilityRule: + assert isinstance(other, MaintainabilityRule) config = MaintainabilityRuleConfig( min(self.config.maintainability_lack_bad, other.config.maintainability_lack_bad), min(self.config.maintainability_lack_moderate, other.config.maintainability_lack_moderate), min(self.config.maintainability_lack_good, other.config.maintainability_lack_good), ) result_rule = MaintainabilityRule(config) + assert self.maintainability_lack is not None + assert other.maintainability_lack is not None result_rule.apply(max(self.maintainability_lack, other.maintainability_lack)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/method_number_scoring.py b/hyperstyle/src/python/review/quality/rules/method_number_scoring.py index 0b5c35f5..2e6f79d3 100644 --- a/hyperstyle/src/python/review/quality/rules/method_number_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/method_number_scoring.py @@ -58,13 +58,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: MethodNumberRule) -> MethodNumberRule: + def merge(self, other: Rule) -> MethodNumberRule: + assert isinstance(other, MethodNumberRule) config = MethodNumberRuleConfig( min(self.config.method_number_bad, other.config.method_number_bad), min(self.config.method_number_moderate, other.config.method_number_moderate), min(self.config.method_number_good, other.config.method_number_good), ) result_rule = MethodNumberRule(config) + assert self.method_number is not None + assert other.method_number is not None result_rule.apply(max(self.method_number, other.method_number)) return result_rule diff --git a/hyperstyle/src/python/review/quality/rules/weighted_methods_scoring.py b/hyperstyle/src/python/review/quality/rules/weighted_methods_scoring.py index 635f6164..2766e57c 100644 --- a/hyperstyle/src/python/review/quality/rules/weighted_methods_scoring.py +++ b/hyperstyle/src/python/review/quality/rules/weighted_methods_scoring.py @@ -56,13 +56,16 @@ def __get_next_quality_type(self) -> QualityType: return QualityType.GOOD return QualityType.EXCELLENT - def merge(self, other: WeightedMethodsRule) -> WeightedMethodsRule: + def merge(self, other: Rule) -> WeightedMethodsRule: + assert isinstance(other, WeightedMethodsRule) config = WeightedMethodsRuleConfig( min(self.config.weighted_methods_bad, other.config.weighted_methods_bad), min(self.config.weighted_methods_moderate, other.config.weighted_methods_moderate), min(self.config.weighted_methods_good, other.config.weighted_methods_good), ) result_rule = WeightedMethodsRule(config) + assert self.weighted_methods is not None + assert other.weighted_methods is not None result_rule.apply(max(self.weighted_methods, other.weighted_methods)) return result_rule diff --git a/hyperstyle/src/python/review/reviewers/common.py b/hyperstyle/src/python/review/reviewers/common.py index 2e85a328..fa952e95 100644 --- a/hyperstyle/src/python/review/reviewers/common.py +++ b/hyperstyle/src/python/review/reviewers/common.py @@ -73,26 +73,28 @@ } -def _inspect_code( - metadata: Metadata, config: ApplicationConfig, language: Language -) -> list[BaseIssue] | None: +def _inspect_code(metadata: Metadata, config: ApplicationConfig, language: Language) -> list[BaseIssue]: inspectors = LANGUAGE_TO_INSPECTORS[language] - ij_inspectors = list( - filter( - lambda inspector: isinstance(inspector, BaseIJInspector) - and inspector.inspector_type not in config.disabled_inspectors, - inspectors, - ), - ) + ij_inspectors: list[BaseIJInspector] = [ + inspector + for inspector in inspectors + if ( + isinstance(inspector, BaseIJInspector) + and inspector.inspector_type not in config.disabled_inspectors + ) + ] connection_parameters = ( None if config.ij_config is None else json.loads(config.ij_config).get(language.value.lower()) ) - if ij_inspectors and connection_parameters is None: - msg = f"Connection parameters for {language.value} inspectors are not provided" - raise ValueError(msg) - for inspector in ij_inspectors: - inspector.setup_connection_parameters(connection_parameters["host"], connection_parameters["port"]) + if ij_inspectors: + if connection_parameters is None: + msg = f"Connection parameters for {language.value} inspectors are not provided" + raise ValueError(msg) + for inspector in ij_inspectors: + inspector.setup_connection_parameters( + connection_parameters["host"], connection_parameters["port"] + ) if isinstance(metadata, InMemoryMetadata): return inspect_in_parallel(run_inspector_in_memory, metadata.code, config, inspectors) @@ -110,19 +112,20 @@ def perform_language_review( if not config.allow_duplicates: issues = filter_duplicate_issues(issues) - current_files = None if isinstance(metadata, FileMetadata): current_files = [metadata.path] issues = filter_out_of_range_issues(issues, config.start_line, config.end_line) elif isinstance(metadata, ProjectMetadata): current_files = metadata.language_to_files[language] + else: + current_files = None file_path_to_issues = defaultdict(list) for issue in issues: file_path_to_issues[issue.file_path].append(issue) if current_files is None: - files = file_path_to_issues.keys() + files = list(file_path_to_issues.keys()) current_files = [] if len(files) == 0 else files previous_issues = get_previous_issues_by_language(config.history, language) diff --git a/hyperstyle/src/python/review/reviewers/utils/code_statistics.py b/hyperstyle/src/python/review/reviewers/utils/code_statistics.py index b9b92509..06d97cb4 100644 --- a/hyperstyle/src/python/review/reviewers/utils/code_statistics.py +++ b/hyperstyle/src/python/review/reviewers/utils/code_statistics.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from hyperstyle.src.python.review.common.file_system import get_total_code_lines_from_file -from hyperstyle.src.python.review.inspectors.common.issue.issue import BaseIssue, IssueType +from hyperstyle.src.python.review.inspectors.common.issue.issue import BaseIssue, IssueType, MeasurableIssue if TYPE_CHECKING: from pathlib import Path @@ -64,7 +64,11 @@ def get_code_style_lines(issues: list[BaseIssue]) -> int: def __get_max_measure_by_issue_type(issue_type: IssueType, issues: list[BaseIssue]) -> int: return max( - (issue.measure() for issue in filter(lambda issue: issue.type == issue_type, issues)), + ( + issue.measure() or 0 + for issue in issues + if isinstance(issue, MeasurableIssue) and issue.type == issue_type + ), default=0, ) diff --git a/hyperstyle/src/python/review/reviewers/utils/issues_filter.py b/hyperstyle/src/python/review/reviewers/utils/issues_filter.py index e2848680..bf72dc79 100644 --- a/hyperstyle/src/python/review/reviewers/utils/issues_filter.py +++ b/hyperstyle/src/python/review/reviewers/utils/issues_filter.py @@ -7,7 +7,7 @@ BaseIssue, IssueDifficulty, IssueType, - Measurable, + MeasurableIssue, ) from hyperstyle.src.python.review.quality.rules.boolean_length_scoring import ( LANGUAGE_TO_BOOLEAN_EXPRESSION_RULE_CONFIG, @@ -60,7 +60,7 @@ def __get_issue_type_to_low_measure_dict(language: Language) -> dict[IssueType, def __more_than_low_measure(issue: BaseIssue, issue_type_to_low_measure_dict: dict[IssueType, int]) -> bool: issue_type = issue.type return not ( - isinstance(issue, Measurable) + isinstance(issue, MeasurableIssue) and issue.measure() <= issue_type_to_low_measure_dict.get(issue_type, -1) ) diff --git a/hyperstyle/src/python/review/reviewers/utils/metadata_exploration.py b/hyperstyle/src/python/review/reviewers/utils/metadata_exploration.py index 7b8dd334..a37abcb3 100644 --- a/hyperstyle/src/python/review/reviewers/utils/metadata_exploration.py +++ b/hyperstyle/src/python/review/reviewers/utils/metadata_exploration.py @@ -29,9 +29,7 @@ class ProjectMetadata: @property def languages(self) -> set[Language]: - languages = set() - languages.update(file_metadata.language for file_metadata in self.inner_files) - return languages + return {file_metadata.language for file_metadata in self.inner_files} @property def size_bytes(self) -> int: diff --git a/hyperstyle/src/python/review/reviewers/utils/print_review.py b/hyperstyle/src/python/review/reviewers/utils/print_review.py index 007a2fb9..f134daf0 100644 --- a/hyperstyle/src/python/review/reviewers/utils/print_review.py +++ b/hyperstyle/src/python/review/reviewers/utils/print_review.py @@ -3,13 +3,10 @@ import json import linecache from enum import Enum, unique -from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, TypedDict from hyperstyle.src.python.review.common.file_system import get_file_line -from hyperstyle.src.python.review.inspectors.common.inspector.inspector_type import InspectorType -from hyperstyle.src.python.review.inspectors.common.issue.issue import BaseIssue, IssueDifficulty, IssueType -from hyperstyle.src.python.review.quality.penalty import PenaltyIssue +from hyperstyle.src.python.review.inspectors.common.issue.issue import BaseIssue, IssueDifficulty from hyperstyle.src.python.review.reviewers.review_result import ( FileReviewResult, GeneralReviewResult, @@ -17,6 +14,8 @@ ) if TYPE_CHECKING: + from pathlib import Path + from hyperstyle.src.python.review.application_config import ApplicationConfig from hyperstyle.src.python.review.quality.model import QualityType @@ -83,9 +82,9 @@ def _get_quality_with_penalty(review_result: ReviewResult) -> dict[IssueDifficul def get_quality_json_dict( quality: dict[IssueDifficulty, QualityType], config: ApplicationConfig -) -> dict[str, object]: - quality_json_dict = { - difficulty.value: { +) -> dict[IssueDifficulty, object] | object: + quality_json_dict: dict[IssueDifficulty, object] = { + difficulty: { OutputJsonFields.CODE.value: quality.value, OutputJsonFields.TEXT.value: f"Code quality (beta): {quality.value}", } @@ -95,7 +94,7 @@ def get_quality_json_dict( if config.group_by_difficulty: return quality_json_dict - return quality_json_dict[IssueDifficulty.HARD.value] + return quality_json_dict[IssueDifficulty.HARD] def get_influence_on_penalty_json_dict( @@ -106,8 +105,8 @@ def get_influence_on_penalty_json_dict( quality_without_penalty = _get_quality_without_penalty(review_result) quality_with_penalty = _get_quality_with_penalty(review_result) - influence_on_penalty_json_dict = { - difficulty.value: punisher.get_issue_influence_on_penalty(origin_class) + influence_on_penalty_json_dict: dict[IssueDifficulty, int] = { + difficulty: punisher.get_issue_influence_on_penalty(origin_class) if quality_with_penalty[difficulty] != quality_without_penalty[difficulty] else 0 for difficulty, punisher in review_result.punisher_by_difficulty.items() @@ -116,25 +115,30 @@ def get_influence_on_penalty_json_dict( if config.group_by_difficulty: return influence_on_penalty_json_dict - return influence_on_penalty_json_dict[IssueDifficulty.HARD.value] + return influence_on_penalty_json_dict[IssueDifficulty.HARD] -def convert_review_result_to_json_dict( - review_result: ReviewResult, config: ApplicationConfig -) -> dict[str, object]: +class OutputJson(TypedDict): + quality: dict[IssueDifficulty, object] | object + issues: list[dict[str, object]] + file_name: str + + +def convert_review_result_to_json_dict(review_result: ReviewResult, config: ApplicationConfig) -> OutputJson: issues = review_result.issues issues.sort(key=lambda issue: issue.line_no) quality_with_penalty = _get_quality_with_penalty(review_result) - output_json = {} + output_json: OutputJson = { + OutputJsonFields.QUALITY.value: get_quality_json_dict(quality_with_penalty, config), + OutputJsonFields.ISSUES.value: [], + OutputJsonFields.FILE_NAME.value: "", + } if isinstance(review_result, FileReviewResult): output_json[OutputJsonFields.FILE_NAME.value] = str(review_result.file_path) - output_json[OutputJsonFields.QUALITY.value] = get_quality_json_dict(quality_with_penalty, config) - output_json[OutputJsonFields.ISSUES.value] = [] - for issue in issues: json_issue = convert_issue_to_json(issue, config) @@ -162,7 +166,7 @@ def print_review_result_as_multi_file_json( def get_review_result_as_multi_file_json( review_result: GeneralReviewResult, config: ApplicationConfig -) -> dict: +) -> dict[str, object]: review_result.file_review_results.sort(key=lambda result: result.file_path) file_review_result_jsons = [ @@ -211,23 +215,3 @@ def convert_issue_to_json(issue: BaseIssue, config: ApplicationConfig) -> dict[s OutputJsonFields.CATEGORY.value: issue_type.value, OutputJsonFields.DIFFICULTY.value: issue.difficulty.value, } - - -# It works only for old json format -def convert_json_to_issues(issues_json: list[dict]) -> list[PenaltyIssue]: - return [ - PenaltyIssue( - origin_class=issue[OutputJsonFields.CODE.value], - description=issue[OutputJsonFields.TEXT.value], - line_no=int(issue[OutputJsonFields.LINE_NUMBER.value]), - column_no=int(issue[OutputJsonFields.COLUMN_NUMBER.value]), - type=IssueType(issue[OutputJsonFields.CATEGORY.value]), - file_path=Path(), - inspector_type=InspectorType.UNDEFINED, - influence_on_penalty=issue.get(OutputJsonFields.INFLUENCE_ON_PENALTY.value, 0), - difficulty=IssueDifficulty( - issue.get(OutputJsonFields.DIFFICULTY.value, IssueDifficulty.HARD.value) - ), - ) - for issue in issues_json - ] diff --git a/hyperstyle/src/python/review/run_tool.py b/hyperstyle/src/python/review/run_tool.py index 7fd1bc0e..adfcba06 100644 --- a/hyperstyle/src/python/review/run_tool.py +++ b/hyperstyle/src/python/review/run_tool.py @@ -35,7 +35,7 @@ def parse_disabled_inspectors(value: str) -> set[InspectorType]: allowed_names = {inspector.value for inspector in InspectorType} if not all(name in allowed_names for name in passed_names): msg = "disable" - raise argparse.ArgumentError(msg, "Incorrect inspectors' names") + raise argparse.ArgumentError(msg, "Incorrect inspectors' names") # type: ignore[arg-type] return {InspectorType(name) for name in passed_names} @@ -50,7 +50,7 @@ def positive_int(value: str) -> int: def configure_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - RunToolArgument.VERBOSITY.value.short_name, + RunToolArgument.VERBOSITY.value.short_name, # type: ignore[arg-type] RunToolArgument.VERBOSITY.value.long_name, help=RunToolArgument.VERBOSITY.value.description, default=VerbosityLevel.DISABLE.value, @@ -60,7 +60,7 @@ def configure_arguments(parser: argparse.ArgumentParser) -> None: # Usage example: -d Flake8,IntelliJ parser.add_argument( - RunToolArgument.DISABLE.value.short_name, + RunToolArgument.DISABLE.value.short_name, # type: ignore[arg-type] RunToolArgument.DISABLE.value.long_name, help=RunToolArgument.DISABLE.value.description, type=parse_disabled_inspectors, @@ -107,7 +107,7 @@ def configure_arguments(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( - RunToolArgument.FORMAT.value.short_name, + RunToolArgument.FORMAT.value.short_name, # type: ignore[arg-type] RunToolArgument.FORMAT.value.long_name, default=OutputFormat.JSON.value, choices=OutputFormat.values(), @@ -116,7 +116,7 @@ def configure_arguments(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( - RunToolArgument.START_LINE.value.short_name, + RunToolArgument.START_LINE.value.short_name, # type: ignore[arg-type] RunToolArgument.START_LINE.value.long_name, default=1, type=positive_int, @@ -124,7 +124,7 @@ def configure_arguments(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( - RunToolArgument.END_LINE.value.short_name, + RunToolArgument.END_LINE.value.short_name, # type: ignore[arg-type] RunToolArgument.END_LINE.value.long_name, default=None, type=positive_int, diff --git a/poetry.lock b/poetry.lock index 2a6d46a5..78a9a004 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1228,29 +1228,29 @@ files = [ [[package]] name = "ruff" -version = "0.6.5" +version = "0.6.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, - {file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, - {file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, - {file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, - {file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, - {file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, - {file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, - {file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, - {file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, + {file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"}, + {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"}, + {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"}, + {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"}, + {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"}, + {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"}, + {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"}, ] [[package]] @@ -1307,6 +1307,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-protobuf" +version = "5.28.0.20240924" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-protobuf-5.28.0.20240924.tar.gz", hash = "sha256:d181af8a256e5a91ce8d5adb53496e880efd9144c7d54483e3653332b60296f0"}, + {file = "types_protobuf-5.28.0.20240924-py3-none-any.whl", hash = "sha256:5cecf612ccdefb7dc95f7a51fb502902f20fc2e6681cd500184aaa1b3931d6a7"}, +] + [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -1336,13 +1347,13 @@ typing-extensions = ">=3.7.4" [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -1463,4 +1474,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.10" -content-hash = "9db75465afe75cbcef197a6aed301dd091eb6d71d5fe460103ff74e8fa35a8e4" +content-hash = "617a81c3d550e69a35f583bb892200431fd63925ca4728c886ec63f07be8625f" diff --git a/pyproject.toml b/pyproject.toml index 858b15f1..7af8e03f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,8 @@ mypy = "1.3.0" pytest = "8.3.3" pytest-runner = "6.0.1" pytest-subtests = "0.13.1" -ruff = "0.6.5" +ruff = "0.6.7" +types-protobuf = "5.28.0.20240924" [build-system] requires = ["poetry-core"] diff --git a/test/python/inspectors/test_filter_duplicate_issues.py b/test/python/inspectors/test_filter_duplicate_issues.py index e815dfca..0b7d351c 100644 --- a/test/python/inspectors/test_filter_duplicate_issues.py +++ b/test/python/inspectors/test_filter_duplicate_issues.py @@ -3,12 +3,17 @@ from pathlib import Path from hyperstyle.src.python.review.inspectors.common.inspector.inspector_type import InspectorType -from hyperstyle.src.python.review.inspectors.common.issue.issue import CodeIssue, IssueDifficulty, IssueType +from hyperstyle.src.python.review.inspectors.common.issue.issue import ( + BaseIssue, + CodeIssue, + IssueDifficulty, + IssueType, +) from hyperstyle.src.python.review.reviewers.utils.issues_filter import filter_duplicate_issues def test_filter_duplicate_issues_when_single_inspector() -> None: - issues = [ + issues: list[BaseIssue] = [ CodeIssue( file_path=Path("code.py"), line_no=10, @@ -57,7 +62,7 @@ def test_filter_duplicate_issues_when_single_inspector() -> None: def test_filter_duplicate_issues_when_several_inspectors() -> None: - issues = [ + issues: list[BaseIssue] = [ CodeIssue( file_path=Path("code.py"), line_no=10, @@ -106,7 +111,7 @@ def test_filter_duplicate_issues_when_several_inspectors() -> None: def test_filter_duplicate_issues_when_several_issues_in_line_no() -> None: - issues = [ + issues: list[BaseIssue] = [ CodeIssue( file_path=Path("code.py"), line_no=10, diff --git a/test/python/inspectors/test_issue_configs.py b/test/python/inspectors/test_issue_configs.py index 7e6137d9..1069fb61 100644 --- a/test/python/inspectors/test_issue_configs.py +++ b/test/python/inspectors/test_issue_configs.py @@ -131,7 +131,7 @@ def test_init_raises_exception( expected_error_message: str, ) -> None: with pytest.raises(expected_exception) as excinfo: - cls(*args) + cls(*args) # type: ignore[arg-type] assert str(excinfo.value) == expected_error_message