proj/pkgcore/pkgdev:main commit in: /, src/pkgdev/scripts/, tests/scripts/
"Arthur Zamarin" <[email protected]>
| Newsgroups | gmane.linux.gentoo.cvs |
|---|---|
| Message-ID | <1786216337.8b06640499d03ffcf3218014145815eb80e601e9.arthurzam@gentoo> |
commit: 8b06640499d03ffcf3218014145815eb80e601e9
Author: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
AuthorDate: Sat Aug 8 11:55:10 2026 +0000
Commit: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Sat Aug 8 19:12:17 2026 +0000
URL: https://gitweb.gentoo.org/proj/pkgcore/pkgdev.git/commit/?id=8b066404
bugs, mask: move Bugzilla handling to pkgcore.bugzilla
pkgcore now ships a typed Bugzilla REST client, so the hand-rolled urllib
calls, JSON payload dicts and API key discovery here can go. Net effect is
317 lines removed against 127 added.
BugzillaApiKey moves to pkgcore.bugzilla.apikey.
Tests drop the local BugsSession fake for the bugzilla_cassette fixture
pkgcore ships, which replays through the real urllib stack, so the request
construction is exercised rather than bypassed.
Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>
pyproject.toml | 2 +-
src/pkgdev/scripts/argparsers.py | 47 +-------
src/pkgdev/scripts/pkgdev_bugs.py | 218 +++++++++++---------------------------
src/pkgdev/scripts/pkgdev_mask.py | 86 +++++----------
src/pkgdev/scripts/pkgdev_tatt.py | 13 ++-
tests/scripts/test_pkgdev_bugs.py | 94 +++++++---------
6 files changed, 139 insertions(+), 321 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 7de37fe..a204d71 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,7 @@ dynamic = ["version"]
dependencies = [
"snakeoil~=0.11.1",
- "pkgcore~=0.12.37",
+ "pkgcore~=0.12.38",
"pkgcheck~=0.10.42",
]
diff --git a/src/pkgdev/scripts/argparsers.py b/src/pkgdev/scripts/argparsers.py
index 9ab58da..08cfef1 100644
--- a/src/pkgdev/scripts/argparsers.py
+++ b/src/pkgdev/scripts/argparsers.py
@@ -1,8 +1,5 @@
import os
import subprocess
-from configparser import ConfigParser
-from contextlib import suppress
-from pathlib import Path
from pkgcore.repository import errors as repo_errors
from snakeoil.cli.arghparse import ArgumentParser
@@ -18,7 +15,7 @@ def _determine_cwd_repo(parser, namespace):
namespace.cwd = os.getcwd()
try:
repo = namespace.domain.find_repo(namespace.cwd, config=namespace.config, configure=False)
- except (repo_errors.InitializationError, IOError) as e:
+ except (OSError, repo_errors.InitializationError) as e:
raise parser.error(str(e))
if repo is None:
@@ -44,45 +41,3 @@ def _determine_git_repo(parser, namespace):
pass
namespace.git_repo = path
-
-
-class BugzillaApiKey:
- @classmethod
- def mangle_argparser(cls, parser):
- parser.add_argument(
- "--api-key",
- metavar="TOKEN",
- help="Bugzilla API key",
- docs="""
- The Bugzilla API key to use for authentication. WARNING: using this
- option will expose your API key to other users of the same system.
- Consider instead saving your API key in a file named ``~/.bugzrc``
- in an INI format like so::
-
- [default]
- key = <your API key>
-
- Another supported option is to save your API key in a file named
- ``~/.bugz_token``.
- """,
- )
-
- parser.bind_delayed_default(1000, "api_key")(cls._default_api_key)
-
- @staticmethod
- def _default_api_key(namespace, attr):
- """Use all known arches by default."""
- if (bugz_rc_file := Path.home() / ".bugzrc").is_file():
- try:
- config = ConfigParser(default_section="default")
- config.read(bugz_rc_file)
- except Exception as e:
- raise ValueError(f"failed parsing {bugz_rc_file}: {e}")
-
- for category in ("default", "gentoo", "Gentoo"):
- with suppress(Exception):
- setattr(namespace, attr, config.get(category, "key"))
- return
-
- if (bugz_token_file := Path.home() / ".bugz_token").is_file():
- setattr(namespace, attr, bugz_token_file.read_text().strip())
diff --git a/src/pkgdev/scripts/pkgdev_bugs.py b/src/pkgdev/scripts/pkgdev_bugs.py
index a3ef0e6..0cfd635 100644
--- a/src/pkgdev/scripts/pkgdev_bugs.py
+++ b/src/pkgdev/scripts/pkgdev_bugs.py
@@ -1,8 +1,6 @@
"""Automatic bugs filer"""
import contextlib
-import enum
-import json
import os
import shlex
import subprocess
@@ -15,7 +13,6 @@ from datetime import datetime
from functools import partial
from itertools import chain
from os.path import join as pjoin
-from urllib.parse import urlencode
from pkgcheck import const as pkgcheck_const
from pkgcheck.addons import ArchesAddon, init_addon
@@ -23,6 +20,9 @@ from pkgcheck.addons.git import GitAddedRepo, GitAddon, GitModifiedRepo
from pkgcheck.addons.profiles import ProfileAddon
from pkgcheck.checks import stablereq, visibility
from pkgcheck.scripts import argparse_actions
+from pkgcore.bugzilla import BugCategory, BugQuery, BugUpdate, Bugzilla, NewBug, PackageList
+from pkgcore.bugzilla.apikey import BugzillaApiKey
+from pkgcore.bugzilla.changes import summarise
from pkgcore.ebuild.atom import atom
from pkgcore.ebuild.ebuild_src import package
from pkgcore.ebuild.errors import MalformedAtom
@@ -37,30 +37,11 @@ from snakeoil.cli.input import userquery
from snakeoil.data_source import bytes_data_source
from snakeoil.formatters import Formatter
+from .. import __version__
from ..cli import ArgumentParser
-from .argparsers import BugzillaApiKey, _determine_cwd_repo, cwd_repo_argparser
+from .argparsers import _determine_cwd_repo, cwd_repo_argparser
-
-class NodeCategory(enum.Enum):
- KEYWORDREQ = enum.auto()
- STABLEREQ = enum.auto()
-
-
-# per-category strings: Bugzilla component, description verb, summary suffix
-_CATEGORY_META = {
- NodeCategory.STABLEREQ: {
- "component": "Stabilization",
- "verb": "stabilize",
- "suffix": "stablereq",
- },
- NodeCategory.KEYWORDREQ: {
- "component": "Keywording",
- "verb": "keyword",
- "suffix": "keywordreq",
- },
-}
-
-_CATEGORY_BY_SUFFIX = {meta["suffix"]: category for category, meta in _CATEGORY_META.items()}
+_CATEGORY_BY_SUFFIX = {x.summary_suffix: x for x in BugCategory}
class StoreTargetArches(commandline.StoreTarget):
@@ -284,7 +265,8 @@ def _validate_args(namespace, attr):
def _validate_args(parser, namespace):
if namespace.keywording and namespace.filter_stablereqs:
parser.error("--keywording is incompatible with --filter-stablereqs")
- namespace.category = NodeCategory.KEYWORDREQ if namespace.keywording else NodeCategory.STABLEREQ
+ namespace.category = BugCategory.KEYWORDREQ if namespace.keywording else BugCategory.STABLEREQ
+ namespace.bugzilla = Bugzilla(namespace.api_key, user_agent=f"pkgdev-bugs/{__version__}")
def _get_suggested_keywords(repo, pkg: package, streq: bool = True):
@@ -324,7 +306,7 @@ class GraphNode:
def __init__(
self,
pkgs: tuple[tuple[package, set[str]], ...],
- category: NodeCategory = NodeCategory.STABLEREQ,
+ category: BugCategory = BugCategory.STABLEREQ,
bugno=None,
):
self.pkgs = pkgs
@@ -337,7 +319,7 @@ class GraphNode:
@property
def is_keywordreq(self):
- return self.category is NodeCategory.KEYWORDREQ
+ return self.category is BugCategory.KEYWORDREQ
def __eq__(self, __o: object):
return self is __o
@@ -383,19 +365,13 @@ class GraphNode:
keywords.clear()
keywords.add("*")
+ @property
+ def package_list(self) -> PackageList:
+ return PackageList("\n".join(self.lines()))
+
@property
def bug_summary(self):
- if self.summary:
- return self.summary
- suffix = _CATEGORY_META[self.category]["suffix"]
- if self.is_keywordreq:
- names = [str(pkg.unversioned_atom) for pkg, _ in self.pkgs]
- else:
- names = [pkg.versioned_atom.cpvstr for pkg, _ in self.pkgs]
- summary = f"{', '.join(names)}: {suffix}"
- if len(summary) > 90 and len(self.pkgs) > 1:
- return f"{names[0]} and friends: {suffix}"
- return summary
+ return self.summary or summarise(self.package_list, self.category)
@property
def node_maintainers(self):
@@ -413,7 +389,7 @@ class GraphNode:
def file_bug(
self,
- api_key: str,
+ bugzilla: Bugzilla,
auto_cc_arches: frozenset[str],
block_bugs: list[int],
modified_repo: multiplex.tree,
@@ -423,15 +399,9 @@ class GraphNode:
return self.bugno
for dep in self.edges:
if dep.bugno is None:
- dep.file_bug(api_key, auto_cc_arches, (), modified_repo, observer)
- maintainers = self.node_maintainers
- if self.should_cc_arches(auto_cc_arches):
- keywords = ["CC-ARCHES"]
- else:
- keywords = []
- maintainers = tuple(maintainers) or ("[email protected]",)
+ dep.file_bug(bugzilla, auto_cc_arches, (), modified_repo, observer)
- description = [f"Please {_CATEGORY_META[self.category]['verb']}", ""]
+ description = [f"Please {self.category.verb}", ""]
if modified_repo is not None:
for pkg, _ in self.pkgs:
with contextlib.suppress(StopIteration):
@@ -442,63 +412,28 @@ class GraphNode:
f" {pkg.versioned_atom.cpvstr}: no change for {days_old} days, since {modified:%Y-%m-%d}"
)
- request_data = dict(
- Bugzilla_api_key=api_key,
- product="Gentoo Linux",
- component=_CATEGORY_META[self.category]["component"],
- severity="enhancement",
- version="unspecified",
- summary=self.bug_summary,
- description="\n".join(description).strip(),
- keywords=keywords,
- cf_stabilisation_atoms="\n".join(self.lines()),
- assigned_to=maintainers[0],
- cc=maintainers[1:],
- depends_on=list({dep.bugno for dep in self.edges}),
- blocks=block_bugs,
- )
- request = urllib.Request(
- url="https://bugs.gentoo.org/rest/bug",
- data=json.dumps(request_data).encode("utf-8"),
- method="POST",
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
+ self.bugno = bugzilla.create(
+ NewBug.arch_request(
+ self.category,
+ self.package_list,
+ maintainers=tuple(self.node_maintainers),
+ cc_arches=self.should_cc_arches(auto_cc_arches),
+ summary=self.bug_summary,
+ description="\n".join(description).strip(),
+ depends_on=tuple({dep.bugno for dep in self.edges}),
+ blocks=tuple(block_bugs),
+ )
)
- with urllib.urlopen(request, timeout=30) as response:
- reply = json.loads(response.read().decode("utf-8"))
- self.bugno = int(reply["id"])
if observer is not None:
observer(self)
- self.obsolete_bugs(api_key)
+ self.obsolete_bugs(bugzilla)
return self.bugno
- def obsolete_bugs(self, api_key: str):
+ def obsolete_bugs(self, bugzilla: Bugzilla):
if not self.obsoletes:
return
assert self.bugno is not None
-
- # Batch all bug IDs into a single PUT request
- request_data = dict(
- Bugzilla_api_key=api_key,
- status="RESOLVED",
- resolution="OBSOLETE",
- see_also={"add": [f"https://bugs.gentoo.org/{self.bugno}"]},
- )
- if len(self.obsoletes) > 1:
- request_data["ids"] = list(self.obsoletes)
- request = urllib.Request(
- url=f"https://bugs.gentoo.org/rest/bug/{','.join(map(str, self.obsoletes))}",
- data=json.dumps(request_data).encode("utf-8"),
- method="PUT",
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
- )
- with urllib.urlopen(request, timeout=30) as response:
- json.loads(response.read().decode("utf-8"))
+ bugzilla.update(sorted(self.obsoletes), BugUpdate.obsoleted_by(self.bugno))
class DependencyGraph:
@@ -682,16 +617,16 @@ class DependencyGraph:
)
def build_full_graph(self):
- STABLEREQ, KEYWORDREQ = NodeCategory.STABLEREQ, NodeCategory.KEYWORDREQ
+ STABLEREQ, KEYWORDREQ = BugCategory.STABLEREQ, BugCategory.KEYWORDREQ
check_nodes = [
(pkg, set(self.target_arches.get(pkg, ())), self.options.category, "")
for pkg in self.targets
]
- vertices: dict[tuple[package, NodeCategory], GraphNode] = {}
+ vertices: dict[tuple[package, BugCategory], GraphNode] = {}
edges = []
- def explore_deps(pkg: package, arches: set[str], category: NodeCategory):
+ def explore_deps(pkg: package, arches: set[str], category: BugCategory):
"""Queue the dependencies of ``pkg`` that are unsolvable on ``arches``."""
for dep, dep_arches in self._find_dependencies(
pkg, arches, stable=category is STABLEREQ
@@ -729,7 +664,7 @@ class DependencyGraph:
continue
streq = category is STABLEREQ
- verb = _CATEGORY_META[category]["verb"]
+ verb = category.verb
if streq:
keywords.update(_get_suggested_keywords(self.options.repo, pkg, streq=True))
if not keywords:
@@ -795,7 +730,7 @@ class DependencyGraph:
continue # already filed
toml.write(f"[bug-{bugno}]\n")
toml.write(f'summary = "{node.bug_summary}"\n')
- toml.write(f'category = "{_CATEGORY_META[node.category]["suffix"]}"\n')
+ toml.write(f'category = "{node.category.summary_suffix}"\n')
toml.write(f"cc_arches = {str(node.should_cc_arches(self.auto_cc_arches)).lower()}\n")
if node in self.starting_nodes:
toml.write("starting = true\n")
@@ -846,7 +781,7 @@ class DependencyGraph:
if pkg.startswith("=")
)
category = _CATEGORY_BY_SUFFIX.get(
- data_node.get("category", "stablereq"), NodeCategory.STABLEREQ
+ data_node.get("category", "stablereq"), BugCategory.STABLEREQ
)
new_bugs[node_name] = GraphNode(pkgs, category=category)
for node_name, data_node in data.items():
@@ -965,7 +900,7 @@ class DependencyGraph:
node
for node in self.nodes
if node.bugno is None
- and node.category is NodeCategory.STABLEREQ
+ and node.category is BugCategory.STABLEREQ
and any(restrict.match(pkg) for pkg, _ in node.pkgs)
)
if mergable:
@@ -984,66 +919,35 @@ class DependencyGraph:
self.merge_nodes(mergable)
return True
- def scan_existing_bugs(self, api_key: str) -> bool:
- # Paginate the search request with batches of 100 items to avoid HTTP 414 errors
+ def scan_existing_bugs(self, bugzilla: Bugzilla) -> bool:
all_packages = list({pkg[0].unversioned_atom for node in self.nodes for pkg in node.pkgs})
- batch_size = 100
- all_bugs = []
has_output = False
- for i in range(0, len(all_packages), batch_size):
- params = urlencode(
- {
- "Bugzilla_api_key": api_key,
- "include_fields": "id,cf_stabilisation_atoms,summary,component",
- "component": ["Stabilization", "Keywording"],
- "resolution": "---",
- "f1": "cf_stabilisation_atoms",
- "o1": "anywords",
- "v1": all_packages[i : i + batch_size],
- },
- doseq=True,
- )
- request = urllib.Request(
- url="https://bugs.gentoo.org/rest/bug?" + params,
- method="GET",
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
- )
- with urllib.urlopen(request, timeout=30) as response:
- reply = json.loads(response.read().decode("utf-8"))
- all_bugs.extend(reply.get("bugs", []))
+ query = (
+ BugQuery.component(BugCategory.KEYWORDREQ, BugCategory.STABLEREQ)
+ & BugQuery.unresolved()
+ & BugQuery.package_list_any(all_packages)
+ )
+ all_bugs = bugzilla.search(query).values()
for bug in all_bugs:
- bug_atoms = (
- parse_atom(line.split(" ", 1)[0]).unversioned_atom
- for line in map(str.strip, bug["cf_stabilisation_atoms"].splitlines())
- if line
- )
- bug_match = boolean.OrRestriction(*bug_atoms)
- exact_match = boolean.OrRestriction(
- *(
- parse_atom(line.split(" ", 1)[0])
- for line in map(str.strip, bug["cf_stabilisation_atoms"].splitlines())
- if line
- )
- )
+ bug_atoms = bug.package_list.atoms
+ bug_match = boolean.OrRestriction(*(a.unversioned_atom for a in bug_atoms))
+ exact_match = boolean.OrRestriction(*bug_atoms)
for node in self.nodes:
- if bug.get("component") != _CATEGORY_META[node.category]["component"]:
+ if bug.component != node.category.component:
continue
if node.bugno is None and all(bug_match.match(pkg[0]) for pkg in node.pkgs):
is_exact_match = all(exact_match.match(pkg[0]) for pkg in node.pkgs)
self.out.write(
self.out.fg("yellow"),
- f"Found https://bugs.gentoo.org/{bug['id']} for node {node}",
+ f"Found {bug.url} for node {node}",
self.out.reset,
" (exact version match)" if is_exact_match else " (atom match)",
)
- self.out.write(" -> bug summary: ", bug["summary"])
+ self.out.write(" -> bug summary: ", bug.summary)
if is_exact_match:
- node.bugno = bug["id"]
+ node.bugno = bug.id
else:
if userquery(
"Not an exact match. Do you want to obsolete?",
@@ -1051,13 +955,13 @@ class DependencyGraph:
self.err,
default_answer=False,
):
- node.obsoletes.add(bug["id"])
+ node.obsoletes.add(bug.id)
else:
- node.bugno = bug["id"]
+ node.bugno = bug.id
has_output = True
return has_output
- def file_bugs(self, api_key: str, auto_cc_arches: frozenset[str], block_bugs: list[int]):
+ def file_bugs(self, bugzilla: Bugzilla, auto_cc_arches: frozenset[str], block_bugs: list[int]):
def observe(node: GraphNode):
self.out.write(
f"https://bugs.gentoo.org/{node.bugno} ",
@@ -1068,7 +972,7 @@ class DependencyGraph:
self.out.flush()
for node in self.starting_nodes:
- node.file_bug(api_key, auto_cc_arches, block_bugs, self.modified_repo, observe)
+ node.file_bug(bugzilla, auto_cc_arches, block_bugs, self.modified_repo, observe)
def _load_from_stdin(out: Formatter):
@@ -1106,7 +1010,7 @@ def main(options, out: Formatter, err: Formatter):
has_output = False
if userquery("Check for open bugs matching current graph?", out, err, default_answer=False):
- if d.scan_existing_bugs(options.api_key):
+ if d.scan_existing_bugs(options.bugzilla):
out.flush()
has_output = True
@@ -1163,8 +1067,8 @@ def main(options, out: Formatter, err: Formatter):
out.write(out.fg("red"), "Nothing to do, exiting", out.reset)
return 1
counts = {
- meta["suffix"]: sum(node.category is category for node in pending)
- for category, meta in _CATEGORY_META.items()
+ category.summary_suffix: sum(node.category is category for node in pending)
+ for category in BugCategory
}
summary = ", ".join(f"{count} {suffix}" for suffix, count in counts.items() if count)
@@ -1175,4 +1079,4 @@ def main(options, out: Formatter, err: Formatter):
disabled, enabled = options.auto_cc_arches
blocks = list(frozenset(map(int, options.blocks)))
- d.file_bugs(options.api_key, frozenset(enabled).difference(disabled), blocks)
+ d.file_bugs(options.bugzilla, frozenset(enabled).difference(disabled), blocks)
diff --git a/src/pkgdev/scripts/pkgdev_mask.py b/src/pkgdev/scripts/pkgdev_mask.py
index 4d70e59..21f00fa 100644
--- a/src/pkgdev/scripts/pkgdev_mask.py
+++ b/src/pkgdev/scripts/pkgdev_mask.py
@@ -1,19 +1,18 @@
-import json
import os
import re
import shlex
import subprocess
import tempfile
import textwrap
-import urllib.request as urllib
from collections import deque
from dataclasses import dataclass
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from itertools import groupby
from operator import itemgetter
from os.path import join as pjoin
-from typing import List
+from pkgcore.bugzilla import BugUpdate, Bugzilla, BugzillaError, ListChange, NewBug
+from pkgcore.bugzilla.apikey import BugzillaApiKey
from pkgcore.ebuild.atom import MalformedAtom
from pkgcore.ebuild.atom import atom as atom_cls
from pkgcore.ebuild.profiles import ProfileNode
@@ -21,8 +20,8 @@ from snakeoil.bash import read_bash
from snakeoil.cli import arghparse
from snakeoil.strings import pluralism
-from .. import git
-from .argparsers import BugzillaApiKey, cwd_repo_argparser, git_repo_argparser
+from .. import __version__, git
+from .argparsers import cwd_repo_argparser, git_repo_argparser
mask = arghparse.ArgumentParser(
prog="pkgdev mask",
@@ -146,6 +145,7 @@ def _mask_validate(parser, namespace):
namespace.atoms = sorted(atoms)
namespace.maintainers = sorted(maintainers) or ["[email protected]"]
+ namespace.bugzilla = Bugzilla(namespace.api_key, user_agent=f"pkgdev-mask/{__version__}")
@dataclass(frozen=True)
@@ -155,8 +155,8 @@ class Mask:
author: str
email: str
date: str
- comment: List[str]
- atoms: List[atom_cls]
+ comment: list[str]
+ atoms: list[atom_cls]
_removal_re = re.compile(r"^Removal: (?P<date>\d{4}-\d{2}-\d{2})")
@@ -274,11 +274,11 @@ def get_comment():
with open(tmp.name) as f:
# strip trailing whitespace from lines
- comment = (x.rstrip() for x in f.readlines())
- # strip comments
- comment = (x for x in comment if not x.startswith("#"))
- # strip leading/trailing newlines
- comment = "\n".join(comment).strip().splitlines()
+ comment = (x.rstrip() for x in f)
+ # strip comments
+ comment = (x for x in comment if not x.startswith("#"))
+ # strip leading/trailing newlines
+ comment = "\n".join(comment).strip().splitlines()
if not comment:
mask.error("empty mask comment")
return comment
@@ -287,7 +287,7 @@ def get_comment():
def message_removal_notice(bugs: list[int], rites: int):
summary = []
if rites:
- summary.append(f"Removal on {datetime.now(timezone.utc) + timedelta(days=rites):%Y-%m-%d}.")
+ summary.append(f"Removal on {datetime.now(UTC) + timedelta(days=rites):%Y-%m-%d}.")
if bugs:
# Bug(s) #A, #B, #C
bug_list = ", ".join(f"#{b}" for b in bugs)
@@ -300,52 +300,24 @@ def file_last_rites_bug(options, message: str) -> int:
summary = f"{', '.join(map(str, options.atoms))}: removal"
if len(summary) > 90 and len(options.atoms) > 1:
summary = f"{options.atoms[0]} and friends: removal"
- request_data = dict(
- Bugzilla_api_key=options.api_key,
- product="Gentoo Linux",
- component="Current packages",
- version="unspecified",
- summary=summary,
- description="\n".join([*message, "", "package list:", *map(str, options.atoms)]).strip(),
- keywords=["PMASKED"],
- assigned_to=options.maintainers[0],
- cc=options.maintainers[1:] + ["[email protected]"],
- deadline=(datetime.now(timezone.utc) + timedelta(days=options.rites)).strftime("%Y-%m-%d"),
- blocks=list(options.bugs),
+ bug = NewBug.package_mask(
+ summary,
+ "\n".join([*message, "", "package list:", *map(str, options.atoms)]).strip(),
+ rites=options.rites,
+ maintainers=options.maintainers,
+ blocks=tuple(options.bugs),
)
- request = urllib.Request(
- url="https://bugs.gentoo.org/rest/bug",
- data=json.dumps(request_data).encode("utf-8"),
- method="POST",
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
- )
- with urllib.urlopen(request, timeout=30) as response:
- reply = json.loads(response.read().decode("utf-8"))
- return int(reply["id"])
+ return options.bugzilla.create(bug)
-def update_bugs_pmasked(api_key: str, bugs: list[int]):
+def update_bugs_pmasked(bugzilla, bugs: list[int]) -> bool:
if not bugs:
return True
- request_data = dict(
- Bugzilla_api_key=api_key,
- ids=bugs,
- keywords=dict(add=["PMASKED"]),
- )
- request = urllib.Request(
- url=f"https://bugs.gentoo.org/rest/bug/{bugs[0]}",
- data=json.dumps(request_data).encode("utf-8"),
- method="PUT",
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json",
- },
- )
- with urllib.urlopen(request, timeout=30) as response:
- return response.status == 200
+ try:
+ bugzilla.update(bugs, BugUpdate(keywords=ListChange.adding("PMASKED")))
+ except BugzillaError:
+ return False
+ return True
def send_last_rites_email(m: Mask, subject_prefix: str):
@@ -372,7 +344,7 @@ def send_last_rites_email(m: Mask, subject_prefix: str):
@mask.bind_main_func
def _mask(options, out, err):
mask_file = MaskFile(pjoin(options.repo.location, "profiles/package.mask"))
- today = datetime.now(timezone.utc)
+ today = datetime.now(UTC)
# pull name/email from git config
p = git.run("config", "user.name", stdout=subprocess.PIPE)
@@ -385,7 +357,7 @@ def _mask(options, out, err):
if bug_no := file_last_rites_bug(options, message):
out.write(out.fg("green"), f"filed bug https://bugs.gentoo.org/{bug_no}", out.reset)
out.flush()
- if not update_bugs_pmasked(options.api_key, options.bugs):
+ if not update_bugs_pmasked(options.bugzilla, options.bugs):
err.write(err.fg("red"), "failed to update referenced bugs", err.reset)
err.flush()
options.bugs.insert(0, bug_no)
diff --git a/src/pkgdev/scripts/pkgdev_tatt.py b/src/pkgdev/scripts/pkgdev_tatt.py
index 5f45ae5..58a7649 100644
--- a/src/pkgdev/scripts/pkgdev_tatt.py
+++ b/src/pkgdev/scripts/pkgdev_tatt.py
@@ -229,11 +229,18 @@ def _validate_args(parser, namespace):
def _get_bugzilla_packages(namespace):
- from nattka.bugzilla import BugCategory, NattkaBugzilla
from nattka.package import match_package_list
- nattka_bugzilla = NattkaBugzilla(api_key=namespace.api_key)
- bug = next(iter(nattka_bugzilla.find_bugs(bugs=[namespace.bug]).values()))
+ try:
+ from nattka.bugzilla import BugCategory, NattkaBugzilla
+
+ nattka_bugzilla = NattkaBugzilla(api_key=namespace.api_key)
+ bug = next(iter(nattka_bugzilla.find_bugs(bugs=[namespace.bug]).values()))
+ except ImportError:
+ from pkgcore.bugzilla import BugCategory, Bugzilla
+
+ bug = Bugzilla(api_key=namespace.api_key).get(namespace.bug)
+
namespace.keywording = bug.category == BugCategory.KEYWORDREQ
repo = namespace.domain.repos["gentoo"].raw_repo
src_repo = namespace.domain.source_repos_raw
diff --git a/tests/scripts/test_pkgdev_bugs.py b/tests/scripts/test_pkgdev_bugs.py
index 49ac736..85e81aa 100644
--- a/tests/scripts/test_pkgdev_bugs.py
+++ b/tests/scripts/test_pkgdev_bugs.py
@@ -1,12 +1,10 @@
-import itertools
-import json
import os
import textwrap
from os.path import join as pjoin
from types import SimpleNamespace
-from unittest.mock import patch
import pytest
+from pkgcore.bugzilla import BugCategory
from pkgcore.ebuild.atom import atom
from pkgdev.scripts import pkgdev_bugs as bugs
@@ -41,79 +39,61 @@ def mk_repo(repo):
mk_pkg(repo, "cat/w-0", ["dev3"], RDEPEND="cat/x")
-class BugsSession:
- def __init__(self):
- self.counter = iter(itertools.count(1))
- self.calls = []
-
- def __enter__(self):
- return self
-
- def __exit__(self, *_args): ...
-
- def read(self):
- return json.dumps({"id": next(self.counter)}).encode("utf-8")
-
- def __call__(self, request, *_args, **_kwargs):
- self.calls.append(json.loads(request.data))
- return self
-
-
class TestBugFiling:
- def test_bug_filing(self, repo):
+ def test_bug_filing(self, repo, bugzilla_cassette):
mk_repo(repo)
- session = BugsSession()
+ bugzilla_cassette.creates_bugs()
pkg = max(repo.itermatch(atom("=cat/u-0")))
- with patch("pkgdev.scripts.pkgdev_bugs.urllib.urlopen", session):
- bugs.GraphNode(((pkg, {"*"}),)).file_bug("API", frozenset(), (), None)
- assert len(session.calls) == 1
- call = session.calls[0]
+ bugs.GraphNode(((pkg, {"*"}),)).file_bug(
+ bugzilla_cassette.client(api_key="API"), frozenset(), (), None
+ )
+ assert len(bugzilla_cassette.calls) == 1
+ call = bugzilla_cassette.calls[0].body
assert call["Bugzilla_api_key"] == "API"
assert call["summary"] == "cat/u-0: stablereq"
assert call["assigned_to"] == "[email protected]"
- assert not call["cc"]
+ assert "cc" not in call
assert call["cf_stabilisation_atoms"] == "=cat/u-0 *"
- assert not call["depends_on"]
+ assert "depends_on" not in call
- def test_bug_filing_maintainer_needed(self, repo):
+ def test_bug_filing_maintainer_needed(self, repo, bugzilla_cassette):
mk_repo(repo)
- session = BugsSession()
+ bugzilla_cassette.creates_bugs()
pkg = max(repo.itermatch(atom("=cat/z-0")))
- with patch("pkgdev.scripts.pkgdev_bugs.urllib.urlopen", session):
- bugs.GraphNode(((pkg, {"*"}),)).file_bug("API", frozenset(), (), None)
- assert len(session.calls) == 1
- call = session.calls[0]
+ bugs.GraphNode(((pkg, {"*"}),)).file_bug(
+ bugzilla_cassette.client(api_key="API"), frozenset(), (), None
+ )
+ assert len(bugzilla_cassette.calls) == 1
+ call = bugzilla_cassette.calls[0].body
assert call["assigned_to"] == "[email protected]"
- assert not call["cc"]
+ assert "cc" not in call
- def test_bug_filing_multiple_pkgs(self, repo):
+ def test_bug_filing_multiple_pkgs(self, repo, bugzilla_cassette):
mk_repo(repo)
- session = BugsSession()
+ bugzilla_cassette.creates_bugs()
pkgX = max(repo.itermatch(atom("=cat/x-0")))
pkgY = max(repo.itermatch(atom("=cat/y-0")))
pkgZ = max(repo.itermatch(atom("=cat/z-0")))
dep = bugs.GraphNode((), bugno=2)
node = bugs.GraphNode(((pkgX, {"*"}), (pkgY, {"*"}), (pkgZ, {"*"})))
node.edges.add(dep)
- with patch("pkgdev.scripts.pkgdev_bugs.urllib.urlopen", session):
- node.file_bug("API", frozenset(), (), None)
- assert len(session.calls) == 1
- call = session.calls[0]
+ node.file_bug(bugzilla_cassette.client(api_key="API"), frozenset(), (), None)
+ assert len(bugzilla_cassette.calls) == 1
+ call = bugzilla_cassette.calls[0].body
assert call["summary"] == "cat/x-0, cat/y-0, cat/z-0: stablereq"
assert call["assigned_to"] == "[email protected]"
assert call["cc"] == ["[email protected]"]
assert call["cf_stabilisation_atoms"] == "=cat/x-0 *\n=cat/y-0 *\n=cat/z-0 *"
assert call["depends_on"] == [2]
- def test_keyword_bug_filing(self, repo):
+ def test_keyword_bug_filing(self, repo, bugzilla_cassette):
mk_repo(repo)
- session = BugsSession()
+ bugzilla_cassette.creates_bugs()
pkg = max(repo.itermatch(atom("=cat/u-0")))
- node = bugs.GraphNode(((pkg, {"amd64"}),), category=bugs.NodeCategory.KEYWORDREQ)
- with patch("pkgdev.scripts.pkgdev_bugs.urllib.urlopen", session):
- node.file_bug("API", frozenset(), (), None)
- assert len(session.calls) == 1
- call = session.calls[0]
+ node = bugs.GraphNode(((pkg, {"amd64"}),), category=BugCategory.KEYWORDREQ)
+ node.file_bug(bugzilla_cassette.client(api_key="API"), frozenset(), (), None)
+ assert len(bugzilla_cassette.calls) == 1
+ call = bugzilla_cassette.calls[0].body
# keywordreq bugs are version-less and request ~arch keywords
assert call["component"] == "Keywording"
assert call["summary"] == "cat/u: keywordreq"
@@ -138,7 +118,7 @@ class TestSuggestedKeywords:
class TestStableKeywordChain:
- def _mk_graph(self, repo, category=bugs.NodeCategory.STABLEREQ):
+ def _mk_graph(self, repo, category=BugCategory.STABLEREQ):
# build a DependencyGraph without running its heavy __init__
graph = bugs.DependencyGraph.__new__(bugs.DependencyGraph)
graph.options = SimpleNamespace(repo=repo, category=category)
@@ -173,9 +153,9 @@ class TestStableKeywordChain:
for node in graph.nodes
for p, _ in node.pkgs
}
- parent_stable = by_key[("cat/parent-2", bugs.NodeCategory.STABLEREQ)]
- dep_stable = by_key[("cat/dep-1", bugs.NodeCategory.STABLEREQ)]
- dep_keyword = by_key[("cat/dep-1", bugs.NodeCategory.KEYWORDREQ)]
+ parent_stable = by_key[("cat/parent-2", BugCategory.STABLEREQ)]
+ dep_stable = by_key[("cat/dep-1", BugCategory.STABLEREQ)]
+ dep_keyword = by_key[("cat/dep-1", BugCategory.KEYWORDREQ)]
# three distinct nodes, dep appears as both stable and keyword
assert len(graph.nodes) == 3
assert dep_stable is not dep_keyword
@@ -220,7 +200,7 @@ class TestStableKeywordChain:
# a keyword target with no other versions to derive arches from must error
repo.create_ebuild("cat/a-1", KEYWORDS=["~amd64"])
pkg = max(repo.itermatch(atom("=cat/a-1")))
- graph = self._mk_graph(repo, category=bugs.NodeCategory.KEYWORDREQ)
+ graph = self._mk_graph(repo, category=BugCategory.KEYWORDREQ)
graph.targets = (pkg,)
graph._find_dependencies = lambda *a, **k: iter(())
with pytest.raises(SystemExit):
@@ -237,7 +217,7 @@ class TestStableKeywordChain:
# requesting a masked keyword is a hard error
repo.create_ebuild("cat/a-1", KEYWORDS=keywords)
pkg = max(repo.itermatch(atom("=cat/a-1")))
- graph = self._mk_graph(repo, category=bugs.NodeCategory.KEYWORDREQ)
+ graph = self._mk_graph(repo, category=BugCategory.KEYWORDREQ)
graph.targets = (pkg,)
graph.target_arches = {pkg: frozenset({"loong"})}
graph._find_dependencies = lambda *a, **k: iter(())
@@ -286,8 +266,8 @@ class TestStableKeywordChain:
)
graph.load_graph_toml(str(toml_file))
assert {node.category for node in graph.nodes} == {
- bugs.NodeCategory.KEYWORDREQ,
- bugs.NodeCategory.STABLEREQ,
+ BugCategory.KEYWORDREQ,
+ BugCategory.STABLEREQ,
}
def test_edit_graph_roundtrip_preserves_starting_node(self, repo):