Linux maintainer tooling and workflows
 help / color / mirror / Atom feed
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


  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