From 64f1319f0bb147d61cb080f07acd5f416d9c680e Mon Sep 17 00:00:00 2001
From: Arthur Zamarin <arthurzam@gentoo.org>
Date: Fri, 7 Aug 2026 10:31:59 +0300
Subject: [PATCH] conditionals: finalize the empty DepSet default

DepSet.__init__ defaulted restrictions to "", so every empty depset -
what ebuild_src returns for a package lacking REQUIRED_USE, BDEPEND or
IDEPEND - held a str rather than a tuple. boolean.base.__hash__ reads
that as "not finalized" and raises TypeError, making those depsets
permanently unhashable and reporting finalized=False in their repr.

Nothing hashed them until 0.12.37 put an lru_cache on the compiled
REQUIRED_USE constraints, keyed on the restriction. From then on
find_constraint_satisfaction() blew up on any package without a
REQUIRED_USE, which broke `pkgdev tatt` for most of the tree:

    File "pkgcore/restrictions/boolean.py", line 37, in __hash__
      raise TypeError(f"{self!r} isn't finalized")
  TypeError: <DepSet negate=False type='package' finalized=False
             restrictions='' ...> isn't finalized

Default to an empty tuple instead. str(), len(), iteration and __eq__
(which compares set(restrictions)) are unchanged for the empty case, so
the only difference is that the depset is now finalized, hence hashable.

Reported-by: Eli Schwartz <eschwartz@gentoo.org>
Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
---
 NEWS.rst                                | 15 +++++++++++++++
 src/pkgcore/ebuild/conditionals.py      |  2 +-
 tests/ebuild/test_conditionals.py       |  5 +++++
 tests/restrictions/test_required_use.py |  9 +++++++++
 4 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/NEWS.rst b/NEWS.rst
index cbbeb9450..6cdf28118 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,21 @@
 Release Notes
 =============
 
+----------------------------
+pkgcore 0.12.38 (unreleased)
+----------------------------
+
+Fixes
+~~~~~
+
+- ``pkgcore.ebuild.conditionals``: the empty depset returned for packages
+  lacking ``REQUIRED_USE``, ``BDEPEND`` or ``IDEPEND`` is now finalized, and
+  thus hashable again. Since caching the compiled REQUIRED_USE constraints in
+  0.12.37 requires hashing the depset, this broke ``pkgdev tatt`` with a
+  ``TypeError`` for every package without ``REQUIRED_USE``
+  (Arthur Zamarin)
+
+
 ----------------------------
 pkgcore 0.12.37 (2026-07-31)
 ----------------------------
diff --git a/src/pkgcore/ebuild/conditionals.py b/src/pkgcore/ebuild/conditionals.py
index 201fa9387..166a10866 100644
--- a/src/pkgcore/ebuild/conditionals.py
+++ b/src/pkgcore/ebuild/conditionals.py
@@ -30,7 +30,7 @@ class DepSet(boolean.AndRestriction, caching=False):
 
     def __init__(
         self,
-        restrictions="",
+        restrictions=(),
         element_class=atom,
         node_conds=True,
         known_conditionals=None,
diff --git a/tests/ebuild/test_conditionals.py b/tests/ebuild/test_conditionals.py
index f310f9c59..cc62fa9d5 100644
--- a/tests/ebuild/test_conditionals.py
+++ b/tests/ebuild/test_conditionals.py
@@ -220,6 +220,11 @@ def test_disabling_or(self):
     def test_atom_interaction(self):
         self.gen_depset("a/b[x(+)]", element_func=atom)
 
+    def test_empty_depset(self):
+        # an empty depset must be finalized, thus hashable
+        assert not conditionals.DepSet().restrictions
+        assert hash(conditionals.DepSet()) == hash(conditionals.DepSet())
+
 
 class TestDepSetConditionalsInspection(base):
     def test_sanity_has_conditionals(self):
diff --git a/tests/restrictions/test_required_use.py b/tests/restrictions/test_required_use.py
index 84168a21d..46d339402 100644
--- a/tests/restrictions/test_required_use.py
+++ b/tests/restrictions/test_required_use.py
@@ -19,6 +19,15 @@ def test_simple():
     assert tuple(solver(required_use, {"bar", "foo"})) == ({"bar": True, "foo": True},)
 
 
+def test_empty():
+    # packages without REQUIRED_USE get an empty depset, which the constraint
+    # cache must still be able to key on
+    required_use = parse(required_use="")
+    solutions = tuple(solver(required_use, {"bar", "foo"}))
+    assert len(solutions) == 4
+    assert {"bar": True, "foo": True} in solutions
+
+
 def test_negative_simple():
     required_use = parse(required_use="!bar foo")
     assert tuple(solver(required_use, {"bar", "foo"})) == ({"bar": False, "foo": True},)
