From 0cfaa007d7894e0b21f9762161c4ccd00573842d Mon Sep 17 00:00:00 2001 From: Gaven Lan Ren Date: Wed, 28 Oct 2026 14:17:57 -0400 Subject: [PATCH 1/2] fix: support index protocol in IterableList lookups `IterableList.__getitem__` advertises `SupportsIndex`, but routes every non-`int` index through named lookup. Valid objects implementing `__index__` therefore raise a string-concatenation `TypeError` instead of selecting an element. Delegate non-string, non-slice indices to the built-in list implementation, which already handles the index protocol and bounds checks. Keep named lookup and explicit slice rejection unchanged. Six protocol/bounds regressions fail before this fix; a lookup/slice control passes. Utility and typing modules pass with both CLI and Gix backends (128 passed, 39 skipped each). Applicable pre-commit checks pass. Assisted-by: OpenAI Codex (model: unknown, autonomous) --- git/util.py | 6 +++--- test/test_util.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/git/util.py b/git/util.py index 5170592ab..d9c1480d3 100644 --- a/git/util.py +++ b/git/util.py @@ -1334,10 +1334,10 @@ def __getattr__(self, attr: str) -> T_IterableObj: def __getitem__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] self, index: Union[SupportsIndex, int, slice, str] ) -> T_IterableObj: - if isinstance(index, int): - return list.__getitem__(self, index) - elif isinstance(index, slice): + if isinstance(index, slice): raise ValueError("Index should be an int or str") + elif not isinstance(index, str): + return list.__getitem__(self, index) else: try: return self.__getattr__(cast(str, index)) diff --git a/test/test_util.py b/test/test_util.py index 0adbd0b2d..45c870777 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,7 +5,6 @@ import contextlib import errno -from datetime import datetime import os import pathlib import pickle @@ -13,6 +12,7 @@ import sys import threading import time +from datetime import datetime from unittest import SkipTest, mock import ddt @@ -43,8 +43,8 @@ rmtree, ) from test.cleanup import cleanup_directory -from test.lib.fixtures import writable_fixture from test.lib import TestBase, requires_symlinks, with_rw_repo +from test.lib.fixtures import writable_fixture @pytest.mark.parametrize( @@ -312,6 +312,45 @@ def test_altz_to_utctz_str_accepts_float_seconds(offset, expected): assert utctz_to_altz(result) == utctz_to_altz(expected) +@pytest.mark.parametrize("prefix", ["", "refs/"]) +@pytest.mark.parametrize("position", [0, -1]) +def test_iterable_list_accepts_index_protocol(prefix, position): + class Index: + def __index__(self): + return position + + values = IterableList("name", prefix) + members = [_Member(prefix + name) for name in ("one", "two")] + values.extend(members) + assert values[Index()] is members[position] + + +@pytest.mark.parametrize("position", [2, -3]) +def test_iterable_list_index_protocol_preserves_bounds_errors(position): + class Index: + def __index__(self): + return position + + values = IterableList("name") + values.extend([_Member("one"), _Member("two")]) + with pytest.raises(IndexError): + values[Index()] + + +def test_iterable_list_index_protocol_preserves_named_lookup_and_slice_rejection(): + values = IterableList("name", "refs/") + named = _Member("refs/0") + values.extend([named, _Member("refs/one")]) + assert values["0"] is named + assert values[False] is named + with pytest.raises(IndexError): + values["missing"] + with pytest.raises(ValueError): + values[:1] + with pytest.raises(TypeError): + values[0.0] + + @ddt.ddt class TestUtils(TestBase): """Tests for most utilities in :mod:`git.util`.""" From 451a52643e9b8682395e8d7f045ddeff8315f246 Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 29 Oct 2026 17:48:58 +0100 Subject: [PATCH 2/2] review --- git/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index d9c1480d3..9877ab8a2 100644 --- a/git/util.py +++ b/git/util.py @@ -1340,7 +1340,7 @@ def __getitem__( # type: ignore[override] # pyright: ignore[reportIncompatible return list.__getitem__(self, index) else: try: - return self.__getattr__(cast(str, index)) + return self.__getattr__(index) except AttributeError as e: raise IndexError(f"No item found with id {self._prefix}{index}") from e # END handle getattr