From 31f4baeb3003112ac6c6faa97c8ab010b2560a65 Mon Sep 17 00:00:00 2001 From: Jacob Pavlock Date: Sun, 29 Jun 2025 13:20:39 +0200 Subject: [PATCH 1/2] build: transition to ruff for linting and formatting --- .github/scripts/get_latest_changelog.py | 4 +- .github/scripts/prep_release.py | 80 ++++++++++++++++--------- .pre-commit-config.yaml | 26 ++------ pyproject.toml | 45 +++++++++----- setup.cfg | 49 --------------- 5 files changed, 89 insertions(+), 115 deletions(-) delete mode 100644 setup.cfg diff --git a/.github/scripts/get_latest_changelog.py b/.github/scripts/get_latest_changelog.py index de82b07..dd1727a 100644 --- a/.github/scripts/get_latest_changelog.py +++ b/.github/scripts/get_latest_changelog.py @@ -26,14 +26,14 @@ def main() -> None: if re.match(rf"{version}", line) or re.match(r"=", line): correct_version = True continue - elif re.match(r"v\d+\.\d+\.\d+", line): + if re.match(r"v\d+\.\d+\.\d+", line): # encountered next version break if correct_version: changes += line - print(convert_rst_to_md(changes).strip()) + print(convert_rst_to_md(changes).strip()) # noqa: T201 def convert_rst_to_md(text: str) -> str: diff --git a/.github/scripts/prep_release.py b/.github/scripts/prep_release.py index 0d4ee11..4c55b98 100644 --- a/.github/scripts/prep_release.py +++ b/.github/scripts/prep_release.py @@ -9,17 +9,23 @@ (pytest's script I stole a lot from) """ +from __future__ import annotations + import argparse -import datetime +import datetime as dt import pathlib import re +import shutil import subprocess from textwrap import indent +from typing import TYPE_CHECKING import github3 -from github3.repos import Repository -SLUG = "MoeMusic/moe_{plugin name}" +if TYPE_CHECKING: + from github3.repos import Repository + +SLUG = "MoeMusic/Moe" PR_BODY = """ This pull request was created automatically by a manual trigger. @@ -67,7 +73,7 @@ class Commit: title=(?P.+);\s body=(?P<body>.*); """, - re.VERBOSE | re.S, + re.VERBOSE | re.DOTALL, ) # based on "git_log_format defined below" def __init__(self, commit_log: str) -> None: @@ -77,7 +83,10 @@ def __init__(self, commit_log: str) -> None: self.breaking = False match = re.match(self.COMMIT_RE, commit_log) - assert match + if not match: + err_msg = f"Could not parse commit log: '{commit_log}'" + raise ValueError(err_msg) + self.commit_hash = match["hash"] title = match["title"] self.body = match["body"].strip() @@ -110,37 +119,52 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("token") + git_path = shutil.which("git") + if not git_path: + err_msg = "Could not find 'git' executable in PATH." + raise RuntimeError(err_msg) + args = parser.parse_args() - prepare_release_pr(args.token) + prepare_release_pr(args.token, git_path) -def prepare_release_pr(token: str) -> None: +def prepare_release_pr(token: str, git_path: str) -> None: """Prepares the release pull request.""" - subprocess.run(["git", "config", "user.name", "Moe bot"], check=True) - subprocess.run(["git", "config", "user.email", "<>"], check=True) + subprocess.run([git_path, "config", "user.name", "Moe bot"], check=True) # noqa: S603 git is a trusted executable + subprocess.run([git_path, "config", "user.email", "<>"], check=True) # noqa: S603 git is a trusted executable - old_version = subprocess.run( - ["cz", "version", "--project"], text=True, capture_output=True, check=True + cz_path = shutil.which("cz") + if not cz_path: + err_msg = "Could not find 'cz' executable in PATH." + raise RuntimeError(err_msg) + + old_version = subprocess.run( # noqa: S603 trusted inputs + [cz_path, "version", "--project"], text=True, capture_output=True, check=True ).stdout old_version = f"v{old_version.strip()}" - subprocess.run(["cz", "bump", "--files-only"], check=True) - new_version = subprocess.run( - ["cz", "version", "--project"], text=True, capture_output=True, check=True + subprocess.run([cz_path, "bump", "--files-only"], check=True) # noqa: S603 trusted inputs + new_version = subprocess.run( # noqa: S603 trusted inputs + [cz_path, "version", "--project"], text=True, capture_output=True, check=True ).stdout new_version = f"v{new_version.strip()}" - generate_changelog(old_version, new_version) + generate_changelog(old_version, new_version, git_path) release_branch = f"release_{new_version}" - subprocess.run(["git", "checkout", "-b", f"{release_branch}"], check=True) + subprocess.run([git_path, "checkout", "-b", f"{release_branch}"], check=True) # noqa: S603 git is a trusted executable - subprocess.run(["git", "commit", "-a", "-m", f"release: {new_version}"], check=True) + subprocess.run( # noqa: S603 git is a trusted executable + [git_path, "commit", "-a", "-m", f"release: {new_version}"], check=True + ) oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" - subprocess.run( - ["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"], check=True + subprocess.run( # noqa: S603 git is a trusted executable + [git_path, "push", oauth_url, f"HEAD:{release_branch}", "--force"], check=True ) repo = login(token) - assert repo + if not repo: + err_msg = f"Failed to get repository '{SLUG}'." + raise RuntimeError(err_msg) + repo.create_pull( f"Prepare release {new_version}", base="main", @@ -149,24 +173,23 @@ def prepare_release_pr(token: str) -> None: ) -def generate_changelog(old_version: str, new_version: str) -> None: +def generate_changelog(old_version: str, new_version: str, git_path: str) -> None: """Generates a changelog for a new release.""" changelog_path = pathlib.Path("CHANGELOG.rst") - release_title = f"{new_version} ({datetime.date.today()})" + release_title = f"{new_version} ({dt.datetime.now(dt.timezone.utc).date()})" changelog_title = f"\n{release_title}\n" changelog_title += "=" * len(release_title) + "\n" field_delim = ";" commit_delim = ";|;" git_log_format = ( - f"hash=%H{field_delim} title=%s{field_delim} " - f"body=%b{field_delim}{commit_delim}" + f"hash=%H{field_delim} title=%s{field_delim} body=%b{field_delim}{commit_delim}" ) git_log = str( - subprocess.run( + subprocess.run( # noqa: S603 git is a trusted executable [ - "git", + git_path, "log", f"--pretty=format:{git_log_format}", f"{old_version}..HEAD", @@ -222,7 +245,10 @@ def generate_changelog(old_version: str, new_version: str) -> None: def login(token: str) -> Repository | None: """Logins to github and returns the working repository.""" github = github3.login(token=token) - assert github + if not github: + err_msg = "Failed to login to GitHub. Check the provided token." + raise RuntimeError(err_msg) + owner, repo = SLUG.split("/") return github.repository(owner, repo) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a19b56a..911033d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,11 @@ default_stages: [pre-commit] repos: - - repo: local + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.1 hooks: - - id: black - name: black - entry: black - language: system - types: [python] - - - repo: local - hooks: - - id: flake8 - name: flake8 - entry: flake8 - language: system - types: [python] + - id: ruff-check + - id: ruff-format - repo: local hooks: @@ -25,14 +15,6 @@ repos: language: system stages: [commit-msg] - - repo: local - hooks: - - id: isort - name: isort - entry: isort - language: system - types: [python] - - repo: local hooks: - id: pyright diff --git a/pyproject.toml b/pyproject.toml index 9ef07bc..8cb24bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,21 +18,11 @@ pytest-cov = "^6.0.0" tox = "^4.0.0" [tool.poetry.group.lint.dependencies] -black = "^24.0.0" +ruff = "==0.12.1" commitizen = "^3.0.0" -darglint = "^1.8.1" -flake8 = "^7.0.0" -flake8-alphabetize = "^0.0.19" -flake8-bugbear = "^24.0.0" -flake8-comprehensions = "^3.10.0" -flake8-docstrings = "^1.5.0" -flake8-pytest-style = "^2.0.0" -flake8-use-fstring = "^1.1" "github3.py" = "^4.0.0" -isort = "^5.10.1" -mccabe = "^0.7.0" pre-commit = "^4.0.0" -pyright = "^1.1.267" +pyright = "==1.1.402" [tool.poetry.group.docs.dependencies] furo = "*" @@ -55,9 +45,6 @@ bump_pattern = '^(build|deprecate|feat|fix|perf)' bump_map = { feat = "MINOR", build = "PATCH", deprecate = "PATCH", fix = "PATCH", perf = "PATCH" } schema_pattern = '(build|ci|deprecate|docs|feat|fix|perf|refactor|release|style|test)(\(\w+\))?!?:\s\S.*' -[tool.isort] -profile = 'black' - [tool.pytest.ini_options] log_cli_level = 10 addopts = "--color=yes" @@ -74,6 +61,34 @@ exclude = [ ] pythonPlatform = "All" +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "COM", # the formatter handles trailing commas + "G004", # use f strings in logging for a consistent string format + "PYI056", # use extend on __all__ + "RET501", # don't care about return None semantics + "RET502", # don't care about return None semantics + "RET503", # don't care about return None semantics +] + +[tool.ruff.lint.per-file-ignores] +"docs/conf.py" = [ + "ERA001", # conf.py includes commented config examples + "INP001", # docs directory isn't a python package +] +"tests/*" = [ + "ANN", # tests don't need type annotations + "ARG", # arguments in pytest are fixtures + "C90", # tests are as complex as they need to be + "INP001", # tests don't need __init__ + "RET", # tests don't need returns + "S", # asserts used in tests +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + [build-system] requires = ["poetry-core>=1.0"] build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4298a81..0000000 --- a/setup.cfg +++ /dev/null @@ -1,49 +0,0 @@ -[flake8] -max-line-length = 88 -docstring-convention = google -docstring_style = google -strictness = long -max-complexity = 10 -use_class_attributes_order_strict_mode = True -select = - AZ400, # alphabetize __all__ list - B, # bugbear - B9, # bugbear opinionated errors; disabled by default - C4 # comprehensions - C90, # mccabe - CCE, # class attributes order - D, # docstrings - DAR, # darglint (another docstring linter) - E, # flake8 and pycodestyle error - F, # pyflakes - FS, # use-fstring - N8, # pep8-naming - PT, # pytest style - W6, # pycodestyle deprecation warnings -ignore = - # allow `@pytest.fixture` (no parens) - PT001, - # allow `@pytest.mark.foo` (no parens) - PT023, - # bug-bear line length; just use flake8 line length limit - B950, - # whitespace before ':' (black conflict) - E203, - # Excess exceptions in "Raises" (I like to document sub-private-methods) - DAR402, - # too many leading '#' for block comment - E266, -per-file-ignores = - # E800: config file has commented out code as examples - alembic/env.py:E800 - # D1: database migrations don't need docstrings - # I: isort errors tend to misinterpret alembic as a local package - alembic/versions/*.py:D1,I - # B907: manually specified quotes are usually intentional for testing purposes - # C901: don't check complexity for tests - # DAR101: pytest uses fixtures as arguments, documenting each use is unnecessary - # DAR102: factory arguments with fixtures can be weird - tests/*:B907,C901,DAR101,DAR102 - # F401: import unused - # F403: allow * imports (used for packaging in some cases) - */__init__.py:F401,F403 From c59f38b006445e38d3c7ccae1f9d3372a89927a4 Mon Sep 17 00:00:00 2001 From: Jacob Pavlock <jtpavlock@gmail.com> Date: Sun, 29 Jun 2025 18:01:19 +0200 Subject: [PATCH 2/2] fixup! build: transition to ruff for linting and formatting --- .github/scripts/get_latest_changelog.py | 0 .github/scripts/prep_release.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/get_latest_changelog.py mode change 100644 => 100755 .github/scripts/prep_release.py diff --git a/.github/scripts/get_latest_changelog.py b/.github/scripts/get_latest_changelog.py old mode 100644 new mode 100755 diff --git a/.github/scripts/prep_release.py b/.github/scripts/prep_release.py old mode 100644 new mode 100755