Skip to content

Commit f21cbb0

Browse files
committed
Simplify alias test by monkeypatching attrs.
1 parent 819797a commit f21cbb0

File tree

3 files changed

+33
-80
lines changed

3 files changed

+33
-80
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ reportUnnecessaryTypeIgnoreComment = true
189189
typeCheckingMode = "strict"
190190

191191
[tool.pytest.ini_options]
192-
addopts = ["--strict-markers", "--strict-config", "-p trio._tests.pytest_plugin"]
192+
addopts = ["--strict-markers", "--strict-config", "-p _trio_check_attrs_aliases", "-p trio._tests.pytest_plugin"]
193193
faulthandler_timeout = 60
194194
filterwarnings = [
195195
"error",

src/_trio_check_attrs_aliases.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Conftest is executed by Pytest before test modules.
2+
3+
We use this to monkeypatch attrs.field(), so that we can detect if aliases are used for test_exports.
4+
"""
5+
6+
from typing import Any
7+
8+
import attrs
9+
10+
orig_field = attrs.field
11+
12+
13+
def field(**kwargs: Any) -> Any:
14+
if "alias" in kwargs:
15+
metadata = kwargs.setdefault("metadata", {})
16+
metadata["trio_test_has_alias"] = True
17+
return orig_field(**kwargs)
18+
19+
20+
field.trio_modded = True # type: ignore
21+
attrs.field = field

src/trio/_tests/test_exports.py

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import json
1010
import socket as stdlib_socket
1111
import sys
12-
import tokenize
1312
import types
1413
from pathlib import Path, PurePath
1514
from types import ModuleType
@@ -576,93 +575,26 @@ def test_classes_are_final() -> None:
576575

577576

578577
def test_pyright_recognizes_init_attributes() -> None:
579-
"""Check whether we provide `alias` for all underscore prefixed attributes
580-
581-
We cannot check this at runtime, as attrs sets the `alias` attribute on
582-
fields, but instead we can reconstruct the source code of the class and
583-
check that. Unfortunately, `inspect.getsourcelines` does not work so we
584-
need to build up this source code ourself.
585-
586-
The approach taken here is:
587-
1. read every file that could contain the classes in question
588-
2. tokenize them, for a couple reasons:
589-
- tokenization unlike ast parsing can be 1-1 undone
590-
- tokenization allows us to get the whole class block
591-
- tokenization allows us to find ``class {name}`` without prefix
592-
matches
593-
3. for every exported class:
594-
1. find the file
595-
2. isolate the class block
596-
3. undo tokenization
597-
4. find the string ``alias="{what it should be}"``
598-
"""
599-
files = []
600-
601-
parent = (Path(inspect.getfile(trio)) / "..").resolve()
602-
for path in parent.glob("**/*.py"):
603-
if "_tests" in str(path)[len(str(parent)) :]:
604-
continue
605-
606-
with open(path, "rb") as f:
607-
files.append(list(tokenize.tokenize(f.readline)))
578+
"""Check whether we provide `alias` for all underscore prefixed attributes.
608579
580+
Attrs always sets the `alias` attribute on fields, so a pytest plugin is used
581+
to monkeypatch `field()` to record whether an alias was defined in the metadata.
582+
See `_trio_check_attrs_aliases`.
583+
"""
584+
assert hasattr(attrs.field, "trio_modded")
609585
for module in PUBLIC_MODULES:
610-
for name, class_ in module.__dict__.items():
586+
for class_ in module.__dict__.values():
611587
if not attrs.has(class_):
612588
continue
613589
if isinstance(class_, _util.NoPublicConstructor):
614590
continue
615591

616-
file = None
617-
start = None
618-
for contents in files:
619-
last_was_class = False
620-
for i, token in enumerate(contents):
621-
if (
622-
token.type == tokenize.NAME
623-
and token.string == name
624-
and last_was_class
625-
):
626-
assert file is None
627-
file = contents
628-
start = i - 1
629-
630-
if token.type == tokenize.NAME and token.string == "class":
631-
last_was_class = True
632-
else:
633-
last_was_class = False
634-
635-
assert file is not None, f"{name}: {class_!r}"
636-
assert start is not None
637-
638-
count = -1
639-
end_offset = 0
640-
for end_offset, token in enumerate( # noqa: B007
641-
file[start:],
642-
): # pragma: no branch
643-
if token.type == tokenize.INDENT:
644-
count += 1
645-
if token.type == tokenize.DEDENT and count:
646-
count -= 1
647-
elif token.type == tokenize.DEDENT:
648-
break
649-
650-
assert token.type == tokenize.DEDENT
651-
class_source = (
652-
tokenize.untokenize(file[start : start + end_offset])
653-
.replace("\\\n", "")
654-
.strip()
655-
)
656-
657-
attributes = list(attrs.fields(class_))
658-
attributes = [attr for attr in attributes if attr.name.startswith("_")]
659-
attributes = [attr for attr in attributes if attr.init]
660-
661592
attributes = [
662-
# could this be improved by parsing AST? yes. this is simpler though.
663593
attr
664-
for attr in attributes
665-
if f'alias="{attr.alias}"' not in class_source
594+
for attr in attrs.fields(class_)
595+
if attr.name.startswith("_")
596+
if attr.init
597+
if "trio_test_has_alias" not in attr.metadata
666598
]
667599

668600
assert attributes == [], class_

0 commit comments

Comments
 (0)