All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.