Refactor how versions are handled

This commit is contained in:
Bruno Alla 2021-11-09 00:21:08 +00:00
parent 007fd0206e
commit 44ba3cf19e

View File

@ -6,9 +6,10 @@ patches, only comparing major and minor version numbers.
This script handles when there are multiple Django versions that need This script handles when there are multiple Django versions that need
to keep up to date. to keep up to date.
""" """
from __future__ import annotations
import os import os
from typing import Sequence, TYPE_CHECKING from typing import NamedTuple, Sequence, TYPE_CHECKING
import requests import requests
import sys import sys
@ -26,6 +27,19 @@ REQUIREMENTS_DIR = ROOT / "{{cookiecutter.project_slug}}" / "requirements"
GITHUB_REPO = "cookiecutter/cookiecutter-django" 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: def get_package_info(package: str) -> dict:
# "django" converts to "Django" on redirect # "django" converts to "Django" on redirect
r = requests.get(f"https://pypi.org/pypi/{package}/json", allow_redirects=True) 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"] # package version, you could simple do get_package_info()["info"]["version"]
releases: Sequence[str] = package_info["releases"].keys() releases: Sequence[str] = package_info["releases"].keys()
if not include_pre: 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) 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("==") full_name, version = requirements_line.split(" ", 1)[0].split("==")
name_without_extras = full_name.split("[", 1)[0] name_without_extras = full_name.split("[", 1)[0]
return name_without_extras, version 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 Grabs all Django versions that are worthy of a GitHub issue. Depends on
if Django versions has higher major version or minor version 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 # Begin parsing and verification
_, current_version_str = get_name_and_version(line) _, current_version_str = get_name_and_version(line)
# Get a tuple of (major, minor) - ignoring patch version # 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")) 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: 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: if released_minor_version > current_minor_version:
newer_versions.add(released_minor_version) newer_versions.add(released_minor_version)
needed_versions_str = ['.'.join(v) for v in sorted(newer_versions)] return current_minor_version, sorted(newer_versions, reverse=True)
return line, needed_versions_str
def get_first_digit(tokens) -> str: def get_first_digit(tokens) -> str:
@ -97,14 +110,14 @@ VITAL_BUT_UNKNOWN = [
class GitHubManager: 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.github = Github(os.getenv("GITHUB_TOKEN", None))
self.repo = self.github.get_repo(GITHUB_REPO) self.repo = self.github.get_repo(GITHUB_REPO)
self.base_dj_version = base_dj_version self.base_dj_version = base_dj_version
self.needed_dj_versions = needed_dj_versions self.needed_dj_versions = needed_dj_versions
# (major+minor) Version and description # (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 # Load all requirements from our requirements files and preload their
# package information like a cache: # package information like a cache:
@ -123,10 +136,11 @@ class GitHubManager:
for requirements_file in self.requirements_files: for requirements_file in self.requirements_files:
with (REQUIREMENTS_DIR / f"{requirements_file}.txt").open() as f: with (REQUIREMENTS_DIR / f"{requirements_file}.txt").open() as f:
for line in f.readlines(): 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) name, version = get_name_and_version(line)
self.requirements[requirements_file][name] = ( self.requirements[requirements_file][name] = (
version, get_package_info(name) version,
get_package_info(name),
) )
def load_existing_issues(self): def load_existing_issues(self):
@ -144,32 +158,16 @@ class GitHubManager:
) )
) )
for issue in issues: for issue in issues:
try: issue_version_str = issue.title.split(" ")[-1]
dj_version = get_first_digit(issue.title.split(" ")) issue_version = Version.parse(issue_version_str)
except StopIteration: if self.base_dj_version > issue_version:
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.edit(state="closed") issue.edit(state="closed")
print(f"Closed issue {issue.title} (ID: [{issue.id}]({issue.url}))") 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: else:
self.existing_issues[dj_version] = issue self.existing_issues[issue_version] = issue
def get_compatibility( 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 Verify compatibility via setup.py classifiers. If Django is not in the
@ -191,24 +189,17 @@ class GitHubManager:
return "", "" return "", ""
# Check classifiers if it includes Django # Check classifiers if it includes Django
supported_dj_versions = [] supported_dj_versions: list[Version] = []
for classifier in package_info["info"]["classifiers"]: for classifier in package_info["info"]["classifiers"]:
# Usually in the form of "Framework :: Django :: 3.2" # Usually in the form of "Framework :: Django :: 3.2"
tokens = classifier.split(" ") tokens = classifier.split(" ")
for token in tokens: if len(tokens) >= 5 and tokens[2].lower() == "django":
if token.lower() == "django": version = Version.parse(tokens[4])
try: if len(version) == 2:
_version = get_first_digit(reversed(tokens)) supported_dj_versions.append(version)
except StopIteration:
pass
else:
supported_dj_versions.append(
float(".".join(_version.split(".", 2)[:2]))
)
if supported_dj_versions: if supported_dj_versions:
needed_dj_version = float(needed_dj_version) if any(v >= needed_dj_version for v in supported_dj_versions):
if any(x >= needed_dj_version for x in supported_dj_versions):
return package_info["info"]["version"], "" return package_info["info"]["version"], ""
else: else:
return "", "" return "", ""
@ -227,19 +218,19 @@ class GitHubManager:
] ]
def _get_md_home_page_url(self, package_info: dict): 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: try:
return f"[{{}}]({next(item for item in urls if item)})" return f"[{{}}]({next(item for item in urls if item)})"
except StopIteration: except StopIteration:
return "{}" 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" requirements = f"{needed_dj_version} requirements tables\n\n"
for _file in self.requirements_files: for _file in self.requirements_files:
requirements += ( requirements += _TABLE_HEADER.format_map(
_TABLE_HEADER.format_map( {"file": _file, "dj_version": needed_dj_version}
{"file": _file, "dj_version": needed_dj_version}
)
) )
for package_name, (version, info) in self.requirements[_file].items(): for package_name, (version, info) in self.requirements[_file].items():
compat_version, icon = self.get_compatibility( compat_version, icon = self.get_compatibility(
@ -251,8 +242,8 @@ class GitHubManager:
) )
return requirements return requirements
def create_or_edit_issue(self, needed_dj_version, description): def create_or_edit_issue(self, needed_dj_version: Version, description: str):
if issue := self.existing_issues.get(str(needed_dj_version)): if issue := self.existing_issues.get(needed_dj_version):
issue.edit(body=description) issue.edit(body=description)
else: else:
self.repo.create_issue( self.repo.create_issue(