From: Brian Masney <masneyb@kernel.org>
To: "Kernel.org Tools" <tools@kernel.org>
Cc: Konstantin Ryabitsev <konstantin@linuxfoundation.org>,
Jerome Brunet <jbrunet@baylibre.com>,
Brian Masney <masneyb@kernel.org>
Subject: [PATCH b4 3/4] review: add pw_fetch_maintainers()
Date: Wed, 14 Oct 2026 20:03:40 -0400 [thread overview]
Message-ID: <20261014-pw-delegate-v1-3-a393ae92152a@kernel.org> (raw)
In-Reply-To: <20261014-pw-delegate-v1-0-a393ae92152a@kernel.org>
Patchwork only accepts a project maintainer as a patch delegate, so the
project's maintainers are the users that a delegate picker should offer.
Add a helper that reads them from the project endpoint.
Assisted-by: LLM
Signed-off-by: Brian Masney <masneyb@kernel.org>
---
src/b4/review/_review.py | 57 +++++++++++++++++++++++++++++++++++++++
src/tests/test_tui_pw.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 126 insertions(+)
diff --git a/src/b4/review/_review.py b/src/b4/review/_review.py
index b69a7f094bc7..a65b6a4f549b 100644
--- a/src/b4/review/_review.py
+++ b/src/b4/review/_review.py
@@ -4152,6 +4152,63 @@ def pw_fetch_states(pwkey: str, pwurl: str, pwproj: str) -> List[Dict[str, Any]]
return [{'slug': s, 'name': s.replace('-', ' ').title()} for s in default_slugs]
+def _pw_lookup_user_id(
+ pses: requests.Session, api_url: str, username: str
+) -> Optional[int]:
+ """Return the Patchwork User id for *username*, or None if not found.
+
+ The users endpoint searches username, names and email by substring, so
+ pick out the exact username match.
+ """
+ users_url = '/'.join((api_url, 'users', ''))
+ resp = pses.get(users_url, params={'q': username})
+ if resp.status_code != 200:
+ raise RuntimeError(f'Patchwork API error: {resp.status_code} {resp.reason}')
+ for u in resp.json():
+ if u.get('username') == username and u.get('id'):
+ return int(u['id'])
+ return None
+
+
+def pw_fetch_maintainers(pwkey: str, pwurl: str, pwproj: str) -> List[Dict[str, Any]]:
+ """Return the maintainers of a Patchwork project.
+
+ Patchwork only accepts a project maintainer as a patch delegate, so
+ this is the list of users a patch can be delegated to.
+
+ Returns a list of user dicts with 'id', 'username', 'name' and 'email'
+ keys, sorted by username. The 'id' is the User id that a patch's
+ delegate field takes.
+ """
+ pses, api_url = b4.get_patchwork_session(pwkey, pwurl)
+ # The projects endpoint accepts the project linkname in place of its id.
+ project_url = '/'.join((api_url, 'projects', pwproj, ''))
+ resp = pses.get(project_url)
+ if resp.status_code != 200:
+ raise RuntimeError(f'Patchwork API error: {resp.status_code} {resp.reason}')
+ users: List[Dict[str, Any]] = []
+ for m in resp.json().get('maintainers', []):
+ username = m.get('username')
+ if not username:
+ continue
+ # The maintainers list carries UserProfile ids, not the User ids
+ # that the delegate field expects, so resolve each one by username.
+ user_id = _pw_lookup_user_id(pses, api_url, username)
+ if user_id is None:
+ logger.debug('Could not find Patchwork user %s', username)
+ continue
+ name = ' '.join(p for p in (m.get('first_name'), m.get('last_name')) if p)
+ users.append(
+ {
+ 'id': user_id,
+ 'username': username,
+ 'name': name,
+ 'email': m.get('email') or '',
+ }
+ )
+ return sorted(users, key=lambda u: u['username'].lower())
+
+
def pw_fetch_checks(
pwkey: str, pwurl: str, patch_ids: List[int]
) -> List[Dict[str, Any]]:
diff --git a/src/tests/test_tui_pw.py b/src/tests/test_tui_pw.py
index c7af958e895f..1d7f62681f26 100644
--- a/src/tests/test_tui_pw.py
+++ b/src/tests/test_tui_pw.py
@@ -930,3 +930,72 @@ class TestPwApplyErrorText:
assert (
'0 updated, 2 failed: state: Object with slug=reviewing does not exist.'
) in notices
+
+
+# ---------------------------------------------------------------------------
+# Changing the delegate
+# ---------------------------------------------------------------------------
+
+
+class TestPwFetchMaintainers:
+ """pw_fetch_maintainers reads the project's maintainers list."""
+
+ def test_resolves_user_ids(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ """The maintainers list carries UserProfile ids; the delegate field
+ wants User ids, so each maintainer is looked up by username."""
+
+ class _Resp:
+ status_code = 200
+ reason = 'OK'
+
+ def __init__(self, body: Any) -> None:
+ self._body = body
+
+ def json(self) -> Any:
+ return self._body
+
+ # Users endpoint search is a substring match across several fields.
+ users_db: List[Dict[str, Any]] = [
+ {'id': 101, 'username': 'zed'},
+ {'id': 102, 'username': 'amy'},
+ {'id': 103, 'username': 'amyx'},
+ ]
+
+ class _Session:
+ def __init__(self) -> None:
+ self.calls: List[Any] = []
+
+ def get(self, url: str, params: Optional[Dict[str, str]] = None) -> _Resp:
+ self.calls.append((url, params))
+ if url.endswith('/projects/proj/'):
+ return _Resp(
+ {
+ 'maintainers': [
+ {
+ 'id': 2,
+ 'username': 'zed',
+ 'first_name': 'Zed',
+ 'last_name': 'Zee',
+ 'email': 'z@x',
+ },
+ {'id': 1, 'username': 'amy', 'email': 'a@x'},
+ {'id': 3, 'username': 'ghost', 'email': 'g@x'},
+ ]
+ }
+ )
+ assert url.endswith('/users/')
+ assert params is not None
+ q = params['q']
+ return _Resp([u for u in users_db if q in u['username']])
+
+ sess = _Session()
+ monkeypatch.setattr(
+ b4, 'get_patchwork_session', lambda key, url: (sess, 'https://pw/api/1.2')
+ )
+ users = b4.review.pw_fetch_maintainers('k', 'https://pw', 'proj')
+ assert sess.calls[0] == ('https://pw/api/1.2/projects/proj/', None)
+ # 'ghost' has no matching user and is left out.
+ assert users == [
+ {'id': 102, 'username': 'amy', 'name': '', 'email': 'a@x'},
+ {'id': 101, 'username': 'zed', 'name': 'Zed Zee', 'email': 'z@x'},
+ ]
--
2.55.0
next prev parent reply other threads:[~2026-10-15 0:04 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 0:03 [PATCH b4 0/4] review-tui: add support for setting the delegate in patchwork Brian Masney
2026-10-15 0:03 ` [PATCH b4 1/4] review-tui: show why a Patchwork update failed Brian Masney
2026-10-15 0:03 ` [PATCH b4 2/4] review-tui: let ApplyStateModal update other patch fields Brian Masney
2026-10-15 0:03 ` Brian Masney [this message]
2026-10-15 0:03 ` [PATCH b4 4/4] review-tui: change the Patchwork delegate from the pw browser Brian Masney
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261014-pw-delegate-v1-3-a393ae92152a@kernel.org \
--to=masneyb@kernel.org \
--cc=jbrunet@baylibre.com \
--cc=konstantin@linuxfoundation.org \
--cc=tools@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox