diff --git a/scripts/create_django_issue.py b/scripts/create_django_issue.py index f10a9446..ccf921c8 100644 --- a/scripts/create_django_issue.py +++ b/scripts/create_django_issue.py @@ -6,9 +6,10 @@ patches, only comparing major and minor version numbers. This script handles when there are multiple Django versions that need to keep up to date. """ +from __future__ import annotations import os -from typing import Sequence, TYPE_CHECKING +from typing import NamedTuple, Sequence, TYPE_CHECKING import requests import sys @@ -26,6 +27,19 @@ REQUIREMENTS_DIR = ROOT / "{{cookiecutter.project_slug}}" / "requirements" GITHUB_REPO = "cookiecutter/cookiecutter-django" +class Version(NamedTuple): + major: str + minor: str + + def __str__(self) -> str: + return f"{self.major}.{self.minor}" + + @classmethod + def parse(cls, version_str: str) -> Version: + major, minor, *_ = version_str.split(".") + return cls(major=major, minor=minor) + + def get_package_info(package: str) -> dict: # "django" converts to "Django" on redirect r = requests.get(f"https://pypi.org/pypi/{package}/json", allow_redirects=True) @@ -40,17 +54,17 @@ def get_package_versions(package_info: dict, reverse=True, *, include_pre=False) # package version, you could simple do get_package_info()["info"]["version"] releases: Sequence[str] = package_info["releases"].keys() if not include_pre: - releases = [x for x in releases if x.replace(".", "").isdigit()] + releases = [r for r in releases if r.replace(".", "").isdigit()] return sorted(releases, reverse=reverse) -def get_name_and_version(requirements_line: str) -> tuple[str, str]: +def get_name_and_version(requirements_line: str) -> tuple[str, ...]: full_name, version = requirements_line.split(" ", 1)[0].split("==") name_without_extras = full_name.split("[", 1)[0] return name_without_extras, version -def get_all_latest_django_versions() -> tuple[str, list[str]]: +def get_all_latest_django_versions() -> tuple[Version, list[Version]]: """ Grabs all Django versions that are worthy of a GitHub issue. Depends on if Django versions has higher major version or minor version @@ -67,16 +81,15 @@ def get_all_latest_django_versions() -> tuple[str, list[str]]: # Begin parsing and verification _, current_version_str = get_name_and_version(line) # Get a tuple of (major, minor) - ignoring patch version - current_minor_version = tuple(current_version_str.split(".")[:2]) + current_minor_version = Version.parse(current_version_str) all_django_versions = get_package_versions(get_package_info("django")) - newer_versions: set[tuple] = set() + newer_versions: set[Version] = set() for version_str in all_django_versions: - released_minor_version = tuple(version_str.split(".")[:2]) + released_minor_version = Version.parse(version_str) if released_minor_version > current_minor_version: newer_versions.add(released_minor_version) - needed_versions_str = ['.'.join(v) for v in sorted(newer_versions)] - return line, needed_versions_str + return current_minor_version, sorted(newer_versions, reverse=True) def get_first_digit(tokens) -> str: @@ -97,14 +110,14 @@ VITAL_BUT_UNKNOWN = [ class GitHubManager: - def __init__(self, base_dj_version: str, needed_dj_versions: list[str]): + def __init__(self, base_dj_version: Version, needed_dj_versions: list[Version]): self.github = Github(os.getenv("GITHUB_TOKEN", None)) self.repo = self.github.get_repo(GITHUB_REPO) self.base_dj_version = base_dj_version self.needed_dj_versions = needed_dj_versions # (major+minor) Version and description - self.existing_issues: dict[str, "Issue"] = {} + self.existing_issues: dict[Version, Issue] = {} # Load all requirements from our requirements files and preload their # package information like a cache: @@ -123,10 +136,11 @@ class GitHubManager: for requirements_file in self.requirements_files: with (REQUIREMENTS_DIR / f"{requirements_file}.txt").open() as f: for line in f.readlines(): - if "==" in line and not line.startswith('{%'): + if "==" in line and not line.startswith("{%"): name, version = get_name_and_version(line) self.requirements[requirements_file][name] = ( - version, get_package_info(name) + version, + get_package_info(name), ) def load_existing_issues(self): @@ -144,32 +158,16 @@ class GitHubManager: ) ) for issue in issues: - try: - dj_version = get_first_digit(issue.title.split(" ")) - except StopIteration: - try: - # Some padding; randomly chose 4 to make sure we don't get a random - # version number from a package that's not Django - dj_version = get_first_digit(issue.body.split(" ", 4)) - except StopIteration: - print( - f"Found issue {issue.title} that had an invalid syntax", - "(Did not have a Django version number in the title or body's" - f" first word. Issue number: [{issue.id}]({issue.url}))" - ) - continue - if self.base_dj_version > dj_version: + issue_version_str = issue.title.split(" ")[-1] + issue_version = Version.parse(issue_version_str) + if self.base_dj_version > issue_version: issue.edit(state="closed") print(f"Closed issue {issue.title} (ID: [{issue.id}]({issue.url}))") - try: - self.needed_dj_versions.remove(dj_version) - except ValueError: - print("Something weird happened. Continuing anyway (Warning ID: 1)") else: - self.existing_issues[dj_version] = issue + self.existing_issues[issue_version] = issue def get_compatibility( - self, package_name: str, package_info: dict, needed_dj_version + self, package_name: str, package_info: dict, needed_dj_version: Version ): """ Verify compatibility via setup.py classifiers. If Django is not in the @@ -191,24 +189,17 @@ class GitHubManager: return "", "❓" # Check classifiers if it includes Django - supported_dj_versions = [] + supported_dj_versions: list[Version] = [] for classifier in package_info["info"]["classifiers"]: # Usually in the form of "Framework :: Django :: 3.2" tokens = classifier.split(" ") - for token in tokens: - if token.lower() == "django": - try: - _version = get_first_digit(reversed(tokens)) - except StopIteration: - pass - else: - supported_dj_versions.append( - float(".".join(_version.split(".", 2)[:2])) - ) + if len(tokens) >= 5 and tokens[2].lower() == "django": + version = Version.parse(tokens[4]) + if len(version) == 2: + supported_dj_versions.append(version) if supported_dj_versions: - needed_dj_version = float(needed_dj_version) - if any(x >= needed_dj_version for x in supported_dj_versions): + if any(v >= needed_dj_version for v in supported_dj_versions): return package_info["info"]["version"], "✅" else: return "", "❌" @@ -227,19 +218,19 @@ class GitHubManager: ] def _get_md_home_page_url(self, package_info: dict): - urls = [package_info["info"].get(x) for x in self.HOME_PAGE_URL_KEYS] + urls = [ + package_info["info"].get(url_key) for url_key in self.HOME_PAGE_URL_KEYS + ] try: return f"[{{}}]({next(item for item in urls if item)})" except StopIteration: return "{}" - def generate_markdown(self, needed_dj_version: str): + def generate_markdown(self, needed_dj_version: Version): requirements = f"{needed_dj_version} requirements tables\n\n" for _file in self.requirements_files: - requirements += ( - _TABLE_HEADER.format_map( - {"file": _file, "dj_version": needed_dj_version} - ) + requirements += _TABLE_HEADER.format_map( + {"file": _file, "dj_version": needed_dj_version} ) for package_name, (version, info) in self.requirements[_file].items(): compat_version, icon = self.get_compatibility( @@ -251,8 +242,8 @@ class GitHubManager: ) return requirements - def create_or_edit_issue(self, needed_dj_version, description): - if issue := self.existing_issues.get(str(needed_dj_version)): + def create_or_edit_issue(self, needed_dj_version: Version, description: str): + if issue := self.existing_issues.get(needed_dj_version): issue.edit(body=description) else: self.repo.create_issue(