Right, I would like to change this (assuming it’s amenable to uv, pip, etc. resolvers, which I don’t know the details of) to saying that both extra == and extra != are evaluated solely on the extras for this particular instance of the package in the dependency graph. If there is another instance somewhere else in the dependency graph with a different set of extras, then those markers are evaluated again on that set of extras, and the resulting requirements are joined additively, just as if they were two different packages.
But this also made me realize another discrepancy between the spec and the pip/uv implementations. Suppose you have
Requires-Dist: something; extra == "this" and extra == "that"
Intuitively, it seems like this should cause foo[this,that] to install something.
By the rules as you’ve described it, this would also cause foo[this] foo[that] (or some equivalent setup, like a dependency chain foo[this] → bar → foo[that]), to install something, because you’d be evaluating set membership in the list of all the accumulated extras for every time we encounter foo in the dependency graph.
However, as best as I can tell, both pip and uv evaluate this condition to false i.e. never install something, which greatly surprises me!
For pip this is because the relevant code (src/pip/_internal/req/req_install.py) evaluates markers one at a time:
if self.markers is not None:
return any(
self.markers.evaluate({"extra": extra}) for extra in extras_requested
)
(See this DPO thread from 2020 about how packaging doesn’t provide an API for the actual thing you want here, which is set membership.) I don’t yet understand why uv has this behavior; a cursory look at the code (which I’m not very familiar with) implies it does try to do actual set membership instead of looping through the set and evaluating the markers one at a time.
Konsti or Mike, if it’s easy for you to do, I’d be curious if there are any /extra ==.*and extra ==/ requirements in the wild, because those are also broken.