Skip to content

Commit

Permalink
Add type hinting to Python code
Browse files Browse the repository at this point in the history
nre-ableton authored and ablbot committed May 29, 2024
1 parent c97f287 commit fd5e1f0
Showing 3 changed files with 40 additions and 32 deletions.
37 changes: 20 additions & 17 deletions run_codenarc.py
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
import time
import zipfile

from typing import Dict, List, Optional
from urllib.error import HTTPError
from urllib.request import urlopen
from xml.etree import ElementTree
@@ -31,7 +32,7 @@
class CodeNarcViolationsException(Exception):
"""Raised if CodeNarc violations were found."""

def __init__(self, num_violations):
def __init__(self, num_violations: int) -> None:
"""Create a new instance of the CodeNarcViolationsException class."""
super().__init__()
self.num_violations = num_violations
@@ -41,7 +42,7 @@ class FileDownloadFailure(Exception):
"""Raised if a file fails to download."""


def _build_classpath(args):
def _build_classpath(args: argparse.Namespace) -> str:
"""Construct the classpath to use for running CodeNarc."""
codenarc_version = _codenarc_version(args.codenarc_version, args.groovy4)
classpath = [
@@ -62,12 +63,12 @@ def _build_classpath(args):
return ":".join(classpath)


def _codenarc_version(version, is_groovy4):
def _codenarc_version(version: str, is_groovy4: bool) -> str:
"""Get the CodeNarc version depending on the version of Groovy being used."""
return f"{version}-groovy-4.0" if is_groovy4 else version


def _download_file(url, output_dir):
def _download_file(url: str, output_dir: str) -> str:
"""Download a file from a URL to the download directory."""
output_file_name = url.split("/")[-1]
output_file_path = os.path.join(output_dir, output_file_name)
@@ -88,7 +89,7 @@ def _download_file(url, output_dir):
return output_file_path


def _download_jar_with_retry(url, output_dir):
def _download_jar_with_retry(url: str, output_dir: str) -> str:
"""Download a JAR file but retry in case of failure."""
download_attempt = MAX_DOWNLOAD_ATTEMPTS
sleep_duration = 1
@@ -112,7 +113,7 @@ def _download_jar_with_retry(url, output_dir):
raise FileDownloadFailure(f"Failed to download {url}")


def _fetch_jars(args):
def _fetch_jars(args: argparse.Namespace) -> None:
"""Fetch JAR file dependencies."""
if not os.path.exists(args.resources):
os.mkdir(args.resources)
@@ -149,7 +150,7 @@ def _fetch_jars(args):
_download_jar_with_retry(url, args.resources)


def _guess_groovy_home():
def _guess_groovy_home() -> Optional[str]:
"""Try to determine the location where Groovy is installed.
:return: Path of the Groovy installation, or None if it can't be determined.
@@ -170,7 +171,7 @@ def _guess_groovy_home():
return None


def _is_slf4j_line(line):
def _is_slf4j_line(line: str) -> bool:
"""Determine if a log line was produced by SLF4J.
CodeNarc in some cases prints things to stdout, or uses multi-line logging calls which
@@ -179,7 +180,7 @@ def _is_slf4j_line(line):
return isinstance(logging.getLevelName(line.split(" ")[0]), int)


def _is_valid_jar(file_path):
def _is_valid_jar(file_path: str) -> bool:
"""Determine if a file is a valid JAR file."""
logging.debug("Verifying %s", file_path)
try:
@@ -194,7 +195,7 @@ def _is_valid_jar(file_path):
return True


def _log_codenarc_output(lines):
def _log_codenarc_output(lines: List[str]) -> None:
"""Re-log lines from CodeNarc's output.
This function takes a log line generated by CodeNarc and re-logs it with the logging
@@ -218,7 +219,7 @@ def _log_codenarc_output(lines):
logging.log(log_level, log_message)


def _print_violations(package_file_path, violations):
def _print_violations(package_file_path: str, violations: List) -> int:
"""Print violations for a file.
:param package_file_path: File path.
@@ -242,7 +243,7 @@ def _print_violations(package_file_path, violations):
return len(violations)


def _print_violations_in_files(package_path, files):
def _print_violations_in_files(package_path: str, files: List) -> int:
"""Print violations for each file in a package.
:param package_path: Package path.
@@ -261,7 +262,7 @@ def _print_violations_in_files(package_path, files):
return num_violations


def _print_violations_in_packages(packages):
def _print_violations_in_packages(packages: List) -> int:
"""Print violations for each package in a list of packages.
:param packages: List of Package elements.
@@ -284,7 +285,9 @@ def _print_violations_in_packages(packages):
return num_violations


def parse_args(args, default_jar_versions):
def parse_args(
args: List[str], default_jar_versions: Dict[str, str]
) -> argparse.Namespace:
"""Parse arguments from the command line."""
arg_parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
@@ -399,7 +402,7 @@ def parse_args(args, default_jar_versions):
return args


def parse_pom():
def parse_pom() -> Dict[str, str]:
"""Parse the pom.xml file and extract default JAR versions."""
jar_versions = {}

@@ -413,7 +416,7 @@ def parse_pom():
return jar_versions


def parse_xml_report(xml_text):
def parse_xml_report(xml_text: str) -> None:
"""Parse XML report text generated by CodeNarc.
:param xml_text: Raw XML text of CodeNarc report.
@@ -430,7 +433,7 @@ def parse_xml_report(xml_text):
raise CodeNarcViolationsException(total_violations)


def run_codenarc(args, report_file=None):
def run_codenarc(args: argparse.Namespace, report_file: str = None) -> str:
"""Run CodeNarc on specified code.
:param args: Parsed command line arguments.
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -2,11 +2,13 @@

"""Test configuration for groovylint tests."""

from typing import Dict

import pytest


@pytest.fixture()
def default_jar_versions():
def default_jar_versions() -> Dict[str, str]:
"""Return a dict for default JAR versions."""
return {
"CodeNarc": "1.0.0",
31 changes: 17 additions & 14 deletions tests/test_run_codenarc.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@
import os
import subprocess

from unittest.mock import patch
from typing import Dict
from unittest.mock import MagicMock, patch
from urllib.error import HTTPError

import pytest
@@ -27,24 +28,24 @@
MOCK_CODENARC_SUMMARY = b"CodeNarc completed: (p1=0; p2=0; p3=0) 6664ms\n"


def _report_file_contents(name):
def _report_file_contents(name: str) -> str:
with open(_report_file_path(name)) as report_file:
return report_file.read()


def _report_file_path(name):
def _report_file_path(name: str) -> str:
return os.path.join(os.path.dirname(__file__), "xml-reports", name)


def test_download_file_4xx():
def test_download_file_4xx() -> None:
"""Test that _download_file handles HTTP 4xx errors as expected."""
with patch("run_codenarc.urlopen") as urlopen_mock:
urlopen_mock.side_effect = HTTPError("url", 404, "Not found", None, None)
with pytest.raises(FileDownloadFailure):
_download_file("http://example.com/mock", "/tmp")


def test_download_file_5xx():
def test_download_file_5xx() -> None:
"""Test that _download_file handles HTTP 5xx errors as expected."""
with patch("run_codenarc.urlopen") as urlopen_mock:
urlopen_mock.side_effect = HTTPError("url", 500, "Whoops", None, None)
@@ -53,7 +54,7 @@ def test_download_file_5xx():


@patch("time.sleep")
def test_download_jar_with_retry_always_fail(sleep_mock):
def test_download_jar_with_retry_always_fail(sleep_mock: MagicMock) -> None:
"""Test that _download_jar_with_retry fails when the download also fails."""
with patch("run_codenarc._download_file") as _download_file_mock:
_download_file_mock.side_effect = FileDownloadFailure()
@@ -62,7 +63,7 @@ def test_download_jar_with_retry_always_fail(sleep_mock):


@patch("time.sleep")
def test_download_jar_with_retry_fail_verification(sleep_mock):
def test_download_jar_with_retry_fail_verification(sleep_mock: MagicMock) -> None:
"""Test that _download_jar_with_retry fails properly when a JAR fails to verify."""
with patch("run_codenarc._download_file") as _download_file_mock:
_download_file_mock.return_value = "outfile"
@@ -74,7 +75,7 @@ def test_download_jar_with_retry_fail_verification(sleep_mock):


@patch("time.sleep")
def test_download_jar_with_retry_survival(sleep_mock):
def test_download_jar_with_retry_survival(sleep_mock: MagicMock) -> None:
"""Test that _download_jar_with_retry can survive a single failure."""
with patch("run_codenarc._download_file") as _download_file_mock:
_download_file_mock.side_effect = [FileDownloadFailure(), "outfile"]
@@ -85,7 +86,7 @@ def test_download_jar_with_retry_survival(sleep_mock):
)


def test_parse_xml_report():
def test_parse_xml_report() -> None:
"""Test that parse_xml_report handles a successful report file as expected."""
parse_xml_report(_report_file_contents("success.xml"))

@@ -100,7 +101,7 @@ def test_parse_xml_report():
("single-violation-single-file.xml", 1),
],
)
def test_parse_xml_report_failed(report_file, num_violations):
def test_parse_xml_report_failed(report_file: str, num_violations: int) -> None:
"""Test that parse_xml_report handles a report file with violations as expected.
These report files were generated by CodeNarc itself.
@@ -111,7 +112,9 @@ def test_parse_xml_report_failed(report_file, num_violations):


@patch("os.remove")
def test_run_codenarc(remove_mock, default_jar_versions):
def test_run_codenarc(
remove_mock: MagicMock, default_jar_versions: Dict[str, str]
) -> None:
"""Test that run_codenarc exits without errors if CodeNarc ran successfully."""
with patch("os.path.exists") as path_exists_mock:
path_exists_mock.return_value = True
@@ -128,7 +131,7 @@ def test_run_codenarc(remove_mock, default_jar_versions):
assert _report_file_contents("success.xml") == output


def test_run_codenarc_compilation_failure(default_jar_versions):
def test_run_codenarc_compilation_failure(default_jar_versions: Dict[str, str]) -> None:
"""Test that run_codenarc raises an error if CodeNarc found compilation errors."""
with patch("subprocess.run") as subprocess_mock:
subprocess_mock.return_value = subprocess.CompletedProcess(
@@ -146,7 +149,7 @@ def test_run_codenarc_compilation_failure(default_jar_versions):
)


def test_run_codenarc_failure_code(default_jar_versions):
def test_run_codenarc_failure_code(default_jar_versions: Dict[str, str]) -> None:
"""Test that run_codenarc raises an error if CodeNarc failed to run."""
with patch("subprocess.run") as subprocess_mock:
subprocess_mock.return_value = subprocess.CompletedProcess(
@@ -159,7 +162,7 @@ def test_run_codenarc_failure_code(default_jar_versions):
)


def test_run_codenarc_no_report_file(default_jar_versions):
def test_run_codenarc_no_report_file(default_jar_versions: Dict[str, str]) -> None:
"""Test that run_codenarc raises an error if CodeNarc did not produce a report."""
with patch("subprocess.run") as subprocess_mock:
subprocess_mock.return_value = subprocess.CompletedProcess(

0 comments on commit fd5e1f0

Please sign in to comment.