From ac1b8e91d6155718a979f53a8176c8f34fb133e1 Mon Sep 17 00:00:00 2001 From: "Commentator2.0" Date: Thu, 27 Nov 2025 19:13:56 +0100 Subject: [PATCH] maintainers: run ruff additionally, the issue_import has been removed, as our mirror has been disconued Change-Id: I8cb4c807db6c415b0fd340ead58be341a05e6a34 --- maintainers/build-release-notes.py | 151 +++++++++++++++++------------ maintainers/issue_import.py | 148 ---------------------------- treefmt.toml | 2 +- 3 files changed, 89 insertions(+), 212 deletions(-) mode change 100644 => 100755 maintainers/build-release-notes.py delete mode 100644 maintainers/issue_import.py diff --git a/maintainers/build-release-notes.py b/maintainers/build-release-notes.py old mode 100644 new mode 100755 index 5b7933065..4d98d115f --- a/maintainers/build-release-notes.py +++ b/maintainers/build-release-notes.py @@ -1,35 +1,39 @@ +#!@python@ + +# ruff: noqa: T201 # Our generated MD file is expected on stdout +# This is the only file and is executable + from collections import defaultdict import frontmatter import pathlib import textwrap -from typing import Any, Tuple +from typing import Any import dataclasses import yaml +import argparse + GH_ROOT = "https://github.com/" GH_REPO_BASE = "https://github.com/NixOS/nix" FORGEJO_REPO_BASE = "https://git.lix.systems/lix-project/lix" FORGEJO_ROOT = "https://git.lix.systems/" GERRIT_BASE = "https://gerrit.lix.systems/c/lix/+" -KNOWN_KEYS = ('synopsis', 'cls', 'issues', 'prs', 'significance', 'category', 'credits') +KNOWN_KEYS = ("synopsis", "cls", "issues", "prs", "significance", "category", "credits") -SIGNIFICANCECES = { - None: 0, - 'significant': 10, -} +SIGNIFICANCECES = {None: 0, "significant": 10} # This is just hardcoded for better validation. If you think there should be # more of them, feel free to add more. # # Please update doc/manual/src/contributing/hacking.md if you do. Thanks~ CATEGORIES = [ - 'Breaking Changes', - 'Features', - 'Improvements', - 'Fixes', - 'Packaging', - 'Development', - 'Miscellany', + "Breaking Changes", + "Features", + "Improvements", + "Fixes", + "Packaging", + "Development", + "Miscellany", ] @@ -45,11 +49,10 @@ class AuthorInfo: def __str__(self) -> str: if self.forgejo: - return f'[{self.show_name()}]({FORGEJO_ROOT}{self.forgejo})' - elif self.github: - return f'[{self.show_name()}]({GH_ROOT}{self.github})' - else: - return self.show_name() + return f"[{self.show_name()}]({FORGEJO_ROOT}{self.forgejo})" + if self.github: + return f"[{self.show_name()}]({GH_ROOT}{self.github})" + return self.show_name() class AuthorInfoDB: @@ -57,14 +60,13 @@ class AuthorInfoDB: self.author_info = {name: AuthorInfo(name=name, **d) for (name, d) in author_info.items()} self.throw_on_missing = throw_on_missing - def __getitem__(self, name) -> str: + def __getitem__(self, name: str) -> str: if name in self.author_info: return str(self.author_info[name]) - else: - if self.throw_on_missing: - raise Exception(f'Missing author info for author {name}') - else: - return name + if self.throw_on_missing: + msg = f"Missing author info for author {name}" + raise Exception(msg) + return name def format_link(ident: str, gh_part: str, fj_part: str) -> str: @@ -80,37 +82,46 @@ def format_link(ident: str, gh_part: str, fj_part: str) -> str: elif ident.startswith("lix#"): num, link, base = int(ident[4:]), ident, f"{FORGEJO_REPO_BASE}/{fj_part}" else: - raise Exception("unrecognized reference format", ident) + msg = f"unrecognized reference format: {ident}" + raise Exception(msg) return f"[{link}]({base}/{num})" + def format_issue(issue: str) -> str: return format_link(issue, "issues", "issues") + + def format_pr(pr: str) -> str: return format_link(pr, "pull", "pulls") + + def format_cl(clid: int) -> str: return f"[cl/{clid}]({GERRIT_BASE}/{clid})" + def plural_list(strs: list[str]) -> str: if len(strs) <= 1: - return ''.join(strs) - else: - comma = ',' if len(strs) >= 3 else '' - return '{}{} and {}'.format(', '.join(strs[:-1]), comma, strs[-1]) + return "".join(strs) + comma = "," if len(strs) >= 3 else "" + return "{}{} and {}".format(", ".join(strs[:-1]), comma, strs[-1]) -def listify(l: list | int) -> list: - if not isinstance(l, list): - return [l] - else: - return l -def do_category(author_info: AuthorInfoDB, entries: list[Tuple[pathlib.Path, Any]]): - for p, entry in sorted(entries, key=lambda e: (-SIGNIFICANCECES[e[1].metadata.get('significance')], e[0])): +def listify(li: list | int) -> list: + if not isinstance(li, list): + return [li] + return li + + +def do_category(author_info: AuthorInfoDB, entries: list[tuple[pathlib.Path, Any]]): + for p, entry in sorted( + entries, key=lambda e: (-SIGNIFICANCECES[e[1].metadata.get("significance")], e[0]) + ): try: - header = entry.metadata['synopsis'] + header = entry.metadata["synopsis"] links = [] - links += [format_issue(str(s)) for s in listify(entry.metadata.get('issues', []))] - links += [format_pr(str(s)) for s in listify(entry.metadata.get('prs', []))] - links += [format_cl(int(cl)) for cl in listify(entry.metadata.get('cls', []))] + links += [format_issue(str(s)) for s in listify(entry.metadata.get("issues", []))] + links += [format_pr(str(s)) for s in listify(entry.metadata.get("prs", []))] + links += [format_cl(int(cl)) for cl in listify(entry.metadata.get("cls", []))] if links != []: header += " " + " ".join(links) @@ -118,12 +129,17 @@ def do_category(author_info: AuthorInfoDB, entries: list[Tuple[pathlib.Path, Any print(f"- {header}") print() else: - print("- ", end='') + print("- ", end="") - print(textwrap.indent(entry.content, ' ')) - if credits := listify(entry.metadata.get('credits', [])): + print(textwrap.indent(entry.content, " ")) + if credits_authors := listify(entry.metadata.get("credits", [])): print() - print(textwrap.indent('Many thanks to {} for this.'.format(plural_list(list(author_info[c] for c in credits))), ' ')) + print( + textwrap.indent( + f"Many thanks to {plural_list([author_info[c] for c in credits_authors])} for this.", + " ", + ) + ) # Blank line after each entry. print() @@ -132,23 +148,27 @@ def do_category(author_info: AuthorInfoDB, entries: list[Tuple[pathlib.Path, Any raise -def run_on_dir(author_info: AuthorInfoDB, d): +def run_on_dir(author_info: AuthorInfoDB, d: str): d = pathlib.Path(d) if not d.is_dir(): - raise ValueError(f'provided path {d} is not a directory') - paths = pathlib.Path(d).glob('[!.]*.md') + msg = f"provided path {d} is not a directory" + raise ValueError(msg) + paths = d.glob("[!.]*.md") entries = defaultdict(list) for p in paths: try: - e = frontmatter.load(p) # type: ignore - if 'synopsis' not in e.metadata: - raise Exception('missing synopsis') - unknownKeys = set(e.metadata.keys()) - set(KNOWN_KEYS) - if unknownKeys: - raise Exception('unknown keys', unknownKeys) - category = e.metadata.get('category', 'Miscellany') + e = frontmatter.load(p) # type: ignore + if "synopsis" not in e.metadata: + msg = "missing synopsis" + raise ValueError(msg) + unknown_keys = set(e.metadata.keys()) - set(KNOWN_KEYS) + if unknown_keys: + msg = f"unknown keys: {unknown_keys}" + raise ValueError(msg) + category = e.metadata.get("category", "Miscellany") if category not in CATEGORIES: - raise Exception('unknown category', category) + msg = f"unknown category: {category}" + raise ValueError(msg) entries[category].append((p, e)) except Exception as e: e.add_note(f"in {p}") @@ -156,7 +176,7 @@ def run_on_dir(author_info: AuthorInfoDB, d): for category in CATEGORIES: if entries[category]: - print('##', category) + print("##", category) # Blank line after each heading. print() do_category(author_info, entries[category]) @@ -165,22 +185,27 @@ def run_on_dir(author_info: AuthorInfoDB, d): # after each entry. print() -def main(): - import argparse +def main(): ap = argparse.ArgumentParser() - ap.add_argument('--change-authors', help='File name of the change authors metadata YAML file', type=argparse.FileType('r')) - ap.add_argument('dirs', help='Directories to run on', nargs='+') + ap.add_argument( + "--change-authors", + help="File name of the change authors metadata YAML file", + type=argparse.FileType("r"), + ) + ap.add_argument("dirs", help="Directories to run on", nargs="+") args = ap.parse_args() - author_info = AuthorInfoDB(yaml.safe_load(args.change_authors), throw_on_missing=True) \ - if args.change_authors \ + author_info = ( + AuthorInfoDB(yaml.safe_load(args.change_authors), throw_on_missing=True) + if args.change_authors else AuthorInfoDB({}, throw_on_missing=False) + ) for d in args.dirs: run_on_dir(author_info, d) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/maintainers/issue_import.py b/maintainers/issue_import.py deleted file mode 100644 index ea226b7b8..000000000 --- a/maintainers/issue_import.py +++ /dev/null @@ -1,148 +0,0 @@ -import requests -import textwrap -import dataclasses -import logging -import re -import os - -API_BASE = 'https://git.lix.systems/api/v1' -API_KEY = os.environ['FORGEJO_API_KEY'] - -log = logging.getLogger(__name__) -log.setLevel(logging.INFO) - -fmt = logging.Formatter('{asctime} {levelname} {name}: {message}', - datefmt='%b %d %H:%M:%S', - style='{') - -if not any(isinstance(h, logging.StreamHandler) for h in log.handlers): - hand = logging.StreamHandler() - hand.setFormatter(fmt) - log.addHandler(hand) - -# These are erring in the direction of re-triage, rather than necessarily -# mapping all metadata of the issue -LABEL_MAPPING = { - 'lix-import': 153, # 'imported', - 'contributor-experience': 148, # 'devx', - 'bug': 150, # 'bug', - 'UX': 149, # 'ux', - 'error-messages': 149, # 'ux', - 'lix-stability': 146, # 'stability', - 'performance': 147, # 'performance', - 'tests': 121, # 'tests', -} - -def api(method, endpoint: str, resp_json=True, **kwargs): - log.info('http %s %s', method, endpoint) - if not endpoint.startswith('https'): - endpoint = API_BASE + endpoint - resp = requests.request(method, - endpoint, - headers={'Authorization': f'Bearer {API_KEY}'}, - **kwargs) - resp.raise_for_status() - if resp_json: - return resp.json() - else: - return resp - -def paginate(method: str, url: str): - while True: - resp = api(method, url, resp_json=False) - yield from resp.json() - next_one = resp.links.get('next') - if not next_one: - return - url = next_one.get('url') - if not url: - return - -class DataClassUnpack: - """Taken from: https://stackoverflow.com/a/72164665""" - classFieldCache = {} - - @classmethod - def instantiate(cls, classToInstantiate, argDict): - if classToInstantiate not in cls.classFieldCache: - cls.classFieldCache[classToInstantiate] = { - f.name - for f in getattr(classToInstantiate, dataclasses._FIELDS).values() if f._field_type is not dataclasses._FIELD_CLASSVAR # type: ignore - } - - fieldSet = cls.classFieldCache[classToInstantiate] - filteredArgDict = {k: v for k, v in argDict.items() if k in fieldSet} - return classToInstantiate(**filteredArgDict) - -@dataclasses.dataclass -class Label: - name: str - description: str - -@dataclasses.dataclass -class Issue: - number: int - url: str - html_url: str - title: str - body: str - labels: dataclasses.InitVar[list[dict]] - labels_clean: list[Label] = dataclasses.field(init=False) - - def __post_init__(self, labels): - self.labels_clean = [DataClassUnpack.instantiate(Label, l) for l in labels] - -def issues_to_import(): - yield from paginate('GET', '/repos/nixos/nix/issues?state=open&labels=lix-import') - -def issues_already_imported(): - yield from paginate('GET', '/repos/lix-project/lix/issues?state=all&labels=imported') - - -UPSTREAM_ISSUE_RE = re.compile(r'^Upstream-Issue: https://git\.lix\.systems/NixOS/nix/issues/(\d+)$', re.MULTILINE) - -def make_already_imported(): - d = {} - for issue in issues_already_imported(): - iss = DataClassUnpack.instantiate(Issue, issue) - print(iss) - match = UPSTREAM_ISSUE_RE.search(iss.body) - if match: - d[int(match.group(1))] = iss - - return d - -def new_issue(title, body, labels): - api('POST', '/repos/lix-project/lix/issues', resp_json=True, json={ - 'labels': labels, - 'body': body, - 'title': title, - 'dont_notify': True, - }) - -already_imported = make_already_imported() - -def import_issue(iss: Issue): - if iss.number in already_imported: - log.info('Skipping already imported %d', iss.number) - return - new_body = textwrap.dedent(''' - Upstream-Issue: {iss} - - {original_body} - ''').format(iss=iss.html_url, original_body=iss.body) - - new_labels = [LABEL_MAPPING[l.name] for l in iss.labels_clean if l.name in LABEL_MAPPING] - - new_title = '[Nix#{num}] {title}'.format(num=iss.number, title=iss.title) - - log.info('%s', f'create issue with: {new_labels} {new_title} {new_body}') - new_issue(new_title, new_body, new_labels) - -def go(): - log.info('Importing issues!') - for issue in issues_to_import(): - import_issue(DataClassUnpack.instantiate(Issue, issue)) - -if __name__ == '__main__': - go() diff --git a/treefmt.toml b/treefmt.toml index 8bbd7a5c8..aa39993f2 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -12,5 +12,5 @@ priority = 0 [formatter.ruff] command = "ruff" options = ["check"] -includes = ["tests/functional2/**/*.py", "meson/clang-tidy/*.py"] +includes = ["tests/functional2/**/*.py", "meson/clang-tidy/*.py", "maintainers/*.py"] priority = 1