Skip to content

[flake8-pyi] Ignore 'unused' private type dicts in class scopes#9952

Merged
charliermarsh merged 2 commits intomainfrom
charlie/private
Feb 12, 2024
Merged

[flake8-pyi] Ignore 'unused' private type dicts in class scopes#9952
charliermarsh merged 2 commits intomainfrom
charlie/private

Conversation

@charliermarsh
Copy link
Member

@charliermarsh charliermarsh commented Feb 12, 2024

Summary

If these are defined within class scopes, they're actually attributes of the class, and can be accessed through the class itself.

(We preserve our existing behavior for .pyi files.)

Closes #9948.

@charliermarsh charliermarsh marked this pull request as ready for review February 12, 2024 15:09
@charliermarsh charliermarsh added the rule Implementing or modifying a lint rule label Feb 12, 2024
@charliermarsh charliermarsh changed the title Ignore 'unused' private type dicts in class scopes [flake8-pyi] Ignore 'unused' private type dicts in class scopes Feb 12, 2024
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe this rule should work differently for .py and .pyi files.

For a .py file, it makes sense not to flag something like this since, as you say, it's an attribute and maybe it's used elsewhere:

class Foo:
    class _Unused(TypeDict):
        pass

But in a .pyi file, I'd definitely still want that flagged. (I'm not sure why you'd do something like that in a .pyi file... but I still feel like the principle is important 😄)

In a .pyi file, it's still highly likely that the TypedDict class there doesn't exist at runtime, even though it's inside the class scope. One of the heuristics these rules rely on (which in general is nearly always accurate) is the idea that TypedDicts, protocols, type aliases and protocols in stub files are nearly always "stub-only constructs" that don't exist at runtime, and are used purely for improving the annotations that we're able to supply in the stubs. I think that heuristic applies just as well to private definitions inside class scopes as it does to private definitions in the global scope.

@charliermarsh
Copy link
Member Author

Ok, I'll gate it by type. Makes sense.

@charliermarsh
Copy link
Member Author

Fixed.

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I think there would still be a false positive if you did something like this in a .pyi file:

class Foo:
    class _VeryPrivate(TypedDict):
        x: str

    bar: _VeryPrivate

But it seems pretty unlikely to actually come up. And I think this PR is probably an improvement as-is since it gets rid of the .py-file false positive (and I think it's probably correct to not worry about any of these rules inside class scopes in .py files). So I'm happy with this to go in as-is.

Comment on lines +40 to +46
# Error
class _CustomClass3:
class _UnusedTypeDict4(TypedDict):
pass

def f(self) -> None:
print(_CustomClass3._UnusedTypeDict4())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe get rid of the print() call in the .pyi fixture here; I'm not sure it adds anything

Suggested change
# Error
class _CustomClass3:
class _UnusedTypeDict4(TypedDict):
pass
def f(self) -> None:
print(_CustomClass3._UnusedTypeDict4())
# Error
# (in .pyi files, we flag unused definitions in class scopes
# as well as the global scope, unlike in .py files)
class _CustomClass3:
class _UnusedTypeDict4(TypedDict):
pass

@AlexWaygood
Copy link
Member

AlexWaygood commented Feb 12, 2024

I think there would still be a false positive if you did something like this in a .pyi file:

class Foo:
    class _VeryPrivate(TypedDict):
        x: str

    bar: _VeryPrivate

Hmm, no, it looks like that isn't flagged as being unused -- sorry, I should have checked. I think I might have slightly misunderstood the root cause of this issue here, in that case

@AlexWaygood
Copy link
Member

Ah, is the cause that the visitor recurses into class scopes when looking for references that might count as "usages", but does not recurse into method definitions inside class scopes?

In that case, we should definitely be fine, since the bodies of methods in .pyi files should always be empty except for ...

@charliermarsh
Copy link
Member Author

The root cause is that given:

class A:
    class _B(TypedDict):
        pass

    def f(self) -> None:
        print(A._B())

Our semantic model doesn't currently detect that A._B() is a usage of _B, since we don't follow attribute references.

But given:

class Foo:
    class _VeryPrivate(TypedDict):
        x: str

    bar: _VeryPrivate

We can resolve _VeryPrivate to the inner class correctly.

@AlexWaygood
Copy link
Member

AlexWaygood commented Feb 12, 2024

Makes sense. I think this should be fine, then, since accessing private attributes in general is very rare in .pyi files, so the false-positive as it was reported in the issue is very unlikely to occur for .pyi files.

Sorry for the slightly confused review!

@charliermarsh charliermarsh merged commit e2785f3 into main Feb 12, 2024
@charliermarsh charliermarsh deleted the charlie/private branch February 12, 2024 17:06
nkxxll pushed a commit to nkxxll/ruff that referenced this pull request Mar 10, 2024
…tral-sh#9952)

## Summary

If these are defined within class scopes, they're actually attributes of
the class, and can be accessed through the class itself.

(We preserve our existing behavior for `.pyi` files.)

Closes astral-sh#9948.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PYI049: false positive for private inner class

2 participants