Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give Pyright what it wants (alias attributes everywhere) #3114

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions newsfragments/3114.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure that Pyright recognizes our underscore prefixed attributes for attrs classes.
4 changes: 2 additions & 2 deletions src/trio/_core/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class RunVar(Generic[T]):

"""

_name: str
_default: T | type[_NoValue] = _NoValue
_name: str = attrs.field(alias="name")
_default: T | type[_NoValue] = attrs.field(default=_NoValue, alias="default")

def get(self, default: T | type[_NoValue] = _NoValue) -> T:
"""Gets the value of this :class:`RunVar` for the current run call."""
Expand Down
10 changes: 7 additions & 3 deletions src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,13 @@ class CancelScope:
cancelled_caught: bool = attrs.field(default=False, init=False)

# Constructor arguments:
_relative_deadline: float = attrs.field(default=inf, kw_only=True)
_deadline: float = attrs.field(default=inf, kw_only=True)
_shield: bool = attrs.field(default=False, kw_only=True)
_relative_deadline: float = attrs.field(
default=inf,
kw_only=True,
alias="relative_deadline",
)
_deadline: float = attrs.field(default=inf, kw_only=True, alias="deadline")
_shield: bool = attrs.field(default=False, kw_only=True, alias="shield")

def __attrs_post_init__(self) -> None:
if isnan(self._deadline):
Expand Down
94 changes: 94 additions & 0 deletions src/trio/_tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import socket as stdlib_socket
import sys
import tokenize
import types
from pathlib import Path, PurePath
from types import ModuleType
Expand Down Expand Up @@ -572,3 +573,96 @@ def test_classes_are_final() -> None:
continue

assert class_is_final(class_)


def test_pyright_recognizes_init_attributes() -> None:
"""Check whether we provide `alias` for all underscore prefixed attributes

We cannot check this at runtime, as attrs sets the `alias` attribute on
fields, but instead we can reconstruct the source code of the class and
check that. Unfortunately, `inspect.getsourcelines` does not work so we
need to build up this source code ourself.

The approach taken here is:
1. read every file that could contain the classes in question
2. tokenize them, for a couple reasons:
- tokenization unlike ast parsing can be 1-1 undone
- tokenization allows us to get the whole class block
- tokenization allows us to find ``class {name}`` without prefix
matches
3. for every exported class:
1. find the file
2. isolate the class block
3. undo tokenization
4. find the string ``alias="{what it should be}"``
"""
files = []

parent = (Path(inspect.getfile(trio)) / "..").resolve()
for path in parent.glob("**/*.py"):
if "_tests" in str(path)[len(str(parent)) :]:
continue

with open(path, "rb") as f:
files.append(list(tokenize.tokenize(f.readline)))

for module in PUBLIC_MODULES:
for name, class_ in module.__dict__.items():
if not attrs.has(class_):
continue
if isinstance(class_, _util.NoPublicConstructor):
continue

file = None
start = None
for contents in files:
last_was_class = False
for i, token in enumerate(contents):
if (
token.type == tokenize.NAME
and token.string == name
and last_was_class
):
assert file is None
file = contents
start = i - 1

if token.type == tokenize.NAME and token.string == "class":
last_was_class = True
else:
last_was_class = False

assert file is not None, f"{name}: {class_!r}"
A5rocks marked this conversation as resolved.
Show resolved Hide resolved
assert start is not None

count = -1
end_offset = 0
for end_offset, token in enumerate( # noqa: B007
file[start:],
): # pragma: no branch
if token.type == tokenize.INDENT:
count += 1
if token.type == tokenize.DEDENT and count:
count -= 1
elif token.type == tokenize.DEDENT:
break

assert token.type == tokenize.DEDENT
class_source = (
tokenize.untokenize(file[start : start + end_offset])
.replace("\\\n", "")
.strip()
)

attributes = list(attrs.fields(class_))
attributes = [attr for attr in attributes if attr.name.startswith("_")]
attributes = [attr for attr in attributes if attr.init]

attributes = [
# could this be improved by parsing AST? yes. this is simpler though.
attr
for attr in attributes
if f'alias="{attr.alias}"' not in class_source
]

assert attributes == [], class_
Loading