Set matchers and the typing of get_qualified_name_for
- Dominant language
- Python
- Stars
- 1.9k
- Forks
- 229
- PR merge metrics
- No merged PRs in 30d
Description
I'll talk about our specific problem and then discuss the possibilities. There's also the chance that I've missed some obvious solution!
Our codebase is large enough that matcher lambdas can't be used (since they don't serialize; see #798), so `MatchMetadataIfTrue` and `MatchIfTrue` isn't available.
If a `Call` resolves to a given qualified name (say a particular imported method, like `unittest.mock.patch`), we'd like to apply a change to the `Call` node or maybe just print out some information about it. When the import is shadowed by conditional imports, (meaning multiple qualified names could be returned), we'd like to match if any of the qualified names match.
To do all of those things, we currently write some code with `get_metadata` and some type conversions like so:
```python
@m.visit(
m.Call(),
)
def print_mock_patch(self, node: cst.Call) -> None:
qualified_names_raw = self.get_metadata(QualifiedNameProvider, node)
qualified_names = set(qualified_names_raw)
if not qualified_names:
raise RuntimeError(
"A Call was found, but didn't resolve to a known name in the module. Fix that compile error first."
)
matches = [
qname.name == "unittest.mock.patch" # Or whatever
and qname.source == QualifiedNameSource.IMPORT
for qname in qualified_names
]
if not any(matches):
return
print("Found one")
```
The conversion from `Collection[QualifiedName]` to `Set` is unfortunate (and possibly buggy if I've misread the underlying code), and it's a non-trivial amount of code. It'd be nice if we could push this into existing matchers.
Unfortunately, using existing matchers doesn't work. As mentioned above, the usual `MatchMetadataIfTrue` breaks due to #798. The closet you could get would be a matcher that fails when the `Call` has more than one imported name. That is, this matcher:
```python
@m.visit(
m.Call(
metadata=m.MatchMetadata(
QualifiedNameProvider,
{
QualifiedName(
name="unittest.mock.patch",
source=QualifiedNameSource.IMPORT,
),
},
),
),
)
```
would fail to match in, say, a `typing.TYPE_CHECKING` case:
```python
if typing.TYPE_CHECKING:
from somewherelse import patch
else:
from unittest.mock import patch
```
Using `AtLeastN(n=1, matchers=QualifiedName(...)` will fail to match.
So, that's the specific problem.
It would be nice if a) there was a way to match on `Set`s, especially those returned by metadata providers and b) that `Scope.get_qualified_names_for` returned a Set like the types it calls do, and that `QualifiedNameProvider` and `FullyQualifiedNameProvider` returned `Set`s, as well.
But I could be told that there's some reason for not doing those things that I missed. (Including that there's a flaw in trying to names in this way that I've missed)
Contributor guide
Assessment
This issue has not been assessed yet.