Skip to content

Commit 0d3defd

Browse files
committed
Fix Version.__eq__ crashing when compared with a non-Version object
The fallthrough guard only handled falsy operands, so comparing a Version to any truthy non-Version (a plain tuple with matching arity, a string, an int) raised AttributeError when trying to read other.tag. Replace the guard with an isinstance check returning False — Version equality is domain-specific (tag has precedence over branch/revision) and should not silently fall back to tuple equality. Add an explicit __hash__ to keep the class hashable under pyright's static rules. https://claude.ai/code/session_01KKvrvnVvsBChohuxbRRmzA
1 parent db1ac11 commit 0d3defd

3 files changed

Lines changed: 25 additions & 1 deletion

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Release 0.14.1 (unreleased)
22
===========================
33

44
* Fix ``dfetch import`` mangling the namespace of a generic VCS URL whose path contains ``.git`` other than as a suffix
5+
* Fix ``Version`` comparison raising ``AttributeError`` when compared against a non-``Version`` object
56

67
Release 0.14.0 (released 2026-06-14)
78
===========================

dfetch/manifest/version.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ class Version(NamedTuple):
1616

1717
def __eq__(self, other: Any) -> bool:
1818
"""Check if two versions can be considered as equal."""
19-
if not other:
19+
if not isinstance(other, Version):
2020
return False
2121

2222
if self.tag or other.tag:
2323
return bool(self.tag == other.tag)
2424

2525
return bool(self.branch == other.branch and self.revision == other.revision)
2626

27+
def __hash__(self) -> int:
28+
"""Hash based on the underlying tuple value."""
29+
return tuple.__hash__(self)
30+
2731
@property
2832
def field(self) -> tuple[str, str]:
2933
"""Return ``(kind, value)`` for the active field: tag, revision, or branch."""

tests/test_project_version.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,22 @@
202202
def test_version_comparison(name, version_1, version_2, expected_equality):
203203
actual_equality = version_1 == version_2
204204
assert actual_equality == expected_equality
205+
206+
207+
@pytest.mark.parametrize(
208+
"other",
209+
[
210+
("", "main", "123"), # a plain tuple
211+
"main", # a string
212+
42, # an int
213+
],
214+
)
215+
def test_version_comparison_with_non_version(other):
216+
"""Comparing a Version with a non-Version must not crash."""
217+
assert (Version(branch="main", revision="123") == other) is False
218+
219+
220+
def test_version_remains_hashable():
221+
"""Defining __eq__ must not break hashing/set membership."""
222+
version = Version(tag="1.0")
223+
assert version in {version}

0 commit comments

Comments
 (0)