From da378b20a46b3c8166d5bedc632c83f271ef85b8 Mon Sep 17 00:00:00 2001 From: Felipe Peter Date: Thu, 14 Dec 2023 10:42:29 +0800 Subject: [PATCH] chore: Backport to Python 3.8 --- docs/_scripts/generate_license_information.py | 4 +-- docs/conf.py | 5 ++-- src/voraus_template_updater/_schemas.py | 16 +++++------ .../_update_projects.py | 27 ++++++++++--------- tests/unit/test_update_projects.py | 12 ++++----- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/docs/_scripts/generate_license_information.py b/docs/_scripts/generate_license_information.py index b0ae051..d0f8bdb 100644 --- a/docs/_scripts/generate_license_information.py +++ b/docs/_scripts/generate_license_information.py @@ -5,7 +5,7 @@ import subprocess from pathlib import Path from tempfile import TemporaryDirectory -from typing import Optional +from typing import Dict, List, Optional import jinja2 @@ -36,7 +36,7 @@ ) with open(licenses_file, encoding="utf-8") as handle: - licenses: list[dict[str, Optional[str]]] = json.load(fp=handle) + licenses: List[Dict[str, Optional[str]]] = json.load(fp=handle) for license_ in licenses: for key in ["LicenseFile", "NoticeFile"]: diff --git a/docs/conf.py b/docs/conf.py index fc30f79..0ff38f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ import json import os import sys +from typing import List import importlib_metadata @@ -36,10 +37,10 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named "sphinx.ext.*") or your custom ones. -custom_extensions: list[str] = [] +custom_extensions: List[str] = [] # DO NOT ADD ANY EXTENSIONS TO THIS LIST, USE THE `custom_extensions` list for your extensions -template_extensions: list[str] = [ +template_extensions: List[str] = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.doctest", diff --git a/src/voraus_template_updater/_schemas.py b/src/voraus_template_updater/_schemas.py index 02d9c9e..b7d570b 100644 --- a/src/voraus_template_updater/_schemas.py +++ b/src/voraus_template_updater/_schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from enum import Enum -from typing import Optional +from typing import Dict, List, Optional from github.PullRequest import PullRequest from pydantic import BaseModel, ConfigDict @@ -56,8 +56,8 @@ class Project(BaseModel): class Summary(BaseModel): """A summary of the checked and updates projects.""" - projects: list[Project] = [] - skipped_projects: list[SkippedProject] = [] + projects: List[Project] = [] + skipped_projects: List[SkippedProject] = [] def print(self) -> None: """Prints the summary.""" @@ -69,7 +69,7 @@ def print(self) -> None: _print_table_of_skipped_projects(self.skipped_projects) -def _print_table_of_projects(projects: list[Project]) -> None: +def _print_table_of_projects(projects: List[Project]) -> None: title = _get_table_title(projects) table = Table(title=title) table.add_column("Maintainer") @@ -125,7 +125,7 @@ def _print_table_of_projects(projects: list[Project]) -> None: Console().print(table) -def _get_table_title(projects: list[Project]) -> str: +def _get_table_title(projects: List[Project]) -> str: processed_projects = len(projects) up_to_date_projects = len(list(filter(lambda p: p.status == Status.UP_TO_DATE, projects))) @@ -136,8 +136,8 @@ def _get_table_title(projects: list[Project]) -> str: ) -def _get_projects_by_maintainer(projects: list[Project]) -> dict[Optional[str], list[Project]]: - projects_by_maintainer: dict[Optional[str], list[Project]] = {} +def _get_projects_by_maintainer(projects: List[Project]) -> Dict[Optional[str], List[Project]]: + projects_by_maintainer: Dict[Optional[str], List[Project]] = {} for project in sorted( projects, key=lambda x: x.maintainer or "z" * 100 # Projects without maintainers sorted to end @@ -149,7 +149,7 @@ def _get_projects_by_maintainer(projects: list[Project]) -> dict[Optional[str], return projects_by_maintainer -def _print_table_of_skipped_projects(projects: list[SkippedProject]) -> None: +def _print_table_of_skipped_projects(projects: List[SkippedProject]) -> None: table = Table(title=f"Skipped projects: {len(projects)}") table.add_column("Project") table.add_column("URL") diff --git a/src/voraus_template_updater/_update_projects.py b/src/voraus_template_updater/_update_projects.py index b2fe5c0..ec2ed45 100644 --- a/src/voraus_template_updater/_update_projects.py +++ b/src/voraus_template_updater/_update_projects.py @@ -6,7 +6,7 @@ from datetime import datetime from pathlib import Path from tempfile import TemporaryDirectory -from typing import Annotated, Optional +from typing import List, Optional import cruft import git @@ -18,6 +18,7 @@ from github.Repository import Repository from requests import HTTPError from typer import Argument, Option, Typer +from typing_extensions import Annotated from voraus_template_updater._schemas import CruftConfig, Project, SkippedProject, Status, Summary @@ -37,7 +38,7 @@ @app.command() -def _check_and_update_projects( +def _check_and_update_projects( # pylint: disable=dangerous-default-value # Only meant for CLI usage github_organization: Annotated[str, Argument(help="The GitHub organization in which to update repositories.")], github_access_token: Annotated[ str, @@ -49,16 +50,14 @@ def _check_and_update_projects( ), ] = "", maintainer_field: Annotated[ - list[str], + List[str], Option( help="Cookiecutter variable name that contains the name of a project maintainer. " "For example, if one of the templates you are updating contains a 'maintainer' variable, specify this " "option as 'maintainer'. Can be provided multiple times if multiple templates are processed and use " "different variable names to define a maintainer. The first hit from the list will be used." ), - ] = [ - "full_name" - ], # pylint: disable=dangerous-default-value + ] = ["full_name"], ) -> Summary: summary = Summary() @@ -132,7 +131,7 @@ def _check_and_update_projects( def _get_cruft_config(repo: Repository) -> CruftConfig: cruft_json = repo.get_contents(".cruft.json") - if isinstance(cruft_json, list): + if isinstance(cruft_json, List): raise RuntimeError( f"Repository '{repo.name}' contains more than one '.cruft.json' file. " "This use case is currently not supported." @@ -144,7 +143,7 @@ def _get_cruft_config(repo: Repository) -> CruftConfig: return CruftConfig.model_validate_json(response.content) -def _get_maintainer(maintainer_fields: list[str], cruft_config: CruftConfig) -> Optional[str]: +def _get_maintainer(maintainer_fields: List[str], cruft_config: CruftConfig) -> Optional[str]: maintainer = None for field_name in maintainer_fields: if field_name in cruft_config.context["cookiecutter"]: @@ -154,10 +153,12 @@ def _get_maintainer(maintainer_fields: list[str], cruft_config: CruftConfig) -> def _clone_repo(repo_url: str, github_access_token: str, target_path: Path) -> Repo: + url = repo_url.replace("git@github.com:", "https://github.com/") + url = url[:-4] if url.endswith(".git") else url # str.removesuffix only supported by Python >= 3.9 + url = url.replace("github.com", f"x-access-token:{github_access_token}@github.com") + return git.Repo.clone_from( - url=repo_url.replace("git@github.com:", "https://github.com/") - .removesuffix(".git") - .replace("github.com", f"x-access-token:{github_access_token}@github.com"), + url=url, to_path=target_path, ) @@ -213,7 +214,9 @@ def _get_pr_body(project: Project, github_access_token: str) -> str: # example `feat: Added feature (#123)`. However GitHub will resolve these links to the current repository although # they refer to pull requests in the template repository. We therefore need to change these links to point to the # pull requests in the template repository. - link = project.template_url.replace("git@github.com:", "https://github.com/").removesuffix(".git") + "/pull/{}" + link = project.template_url.replace("git@github.com:", "https://github.com/") + link = link[:-4] if link.endswith(".git") else link # str.removesuffix only supported by Python >= 3.9 + link += "/pull/{}" for i_message, message in enumerate(commit_messages): commit_messages[i_message] = re.sub( r"\(#(\d+)\)", lambda match: f"([PR]({link.format(match.groups()[0])}))", message diff --git a/tests/unit/test_update_projects.py b/tests/unit/test_update_projects.py index 0a56441..199175c 100644 --- a/tests/unit/test_update_projects.py +++ b/tests/unit/test_update_projects.py @@ -1,9 +1,9 @@ """Contains unit tests for template updates.""" import logging -from collections.abc import Generator from datetime import datetime from pathlib import Path +from typing import Generator, List from unittest.mock import MagicMock, patch import pytest @@ -26,7 +26,7 @@ @pytest.fixture(autouse=True) def _set_up_mocks( - cloned_repo_mocks: list[MagicMock], # pylint: disable=unused-argument + cloned_repo_mocks: List[MagicMock], # pylint: disable=unused-argument cruft_config: MagicMock, # pylint: disable=unused-argument organization_mock: MagicMock, repo_mock: MagicMock, @@ -89,7 +89,7 @@ def _cruft_config_fixture(request: pytest.FixtureRequest) -> Generator[CruftConf @pytest.fixture(name="cloned_repo_mocks") -def _cloned_repo_mocks_fixture(request: pytest.FixtureRequest) -> Generator[list[MagicMock], None, None]: +def _cloned_repo_mocks_fixture(request: pytest.FixtureRequest) -> Generator[List[MagicMock], None, None]: """Yields a list of git.Repo mocks that can be used for mocking behavior on a cloned project or template repo.""" if "no_clone_repo_mock" in request.keywords: @@ -185,8 +185,8 @@ def test_repos_are_skipped_if_cruft_json_cannot_be_downloaded( with caplog.at_level(logging.WARNING): summary = _check_and_update_projects(ORGANIZATION) - assert requests_mock.get.called_once_with(repo_mock.get_contents.return_value.download_url, timeout=10) - assert response_mock.raise_for_status.called_once() + requests_mock.get.assert_called_once_with(repo_mock.get_contents.return_value.download_url, timeout=10) + response_mock.raise_for_status.assert_called_once() assert caplog.record_tuples == [ ( @@ -324,7 +324,7 @@ def test_update_project_success( cruft_check_mock: MagicMock, cruft_update_mock: MagicMock, repo_mock: MagicMock, - cloned_repo_mocks: list[MagicMock], + cloned_repo_mocks: List[MagicMock], cruft_config: CruftConfig, caplog: pytest.LogCaptureFixture, ) -> None: