Linux maintainer tooling and workflows
 help / color / mirror / Atom feed
From: Tamir Duberstein <tamird@kernel.org>
To: "Kernel.org Tools" <tools@kernel.org>
Cc: Konstantin Ryabitsev <konstantin@linuxfoundation.org>,
	 Tamir Duberstein <tamird@kernel.org>
Subject: [PATCH ezgb 3/6] Add Ruff format check
Date: Sun, 19 Apr 2026 21:39:24 -0700	[thread overview]
Message-ID: <20260419-stronger-type-checking-v1-3-222775b987e5@kernel.org> (raw)
In-Reply-To: <20260419-stronger-type-checking-v1-0-222775b987e5@kernel.org>

Configure Ruff formatting to preserve the repository's single-quote
style and add the format check to local CI.

Apply the formatter once so the new check starts green.

Signed-off-by: Tamir Duberstein <tamird@kernel.org>
---
 ci.sh                     |   1 +
 pyproject.toml            |   3 +
 src/ezgb/__init__.py      |  74 ++++++++++----------
 src/ezgb/_git.py          |   4 +-
 src/ezgb/_models.py       |   9 +++
 src/ezgb/_reader.py       |  55 ++++++++++-----
 src/ezgb/_types.py        |   1 +
 src/ezgb/_writer.py       |  27 +++++---
 tests/conftest.py         |  62 ++++++++++++-----
 tests/test_ezgb.py        | 169 ++++++++++++++++++++++++++++++----------------
 tests/test_integration.py |  32 +++++++--
 11 files changed, 290 insertions(+), 147 deletions(-)

diff --git a/ci.sh b/ci.sh
index 3001db2..db7914f 100755
--- a/ci.sh
+++ b/ci.sh
@@ -3,5 +3,6 @@
 set -eu
 
 uv run ruff check
+uv run ruff format --check
 uv run mypy .
 uv run pytest --durations=0
diff --git a/pyproject.toml b/pyproject.toml
index aeae989..c043817 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,3 +47,6 @@ strict = true
 
 [tool.ruff.lint]
 select = ["E", "F", "W", "I"]
+
+[tool.ruff.format]
+quote-style = "single"
diff --git a/src/ezgb/__init__.py b/src/ezgb/__init__.py
index f60d63e..5cb23ae 100644
--- a/src/ezgb/__init__.py
+++ b/src/ezgb/__init__.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2024 by the Linux Foundation
 """ezgb: a standalone Python library for git-bug repositories."""
+
 from __future__ import annotations
 
 from collections.abc import Iterator
@@ -77,6 +78,7 @@ class GitBugRepo:
             return int(since.timestamp())
         # str -- delegate to the reader's parser
         from ezgb._reader import BugReader
+
         return BugReader._parse_since(since)
 
     def list_bugs(
@@ -94,9 +96,13 @@ class GitBugRepo:
         ``YYYYMMDDHHMMSS`` format.  Only bugs whose tip commit is
         newer than this value are returned.
         """
-        return list(self.iter_bugs(
-            status=status, label=label, since=since,
-        ))
+        return list(
+            self.iter_bugs(
+                status=status,
+                label=label,
+                since=since,
+            )
+        )
 
     def iter_bugs(
         self,
@@ -143,16 +149,18 @@ class GitBugRepo:
             if cached is not None:
                 results = cached
                 if status is not None:
-                    results = [s for s in results
-                               if s.status == status]
+                    results = [s for s in results if s.status == status]
                 if label is not None:
-                    results = [s for s in results
-                               if label in s.labels]
+                    results = [s for s in results if label in s.labels]
                 return results
         # Slow path: native git object reads
-        return list(self.iter_bug_summaries(
-            status=status, label=label, since=since,
-        ))
+        return list(
+            self.iter_bug_summaries(
+                status=status,
+                label=label,
+                since=since,
+            )
+        )
 
     def _list_summaries_from_cli(self) -> list[BugSummary] | None:
         """Try to list summaries via the git-bug CLI cache.
@@ -167,8 +175,8 @@ class GitBugRepo:
             return None
 
         from ezgb._git import git_bug_cli
-        ecode, out, _err = git_bug_cli(
-            self._repo, ['bug', '-f', 'json'])
+
+        ecode, out, _err = git_bug_cli(self._repo, ['bug', '-f', 'json'])
         if ecode != 0 or not out.strip():
             return None
         try:
@@ -183,24 +191,19 @@ class GitBugRepo:
             if not bid:
                 continue
             status_str = str(raw.get('status', 'open'))
-            bug_status = (Status.CLOSED if status_str == 'closed'
-                          else Status.OPEN)
+            bug_status = Status.CLOSED if status_str == 'closed' else Status.OPEN
             create_time = raw.get('create_time') or {}
             edit_time = raw.get('edit_time') or {}
-            ct = (create_time.get('timestamp', 0)
-                  if isinstance(create_time, dict) else 0)
-            et = (edit_time.get('timestamp', 0)
-                  if isinstance(edit_time, dict) else 0)
+            ct = create_time.get('timestamp', 0) if isinstance(create_time, dict) else 0
+            et = edit_time.get('timestamp', 0) if isinstance(edit_time, dict) else 0
             if not isinstance(ct, (bool, int, float, str)):
                 ct = 0
             if not isinstance(et, (bool, int, float, str)):
                 et = 0
             author = raw.get('author') or {}
-            author_name = (author.get('name', '')
-                           if isinstance(author, dict) else '')
+            author_name = author.get('name', '') if isinstance(author, dict) else ''
             assert isinstance(author_name, str)
-            author_id = (author.get('id', '')
-                         if isinstance(author, dict) else '')
+            author_id = author.get('id', '') if isinstance(author, dict) else ''
             raw_labels = raw.get('labels') or []
             if isinstance(raw_labels, list):
                 labels = frozenset(str(lb) for lb in raw_labels)
@@ -209,19 +212,19 @@ class GitBugRepo:
             comment_count = raw.get('comments', 0)
             if not isinstance(comment_count, int):
                 comment_count = 0
-            results.append(BugSummary(
-                id=bid,
-                title=str(raw.get('title', '')),
-                status=bug_status,
-                creator_id=str(author_id),
-                created_at=datetime.fromtimestamp(
-                    int(ct), tz=timezone.utc),
-                labels=labels,
-                comment_count=comment_count,
-                author_name=author_name,
-                edited_at=datetime.fromtimestamp(
-                    int(et), tz=timezone.utc),
-            ))
+            results.append(
+                BugSummary(
+                    id=bid,
+                    title=str(raw.get('title', '')),
+                    status=bug_status,
+                    creator_id=str(author_id),
+                    created_at=datetime.fromtimestamp(int(ct), tz=timezone.utc),
+                    labels=labels,
+                    comment_count=comment_count,
+                    author_name=author_name,
+                    edited_at=datetime.fromtimestamp(int(et), tz=timezone.utc),
+                )
+            )
         return results
 
     def iter_bug_summaries(
@@ -269,6 +272,7 @@ class GitBugRepo:
         import json
 
         from ezgb._git import git_bug_cli
+
         ecode, out, _err = git_bug_cli(self._repo, ['bug', '-f', 'json', query])
         if ecode != 0:
             return []
diff --git a/src/ezgb/_git.py b/src/ezgb/_git.py
index c09b066..4e29646 100644
--- a/src/ezgb/_git.py
+++ b/src/ezgb/_git.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2024 by the Linux Foundation
 """Thin git subprocess helpers for ezgb."""
+
 from __future__ import annotations
 
 import os
@@ -51,7 +52,8 @@ def git_lines(repo_path: str, args: list[str]) -> list[str]:
 
 
 def git_bug_cli(
-    repo_path: str, args: list[str],
+    repo_path: str,
+    args: list[str],
     stdin: str | None = None,
 ) -> tuple[int, str, str]:
     """Run ``git -C REPO bug <args>`` for write operations.
diff --git a/src/ezgb/_models.py b/src/ezgb/_models.py
index 144a7ee..b8716f0 100644
--- a/src/ezgb/_models.py
+++ b/src/ezgb/_models.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2024 by the Linux Foundation
 """Data models, enums, exceptions, and constants for ezgb."""
+
 from __future__ import annotations
 
 import enum
@@ -13,6 +14,7 @@ _EPOCH_UTC = datetime.min.replace(tzinfo=timezone.utc)
 
 # -- Exceptions --------------------------------------------------------------
 
+
 class EzgbError(Exception):
     """Base exception for all ezgb errors."""
 
@@ -43,8 +45,10 @@ class CliError(EzgbError):
 
 # -- Enums -------------------------------------------------------------------
 
+
 class Status(enum.Enum):
     """Bug status matching git-bug's common.Status values."""
+
     OPEN = 1
     CLOSED = 2
 
@@ -72,9 +76,11 @@ STATUS_CLOSED: int = 2
 
 # -- Dataclasses -------------------------------------------------------------
 
+
 @dataclass(frozen=True)
 class Identity:
     """A git-bug user identity."""
+
     id: str
     name: str
     email: str
@@ -84,6 +90,7 @@ class Identity:
 @dataclass
 class Comment:
     """A comment on a bug."""
+
     id: str
     author: Identity
     text: str
@@ -95,6 +102,7 @@ class Comment:
 @dataclass
 class Bug:
     """A git-bug bug snapshot reconstructed from operations."""
+
     id: str
     title: str
     status: Status
@@ -117,6 +125,7 @@ class BugSummary:
     *edited_at* are available. When built from native git objects,
     these may be empty/epoch if identity resolution was skipped.
     """
+
     id: str
     title: str
     status: Status
diff --git a/src/ezgb/_reader.py b/src/ezgb/_reader.py
index dac4a8c..735d41d 100644
--- a/src/ezgb/_reader.py
+++ b/src/ezgb/_reader.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2024 by the Linux Foundation
 """Git object reading, operation pack replay, and caching for ezgb."""
+
 from __future__ import annotations
 
 import hashlib
@@ -47,12 +48,15 @@ _K = TypeVar('_K')
 _T = TypeVar('_T')
 _U = TypeVar('_U')
 
+
 def _is_list(value: list[_T], item_ty: type[_U]) -> TypeGuard[list[_U]]:
     return all(isinstance(item, item_ty) for item in value)
 
+
 def _is_dict_values(value: dict[_K, _T], value_ty: type[_U]) -> TypeGuard[dict[_K, _U]]:
     return all(isinstance(value, value_ty) for value in value.values())
 
+
 def _combine_ids(primary: str, secondary: str) -> str:
     """Interleave primary and secondary IDs into a CombinedId.
 
@@ -75,6 +79,7 @@ def _combine_ids(primary: str, secondary: str) -> str:
             pi += 1
     return ''.join(result)
 
+
 class BugReader:
     """Reads bug and identity data from git-bug's git object storage."""
 
@@ -113,7 +118,9 @@ class BugReader:
         return bytes(obj.data)
 
     def _walk_ref_tree_blobs(
-        self, refname: str, target_file: str = 'ops',
+        self,
+        refname: str,
+        target_file: str = 'ops',
     ) -> list[tuple[str, str]]:
         """Walk the commit chain for a ref and find named blobs.
 
@@ -142,7 +149,9 @@ class BugReader:
 
     @staticmethod
     def _check_format_version_tree(
-        tree: pygit2.Tree, prefix: str, supported: int,
+        tree: pygit2.Tree,
+        prefix: str,
+        supported: int,
     ) -> None:
         """Validate the git-bug format version from a commit tree.
 
@@ -153,7 +162,7 @@ class BugReader:
             name: str = entry.name or ''
             if name.startswith(prefix):
                 try:
-                    version = int(name[len(prefix):])
+                    version = int(name[len(prefix) :])
                 except ValueError:
                     continue
                 if version != supported:
@@ -166,7 +175,10 @@ class BugReader:
     # -- Ref enumeration -----------------------------------------------------
 
     def _list_refs(
-        self, prefix: str, *, since: int = 0,
+        self,
+        prefix: str,
+        *,
+        since: int = 0,
     ) -> list[tuple[str, str]]:
         """Return ``[(entity_id, commit_hex)]`` for refs under *prefix*.
 
@@ -188,7 +200,9 @@ class BugReader:
         return results
 
     def list_bug_refs(
-        self, *, since: int = 0,
+        self,
+        *,
+        since: int = 0,
     ) -> list[tuple[str, str]]:
         """Return ``[(bug_id, commit_hash)]`` for all bugs."""
         return self._list_refs('refs/bugs/', since=since)
@@ -227,7 +241,8 @@ class BugReader:
     # -- Operation pack parsing ----------------------------------------------
 
     def _get_op_packs(
-        self, bid: str,
+        self,
+        bid: str,
     ) -> tuple[list[_JsonObject], list[str]]:
         """Walk the commit chain for a bug and return operation packs
         (oldest first) together with the raw blob strings.
@@ -246,8 +261,7 @@ class BugReader:
         ref = self._pygit.references.get(refname)
         if ref is not None:
             tip = ref.peel(pygit2.Commit)
-            self._check_format_version_tree(
-                tip.tree, 'version-', SUPPORTED_BUG_FORMAT)
+            self._check_format_version_tree(tip.tree, 'version-', SUPPORTED_BUG_FORMAT)
         packs: list[_JsonObject] = []
         raw_blobs: list[str] = []
         for _commit, blob_hash in entries:
@@ -257,7 +271,8 @@ class BugReader:
             except json.JSONDecodeError:
                 logger.warning(
                     'failed to parse ops blob %s for bug %s',
-                    blob_hash, bid,
+                    blob_hash,
+                    bid,
                 )
                 continue
             assert isinstance(pack, dict)
@@ -277,7 +292,9 @@ class BugReader:
             return self._identity_cache[identity_id]
 
         fallback = Identity(
-            id=identity_id, name=identity_id, email=identity_id,
+            id=identity_id,
+            name=identity_id,
+            email=identity_id,
         )
         refname = 'refs/identities/' + identity_id
         try:
@@ -417,7 +434,8 @@ class BugReader:
         raise ValueError('cannot parse since=%s' % since)
 
     def build_bug(
-        self, bid: str,
+        self,
+        bid: str,
         packs: list[_JsonObject] | None = None,
         raw_blobs: list[str] | None = None,
     ) -> Bug:
@@ -457,8 +475,9 @@ class BugReader:
             assert isinstance(author_value, dict)
             author_id = author_value.get('id', '')
             assert isinstance(author_id, str)
-            raw_ops = (raw_ops_per_pack[pack_idx]
-                       if pack_idx < len(raw_ops_per_pack) else [])
+            raw_ops = (
+                raw_ops_per_pack[pack_idx] if pack_idx < len(raw_ops_per_pack) else []
+            )
             ops = pack.get('ops', [])
             assert isinstance(ops, list)
             for op_idx, op in enumerate(ops):
@@ -467,8 +486,7 @@ class BugReader:
                 assert isinstance(op_type, int)
                 timestamp = op.get('timestamp', 0)
                 assert isinstance(timestamp, int)
-                raw_json = (raw_ops[op_idx]
-                            if op_idx < len(raw_ops) else '')
+                raw_json = raw_ops[op_idx] if op_idx < len(raw_ops) else ''
                 op_id = self._op_hash(raw_json) if raw_json else ''
 
                 if op_type == OP_CREATE:
@@ -530,7 +548,7 @@ class BugReader:
                 elif op_type == OP_SET_STATUS:
                     status_val = op.get('status', STATUS_OPEN)
                     assert isinstance(status_val, int)
-                    is_open = (status_val == STATUS_OPEN)
+                    is_open = status_val == STATUS_OPEN
 
                 elif op_type == OP_LABEL_CHANGE:
                     added = op.get('added', [])
@@ -588,7 +606,8 @@ class BugReader:
         return bug
 
     def build_bug_summary(
-        self, bid: str,
+        self,
+        bid: str,
         packs: list[_JsonObject] | None = None,
     ) -> BugSummary:
         """Build a lightweight bug summary by replaying operation packs.
@@ -649,7 +668,7 @@ class BugReader:
                 elif op_type == OP_SET_STATUS:
                     status_val = op.get('status', STATUS_OPEN)
                     assert isinstance(status_val, int)
-                    is_open = (status_val == STATUS_OPEN)
+                    is_open = status_val == STATUS_OPEN
 
                 elif op_type == OP_LABEL_CHANGE:
                     added = op.get('added', [])
diff --git a/src/ezgb/_types.py b/src/ezgb/_types.py
index e3e46da..389b743 100644
--- a/src/ezgb/_types.py
+++ b/src/ezgb/_types.py
@@ -1,4 +1,5 @@
 """Internal type aliases."""
+
 from __future__ import annotations
 
 from typing import Dict, List, Union
diff --git a/src/ezgb/_writer.py b/src/ezgb/_writer.py
index 8888a04..ae1d45f 100644
--- a/src/ezgb/_writer.py
+++ b/src/ezgb/_writer.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2024 by the Linux Foundation
 """CLI wrappers for git-bug write operations."""
+
 from __future__ import annotations
 
 import logging
@@ -35,8 +36,7 @@ class BugWriter:
         self._repo = repo_path
         self._reader = reader
 
-    def _cli(self, args: list[str],
-             stdin: str | None = None) -> tuple[int, str, str]:
+    def _cli(self, args: list[str], stdin: str | None = None) -> tuple[int, str, str]:
         """Run a git-bug CLI command."""
         return git_bug_cli(self._repo, args, stdin=stdin)
 
@@ -58,9 +58,7 @@ class BugWriter:
         # Parse human_id from output like "abc1234 created"
         match = _NEW_BUG_RE.match(out.strip())
         if not match:
-            raise CliError(
-                'could not parse bug ID from git bug new output: %s' % out
-            )
+            raise CliError('could not parse bug ID from git bug new output: %s' % out)
         human_id = match.group(1)
 
         self._reader.invalidate()
@@ -71,8 +69,13 @@ class BugWriter:
         """Add a comment to a bug and return the new Comment."""
         bid = self._reader.resolve_bug_id(bid)
         args = [
-            'bug', 'comment', 'new', bid,
-            '-F', '-', '--non-interactive',
+            'bug',
+            'comment',
+            'new',
+            bid,
+            '-F',
+            '-',
+            '--non-interactive',
         ]
         ecode, out, err = self._cli(args, stdin=text)
         if ecode != 0:
@@ -89,8 +92,13 @@ class BugWriter:
         """
         bid = self._reader.resolve_bug_id(bid)
         args = [
-            'bug', 'comment', 'edit', comment_id,
-            '-F', '-', '--non-interactive',
+            'bug',
+            'comment',
+            'edit',
+            comment_id,
+            '-F',
+            '-',
+            '--non-interactive',
         ]
         ecode, out, err = self._cli(args, stdin=text)
         if ecode != 0:
@@ -166,4 +174,3 @@ class BugWriter:
         invalidate caches after a successful pull.
         """
         return self._cli(['pull', remote])
-
diff --git a/tests/conftest.py b/tests/conftest.py
index ed03529..75e3b95 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@
 Creates real git objects in temporary bare repos using pygit2 so
 the BugReader can read them without subprocess mocking.
 """
+
 from __future__ import annotations
 
 import json
@@ -31,23 +32,28 @@ CliResult = tuple[int, str, str]
 
 # -- Test data factories -----------------------------------------------------
 
+
 def make_identity_version(name: str, email: str) -> str:
     """Build a JSON identity version blob (format 2)."""
-    return json.dumps({
-        'version': 2,
-        'unix_time': 1700000000,
-        'name': name,
-        'email': email,
-        'login': email.split('@')[0],
-    })
+    return json.dumps(
+        {
+            'version': 2,
+            'unix_time': 1700000000,
+            'name': name,
+            'email': email,
+            'login': email.split('@')[0],
+        }
+    )
 
 
 def make_op_pack(author_id: str, ops: list[JsonObject]) -> str:
     """Build a JSON operation pack with author and ops array."""
-    return json.dumps({
-        'author': {'id': author_id},
-        'ops': ops,
-    })
+    return json.dumps(
+        {
+            'author': {'id': author_id},
+            'ops': ops,
+        }
+    )
 
 
 def make_create_op(
@@ -101,8 +107,10 @@ def make_edit_comment_op(
 ) -> JsonObject:
     """Build an OP_EDIT_COMMENT (type 6) operation."""
     return {
-        'type': 6, 'timestamp': timestamp,
-        'target': target, 'message': message,
+        'type': 6,
+        'timestamp': timestamp,
+        'target': target,
+        'message': message,
     }
 
 
@@ -117,10 +125,12 @@ def make_label_change_op(
     if removed is None:
         removed = []
     return {
-        'type': 5, 'timestamp': timestamp,
+        'type': 5,
+        'timestamp': timestamp,
         # Shallow copies are needed because list is invariant. We could use
         # Sequence instead, but that's fancy for little benefit.
-        'added': [a for a in added], 'removed': [r for r in removed],
+        'added': [a for a in added],
+        'removed': [r for r in removed],
     }
 
 
@@ -139,6 +149,7 @@ def make_noop_op(timestamp: int = 1700006000) -> JsonObject:
 
 # -- Real git object helpers -------------------------------------------------
 
+
 def _create_bug_commit(
     repo: pygit2.Repository,
     refname: str,
@@ -161,7 +172,12 @@ def _create_bug_commit(
 
     parents = [parent_oid] if parent_oid else []
     return repo.create_commit(
-        refname, _SIG, _SIG, 'op pack', tree_oid, parents,
+        refname,
+        _SIG,
+        _SIG,
+        'op pack',
+        tree_oid,
+        parents,
     )
 
 
@@ -181,12 +197,18 @@ def _create_identity_commit(
     tree_oid = tb.write()
 
     return repo.create_commit(
-        refname, _SIG, _SIG, 'identity', tree_oid, [],
+        refname,
+        _SIG,
+        _SIG,
+        'identity',
+        tree_oid,
+        [],
     )
 
 
 # -- Convenience setup -------------------------------------------------------
 
+
 def setup_identity(
     repo_path: str,
     identity_id: str,
@@ -236,7 +258,10 @@ def setup_single_bug(
     if extra_packs:
         for extra_json in extra_packs:
             parent = _create_bug_commit(
-                repo, refname, extra_json, parent_oid=parent,
+                repo,
+                refname,
+                extra_json,
+                parent_oid=parent,
             )
 
     # Pre-cache bug ID resolution so tests don't need separate
@@ -249,6 +274,7 @@ def setup_single_bug(
 
 # -- Fixtures ----------------------------------------------------------------
 
+
 class RecordingBugWriter(BugWriter):
     """BugWriter test double that records CLI calls and canned responses."""
 
diff --git a/tests/test_ezgb.py b/tests/test_ezgb.py
index 6d9f491..c36a454 100644
--- a/tests/test_ezgb.py
+++ b/tests/test_ezgb.py
@@ -6,6 +6,7 @@ writer (CLI-based mutations), and GitBugRepo facade.
 Uses real git objects in temporary bare repos (via pygit2) instead
 of subprocess mocking. See conftest.py for fixtures and helpers.
 """
+
 import json
 
 import pygit2
@@ -44,6 +45,7 @@ from ezgb._reader import BugReader, _combine_ids
 # Unit tests: _combine_ids
 # ------------------------------------------------------------------
 
+
 class TestCombineIds:
     """Unit tests for the CombinedId interleaving function."""
 
@@ -72,6 +74,7 @@ class TestCombineIds:
 # Bug ID resolution
 # ------------------------------------------------------------------
 
+
 class TestResolveBugId:
     def test_resolves_full_id(self, reader: BugReader, repo_path: str) -> None:
         repo = pygit2.Repository(repo_path)
@@ -117,6 +120,7 @@ class TestResolveBugId:
 # Identity resolution
 # ------------------------------------------------------------------
 
+
 class TestResolveIdentity:
     def test_resolves_by_id(self, reader: BugReader, repo_path: str) -> None:
         setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com')
@@ -139,11 +143,13 @@ class TestResolveIdentity:
 
     def test_unsupported_format_raises(self, reader: BugReader, repo_path: str) -> None:
         repo = pygit2.Repository(repo_path)
-        identity_json = json.dumps({
-            'version': 99,
-            'name': 'Alice',
-            'email': 'alice@example.com',
-        })
+        identity_json = json.dumps(
+            {
+                'version': 99,
+                'name': 'Alice',
+                'email': 'alice@example.com',
+            }
+        )
         refname = 'refs/identities/%s' % IDENTITY_ID
         _create_identity_commit(repo, refname, identity_json)
         with pytest.raises(UnsupportedFormatError, match='identity format'):
@@ -154,6 +160,7 @@ class TestResolveIdentity:
 # Bug snapshot reconstruction
 # ------------------------------------------------------------------
 
+
 class TestBuildBug:
     def test_snapshot_reconstruction(self, reader: BugReader, repo_path: str) -> None:
         setup_single_bug(repo_path, reader)
@@ -188,7 +195,8 @@ class TestBuildBug:
     def test_label_add_and_remove(self, reader: BugReader, repo_path: str) -> None:
         add_op = make_label_change_op(added=['bug', 'priority/high'])
         rm_op = make_label_change_op(
-            removed=['bug'], timestamp=1700005000,
+            removed=['bug'],
+            timestamp=1700005000,
         )
         setup_single_bug(repo_path, reader, extra_ops=[add_op, rm_op])
         bug = reader.build_bug(BUG_ID)
@@ -206,16 +214,20 @@ class TestBuildBug:
         assert bug.comments[1].text == 'A follow-up'
 
     def test_comment_with_attachment(self, reader: BugReader, repo_path: str) -> None:
-        ops: list[JsonObject] = [{
-            'type': 1,
-            'timestamp': 1700000000,
-            'title': 'Bug with file',
-            'message': 'See attached',
-            'files': ['blobhash123'],
-        }]
+        ops: list[JsonObject] = [
+            {
+                'type': 1,
+                'timestamp': 1700000000,
+                'title': 'Bug with file',
+                'message': 'See attached',
+                'files': ['blobhash123'],
+            }
+        ]
         setup_single_bug(
-            repo_path, reader,
-            title='Bug with file', message='See attached',
+            repo_path,
+            reader,
+            title='Bug with file',
+            message='See attached',
             extra_ops=None,
         )
         # Rebuild with custom ops containing files
@@ -242,7 +254,8 @@ class TestBuildBug:
         target_hash = BugReader._op_hash(raw_ops[0])
 
         edit_op = make_edit_comment_op(
-            target=target_hash, message='Edited text',
+            target=target_hash,
+            message='Edited text',
         )
         # Build the full pack with both ops
         ops = [create_op, edit_op]
@@ -250,7 +263,9 @@ class TestBuildBug:
 
         repo = pygit2.Repository(repo_path)
         _create_bug_commit(
-            repo, 'refs/bugs/%s' % BUG_ID, full_pack_json,
+            repo,
+            'refs/bugs/%s' % BUG_ID,
+            full_pack_json,
         )
         reader._resolve_cache[BUG_ID] = BUG_ID
         setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com')
@@ -259,18 +274,23 @@ class TestBuildBug:
         assert bug.comments[0].text == 'Edited text'
 
     def test_edit_comment_unmatched_target(
-        self, reader: BugReader, repo_path: str,
+        self,
+        reader: BugReader,
+        repo_path: str,
     ) -> None:
         """Unmatched OP_EDIT_COMMENT target leaves text unchanged."""
         edit_op = make_edit_comment_op(
-            target='nonexistent', message='Edited text',
+            target='nonexistent',
+            message='Edited text',
         )
         setup_single_bug(repo_path, reader, extra_ops=[edit_op])
         bug = reader.build_bug(BUG_ID)
         assert bug.comments[0].text == 'Bug description'
 
     def test_metadata_from_set_metadata(
-        self, reader: BugReader, repo_path: str,
+        self,
+        reader: BugReader,
+        repo_path: str,
     ) -> None:
         meta_op = make_set_metadata_op({'key': 'value'})
         setup_single_bug(repo_path, reader, extra_ops=[meta_op])
@@ -286,12 +306,17 @@ class TestBuildBug:
     def test_multiple_op_packs(self, reader: BugReader, repo_path: str) -> None:
         """Operations spread across multiple commits are replayed
         in order."""
-        pack2_json = make_op_pack(IDENTITY_ID, [
-            make_set_title_op('Updated'),
-        ])
+        pack2_json = make_op_pack(
+            IDENTITY_ID,
+            [
+                make_set_title_op('Updated'),
+            ],
+        )
         setup_single_bug(
-            repo_path, reader,
-            title='Original', message='Body',
+            repo_path,
+            reader,
+            title='Original',
+            message='Body',
             extra_packs=[pack2_json],
         )
         bug = reader.build_bug(BUG_ID)
@@ -310,11 +335,16 @@ class TestBuildBug:
 
     def test_unsupported_format_raises(self, reader: BugReader, repo_path: str) -> None:
         repo = pygit2.Repository(repo_path)
-        ops_json = make_op_pack(IDENTITY_ID, [
-            make_create_op('Test', 'body'),
-        ])
+        ops_json = make_op_pack(
+            IDENTITY_ID,
+            [
+                make_create_op('Test', 'body'),
+            ],
+        )
         _create_bug_commit(
-            repo, 'refs/bugs/%s' % BUG_ID, ops_json,
+            repo,
+            'refs/bugs/%s' % BUG_ID,
+            ops_json,
             version_tag='version-99',
         )
         reader._resolve_cache[BUG_ID] = BUG_ID
@@ -326,6 +356,7 @@ class TestBuildBug:
 # Bug summary (lightweight list-view snapshot)
 # ------------------------------------------------------------------
 
+
 class TestBuildBugSummary:
     def test_basic(self, reader: BugReader, repo_path: str) -> None:
         setup_single_bug(repo_path, reader)
@@ -352,7 +383,8 @@ class TestBuildBugSummary:
     def test_label_changes(self, reader: BugReader, repo_path: str) -> None:
         add_op = make_label_change_op(added=['bug', 'priority/high'])
         rm_op = make_label_change_op(
-            removed=['bug'], timestamp=1700005000,
+            removed=['bug'],
+            timestamp=1700005000,
         )
         setup_single_bug(repo_path, reader, extra_ops=[add_op, rm_op])
         s = reader.build_bug_summary(BUG_ID)
@@ -378,10 +410,13 @@ class TestBuildBugSummary:
         assert s.comment_count == 0
 
     def test_edit_comment_does_not_affect_count(
-        self, reader: BugReader, repo_path: str,
+        self,
+        reader: BugReader,
+        repo_path: str,
     ) -> None:
         edit_op = make_edit_comment_op(
-            target='whatever', message='Edited',
+            target='whatever',
+            message='Edited',
         )
         setup_single_bug(repo_path, reader, extra_ops=[edit_op])
         s = reader.build_bug_summary(BUG_ID)
@@ -434,6 +469,7 @@ class TestBuildBugSummary:
 # Prefetch
 # ------------------------------------------------------------------
 
+
 class TestPrefetchBugs:
     def test_warms_cache(self, reader: BugReader, repo_path: str) -> None:
         setup_single_bug(repo_path, reader)
@@ -458,6 +494,7 @@ class TestPrefetchBugs:
 # Ref enumeration
 # ------------------------------------------------------------------
 
+
 class TestListBugRefs:
     def test_returns_refs(self, reader: BugReader, repo_path: str) -> None:
         setup_single_bug(repo_path, reader)
@@ -482,6 +519,7 @@ class TestListIdentityRefs:
 # Cache management
 # ------------------------------------------------------------------
 
+
 class TestInvalidate:
     def test_invalidate_single_bug(self, reader: BugReader, repo_path: str) -> None:
         setup_single_bug(repo_path, reader)
@@ -505,6 +543,7 @@ class TestInvalidate:
 # Writer: create_bug
 # ------------------------------------------------------------------
 
+
 class TestCreateBug:
     def test_creates_and_returns_bug(
         self,
@@ -536,6 +575,7 @@ class TestCreateBug:
 # Writer: add_comment
 # ------------------------------------------------------------------
 
+
 class TestAddComment:
     def test_adds_and_returns_comment(
         self,
@@ -558,7 +598,9 @@ class TestAddComment:
         comment_op = make_comment_op('New comment', timestamp=1700005000)
         pack_json = make_op_pack(IDENTITY_ID, [comment_op])
         _create_bug_commit(
-            repo, 'refs/bugs/%s' % BUG_ID, pack_json,
+            repo,
+            'refs/bugs/%s' % BUG_ID,
+            pack_json,
             parent_oid=parent.id,
         )
 
@@ -571,6 +613,7 @@ class TestAddComment:
 # Writer: set_status
 # ------------------------------------------------------------------
 
+
 class TestSetStatus:
     def test_close(
         self,
@@ -580,9 +623,7 @@ class TestSetStatus:
     ) -> None:
         setup_single_bug(repo_path, reader)
         writer.set_status(BUG_ID, Status.CLOSED)
-        assert any(
-            'close' in ' '.join(c) for c in writer._cli_calls
-        )
+        assert any('close' in ' '.join(c) for c in writer._cli_calls)
 
     def test_open(
         self,
@@ -592,15 +633,14 @@ class TestSetStatus:
     ) -> None:
         setup_single_bug(repo_path, reader)
         writer.set_status(BUG_ID, Status.OPEN)
-        assert any(
-            'open' in ' '.join(c) for c in writer._cli_calls
-        )
+        assert any('open' in ' '.join(c) for c in writer._cli_calls)
 
 
 # ------------------------------------------------------------------
 # Writer: set_title
 # ------------------------------------------------------------------
 
+
 class TestSetTitle:
     def test_updates_title(
         self,
@@ -610,15 +650,14 @@ class TestSetTitle:
     ) -> None:
         setup_single_bug(repo_path, reader)
         writer.set_title(BUG_ID, 'New title')
-        assert any(
-            'New title' in ' '.join(c) for c in writer._cli_calls
-        )
+        assert any('New title' in ' '.join(c) for c in writer._cli_calls)
 
 
 # ------------------------------------------------------------------
 # Writer: labels
 # ------------------------------------------------------------------
 
+
 class TestLabels:
     def test_add_label(
         self,
@@ -628,9 +667,7 @@ class TestLabels:
     ) -> None:
         setup_single_bug(repo_path, reader)
         writer.add_label(BUG_ID, 'priority/high')
-        assert any(
-            'priority/high' in ' '.join(c) for c in writer._cli_calls
-        )
+        assert any('priority/high' in ' '.join(c) for c in writer._cli_calls)
 
     def test_remove_label(
         self,
@@ -640,9 +677,7 @@ class TestLabels:
     ) -> None:
         setup_single_bug(repo_path, reader)
         writer.remove_label(BUG_ID, 'old-label')
-        assert any(
-            'old-label' in ' '.join(c) for c in writer._cli_calls
-        )
+        assert any('old-label' in ' '.join(c) for c in writer._cli_calls)
 
 
 # ------------------------------------------------------------------
@@ -650,6 +685,7 @@ class TestLabels:
 # GitBugRepo facade
 # ------------------------------------------------------------------
 
+
 class TestGitBugRepo:
     def test_get_bug(self, repo_path: str) -> None:
         repo_obj = GitBugRepo(repo_path)
@@ -764,8 +800,7 @@ class TestGitBugRepo:
         repo_obj = GitBugRepo(repo_path)
         # Create bug with a specific commit timestamp
         repo = pygit2.Repository(repo_path)
-        sig = pygit2.Signature('Test', 'test@test.com',
-                               time=1700005000, offset=0)
+        sig = pygit2.Signature('Test', 'test@test.com', time=1700005000, offset=0)
         ops = [make_create_op('Test bug', 'Bug description')]
         pack_json = make_op_pack(IDENTITY_ID, ops)
         ops_blob = repo.create_blob(pack_json.encode())
@@ -775,7 +810,12 @@ class TestGitBugRepo:
         tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB)
         tree_oid = tb.write()
         repo.create_commit(
-            'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [],
+            'refs/bugs/%s' % BUG_ID,
+            sig,
+            sig,
+            'op pack',
+            tree_oid,
+            [],
         )
         repo_obj._reader._resolve_cache[BUG_ID] = BUG_ID
         setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com')
@@ -795,8 +835,7 @@ class TestGitBugRepo:
         """list_bugs(since='2023-11-14 ...') parses the string."""
         repo_obj = GitBugRepo(repo_path)
         repo = pygit2.Repository(repo_path)
-        sig = pygit2.Signature('Test', 'test@test.com',
-                               time=1700005000, offset=0)
+        sig = pygit2.Signature('Test', 'test@test.com', time=1700005000, offset=0)
         ops = [make_create_op('Test bug', 'Bug description')]
         pack_json = make_op_pack(IDENTITY_ID, ops)
         ops_blob = repo.create_blob(pack_json.encode())
@@ -806,7 +845,12 @@ class TestGitBugRepo:
         tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB)
         tree_oid = tb.write()
         repo.create_commit(
-            'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [],
+            'refs/bugs/%s' % BUG_ID,
+            sig,
+            sig,
+            'op pack',
+            tree_oid,
+            [],
         )
         repo_obj._reader._resolve_cache[BUG_ID] = BUG_ID
         setup_identity(repo_path, IDENTITY_ID, 'Alice', 'alice@example.com')
@@ -819,6 +863,7 @@ class TestGitBugRepo:
 # Attachment reading
 # ------------------------------------------------------------------
 
+
 class TestCatBlobBytes:
     def test_reads_bytes(self, reader: BugReader, repo_path: str) -> None:
         repo = pygit2.Repository(repo_path)
@@ -836,14 +881,17 @@ class TestCatBlobBytes:
 # Since filtering on list_bug_refs
 # ------------------------------------------------------------------
 
+
 class TestListBugRefsSince:
     def _create_bug_with_timestamp(
-        self, repo_path: str, reader: BugReader, ts: int,
+        self,
+        repo_path: str,
+        reader: BugReader,
+        ts: int,
     ) -> None:
         """Helper: create a bug ref with a specific commit timestamp."""
         repo = pygit2.Repository(repo_path)
-        sig = pygit2.Signature('Test', 'test@test.com',
-                               time=ts, offset=0)
+        sig = pygit2.Signature('Test', 'test@test.com', time=ts, offset=0)
         ops = [make_create_op('Test bug', 'description')]
         pack_json = make_op_pack(IDENTITY_ID, ops)
         ops_blob = repo.create_blob(pack_json.encode())
@@ -853,7 +901,12 @@ class TestListBugRefsSince:
         tb.insert('version-4', version_blob, pygit2.GIT_FILEMODE_BLOB)
         tree_oid = tb.write()
         repo.create_commit(
-            'refs/bugs/%s' % BUG_ID, sig, sig, 'op pack', tree_oid, [],
+            'refs/bugs/%s' % BUG_ID,
+            sig,
+            sig,
+            'op pack',
+            tree_oid,
+            [],
         )
 
     def test_no_filter(self, reader: BugReader, repo_path: str) -> None:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index dcbbc6c..963f635 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -4,6 +4,7 @@ These tests create ephemeral git repositories, initialize git-bug
 with a test identity, and exercise the full ezgb stack end-to-end.
 Skip automatically when ``git-bug`` is not installed.
 """
+
 import json
 import shutil
 import subprocess
@@ -29,20 +30,34 @@ def gb_repo(tmp_path: Path) -> GitBugRepo:
 
     def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
         return subprocess.run(
-            args, capture_output=True, text=True, check=True,
+            args,
+            capture_output=True,
+            text=True,
+            check=True,
         )
 
     # Initialise the git repo
     _run(['git', 'init', repo_path])
     _run(['git', '-C', repo_path, 'config', 'user.name', 'Test User'])
-    _run(['git', '-C', repo_path, 'config', 'user.email',
-          'test@example.com'])
+    _run(['git', '-C', repo_path, 'config', 'user.email', 'test@example.com'])
     _run(['git', '-C', repo_path, 'commit', '--allow-empty', '-m', 'init'])
 
     # Create and adopt a git-bug identity
-    _run(['git', '-C', repo_path, 'bug', 'user', 'new',
-          '-n', 'Test User', '-e', 'test@example.com',
-          '--non-interactive'])
+    _run(
+        [
+            'git',
+            '-C',
+            repo_path,
+            'bug',
+            'user',
+            'new',
+            '-n',
+            'Test User',
+            '-e',
+            'test@example.com',
+            '--non-interactive',
+        ]
+    )
     result = _run(['git', '-C', repo_path, 'bug', 'user', '-f', 'json'])
     users: JsonValue = json.loads(result.stdout)
     assert isinstance(users, list)
@@ -59,6 +74,7 @@ def gb_repo(tmp_path: Path) -> GitBugRepo:
 # Tests
 # ------------------------------------------------------------------
 
+
 class TestCreateAndRead:
     def test_round_trip(self, gb_repo: GitBugRepo) -> None:
         bug = gb_repo.create_bug('Test bug', 'Description text')
@@ -283,11 +299,13 @@ class TestAttachments:
         # blob. Use the ops blob itself as a stand-in.
         # Instead, test against an actual blob by writing one via git.
         import subprocess
+
         repo_path = gb_repo._repo
         result = subprocess.run(
             ['git', '-C', repo_path, 'hash-object', '-w', '--stdin'],
             input=b'test file content',
-            capture_output=True, check=True,
+            capture_output=True,
+            check=True,
         )
         blob_hash = result.stdout.decode().strip()
 

-- 
2.53.0


  parent reply	other threads:[~2026-04-20  4:39 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-20  4:39 [PATCH ezgb 0/6] Harden local CI checks Tamir Duberstein
2026-04-20  4:39 ` [PATCH ezgb 1/6] Add local CI script Tamir Duberstein
2026-04-20  4:39 ` [PATCH ezgb 2/6] Add mypy checks Tamir Duberstein
2026-04-20  4:39 ` Tamir Duberstein [this message]
2026-04-20  4:39 ` [PATCH ezgb 4/6] Add pyright checks Tamir Duberstein
2026-04-20  4:39 ` [PATCH ezgb 5/6] Add ty checks Tamir Duberstein
2026-04-20  4:39 ` [PATCH ezgb 6/6] Document local CI checks Tamir Duberstein
2026-04-27 20:20 ` [PATCH ezgb 0/6] Harden " Tamir Duberstein

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=20260419-stronger-type-checking-v1-3-222775b987e5@kernel.org \
    --to=tamird@kernel.org \
    --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