From 7b12e3255619f4757cfff9af07b83744f078c9dc Mon Sep 17 00:00:00 2001 From: Stanislav Pankevich Date: Sun, 2 Feb 2025 17:58:40 +0100 Subject: [PATCH] tasks: add lint tasks: lint, format, mypy --- hpdf/hpdf.py | 26 ++++---- requirements.development.txt | 6 ++ ruff.toml | 103 ++++++++++++++++++++++++++++++ tasks.py | 63 ++++++++++++++++-- tests/integration/check_exists.py | 12 +--- tests/integration/expect_exit.py | 4 +- 6 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 ruff.toml diff --git a/hpdf/hpdf.py b/hpdf/hpdf.py index af4ddc9..080dbaa 100644 --- a/hpdf/hpdf.py +++ b/hpdf/hpdf.py @@ -4,12 +4,11 @@ import base64 import os.path import sys -import tempfile from datetime import datetime from pathlib import Path from shutil import copy from time import sleep -from typing import Optional, List +from typing import Dict, List, Optional import requests from requests import Response @@ -151,17 +150,18 @@ def get_pdf_from_html(driver, url) -> bytes: print("html2pdf: executing print command with ChromeDriver.") # noqa: T201 result = driver.execute_cdp_cmd("Page.printToPDF", calculated_print_options) - class Done(Exception): pass + class Done(Exception): + pass datetime_start = datetime.today() - logs = None + logs: List[Dict[str, str]] = [] try: while True: logs = driver.get_log("browser") for entry_ in logs: if "HTML2PDF4DOC time" in entry_["message"]: - print("success: HTML2PDF completed its job.") + print("success: HTML2PDF completed its job.") # noqa: T201 raise Done if (datetime.today() - datetime_start).total_seconds() > 60: raise TimeoutError @@ -169,7 +169,9 @@ class Done(Exception): pass except Done: pass except TimeoutError: - print("error: could not receive a successful completion status from HTML2PDF.") + print( # noqa: T201 + "error: could not receive a successful completion status from HTML2PDF." + ) sys.exit(1) print("html2pdf: JS logs from the print session:") # noqa: T201 @@ -244,7 +246,7 @@ def main(): type=str, help="Optional path to a cache directory whereto the ChromeDriver is downloaded.", ) - parser.add_argument("paths", nargs='+', help="Paths to input HTML file.") + parser.add_argument("paths", nargs="+", help="Paths to input HTML file.") args = parser.parse_args() paths: List[str] = args.paths @@ -252,11 +254,7 @@ def main(): path_to_cache_dir: str = ( args.cache_dir if args.cache_dir is not None - else ( - os.path.join( - Path.home(), ".hpdf", "chromedriver" - ) - ) + else (os.path.join(Path.home(), ".hpdf", "chromedriver")) ) driver = create_webdriver(args.chromedriver, path_to_cache_dir) @@ -265,7 +263,9 @@ def exit_handler(): print("html2pdf: exit handler: quitting the ChromeDriver.") # noqa: T201 driver.quit() - assert len(paths) % 2 == 0, f"Expecting an even number of input/output path arguments: {paths}." + assert len(paths) % 2 == 0, ( + f"Expecting an even number of input/output path arguments: {paths}." + ) for current_pair_idx in range(0, 2, len(paths)): path_to_input_html = paths[current_pair_idx] path_to_output_pdf = paths[current_pair_idx + 1] diff --git a/requirements.development.txt b/requirements.development.txt index 24bd792..ff5ec4e 100644 --- a/requirements.development.txt +++ b/requirements.development.txt @@ -3,6 +3,12 @@ toml packaging setuptools +# +# Lint +# +mypy>=0.910 +ruff>=0.9 + # # Integration tests # diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..92698c7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,103 @@ +target-version = "py38" +line-length = 80 + +[lint] +select = [ + "A", + "ARG", + "B", + # "BLE", + "C", + "C4", + # "C90" mccabe + + # Not clear if this check is useful. Too many commas everywhere. + # "COM", + + # "D", + # "DTZ", + # "E", + "EXE", + "ERA", + "F", + # "FBT", + "G", + "I", + "ISC", + "ICN", + # "N", + "PGH", + # "PIE", + "PL", # Pylint + # "PT", # pytest + # "PTH", + "Q", # Quotes + # "RET", + # "RUF", + # "S", + # "SIM", + # "SLF", + "T20", + # "TCH", + # "TRY", + "UP", + "W", + "YTT", +] + +ignore = [ + # TBD: Unnecessary `list` comprehension (rewrite using `list()`) + "C416", + # TBD: Unnecessary `map` usage (rewrite using a `list` comprehension) + "C417", + + # X is too complex + "C901", + + "E501", # (line length violations). + + # A warning by ruff format: + # warning: The following rules may cause conflicts when used with the formatter: `ISC001`. + "ISC001", + + # Too many return statements + "PLR0911", + + # Too many branches + "PLR0912", + + # Too many arguments in function definition + "PLR0913", + + # Too many statements + "PLR0915", + + # TBD: Enable: Use `sys.exit()` instead of `exit` + "PLR1722", + + # Magic value used in comparison + "PLR2004", + + # Use `elif` instead of `else` then `if`, to reduce indentation + "PLR5501", + + "UP035", # [*] Import from `collections.abc` instead: `Iterator` + "UP038", # [*] Use `X | Y` in `isinstance` call instead of `(X, Y)` (conflict with Pylint) +] + +# Avoid trying to fix flake8-bugbear (`B`) violations. +unfixable = ["B"] + +# Skip non UTF-8 test files +# exclude = ["tests/**/invalid_file*"] + +# B008 Do not perform function calls in argument defaults. +# The call is performed only once at function definition time. + +[lint.per-file-ignores] +# "foo.py" = ["B008"] + +# Some of our helpers have deliberatly the name of a standard library module + +[lint.flake8-builtins] +builtins-allowed-modules = ["math", "pickle", "string"] diff --git a/tasks.py b/tasks.py index 6188f81..828b9bb 100644 --- a/tasks.py +++ b/tasks.py @@ -60,21 +60,76 @@ def bootstrap(context): @task def build(context): - run_invoke(context, "cd submodules/html2pdf && npm install && npm run build") + run_invoke( + context, "cd submodules/html2pdf && npm install && npm run build" + ) @task def format_readme(context): - run_invoke(context, """ + run_invoke( + context, + """ prettier --write --print-width 80 --prose-wrap always --parser=markdown README.md - """) + """, + ) @task +def lint_ruff_format(context): + result: invoke.runners.Result = run_invoke( + context, + """ + ruff + format + *.py + hpdf/ + tests/integration/ + """, + ) + # Ruff always exits with 0, so we handle the output. + if "reformatted" in result.stdout: + print("invoke: ruff format found issues") # noqa: T201 + result.exited = 1 + raise invoke.exceptions.UnexpectedExit(result) + + +@task(aliases=["lr"]) +def lint_ruff(context): + run_invoke( + context, + """ + ruff check *.py hpdf/ --fix --cache-dir build/ruff + """, + ) + + +@task(aliases=["lm"]) +def lint_mypy(context): + # These checks do not seem to be useful: + # - import + # - misc + run_invoke( + context, + """ + mypy hpdf/ + --show-error-codes + --disable-error-code=import + --disable-error-code=misc + --cache-dir=build/mypy + --strict + --python-version=3.8 + """, + ) + + +@task(aliases=["l"]) def lint(context): - pass + lint_ruff_format(context) + lint_ruff(context) + lint_mypy(context) @task diff --git a/tests/integration/check_exists.py b/tests/integration/check_exists.py index 962b4d4..718a3d6 100644 --- a/tests/integration/check_exists.py +++ b/tests/integration/check_exists.py @@ -56,25 +56,19 @@ else: if os.path.exists(args.input_path): print( # noqa: T201 - "error: expected path to not exist, but it does: {}".format( - args.input_path - ), + f"error: expected path to not exist, but it does: {args.input_path}", file=sys.stderr, ) exit(1) if args.file and os.path.isfile(args.input_path): print( # noqa: T201 - "error: expected path to not exist, but is a file: {}".format( - args.input_path - ), + f"error: expected path to not exist, but is a file: {args.input_path}", file=sys.stderr, ) exit(1) if args.dir and os.path.isdir(args.input_path): print( # noqa: T201 - "error: expected path to not exist, but is a directory: {}".format( - args.input_path - ), + f"error: expected path to not exist, but is a directory: {args.input_path}", file=sys.stderr, ) exit(1) diff --git a/tests/integration/expect_exit.py b/tests/integration/expect_exit.py index b026418..1bc6312 100644 --- a/tests/integration/expect_exit.py +++ b/tests/integration/expect_exit.py @@ -51,9 +51,7 @@ unexpected_exit_code = process.returncode != expected_exit_code if unexpected_exit_code: print( # noqa: T201 - "error: expect_exit: expected exit code: {}, actual: {}".format( - expected_exit_code, process.returncode - ) + f"error: expect_exit: expected exit code: {expected_exit_code}, actual: {process.returncode}" ) unexpected_content = expect_no_content and len(stdout) > 0