11
11
from pathlib import Path
12
12
13
13
import tomli
14
+ import yaml
14
15
from packaging .requirements import Requirement
16
+ from packaging .specifiers import SpecifierSet
15
17
from packaging .version import Version
16
18
17
19
metadata_keys = {"version" , "requires" , "extra_description" , "obsolete_since" , "no_longer_updated" , "tool" }
18
20
tool_keys = {"stubtest" : {"skip" , "apt_dependencies" , "extras" , "ignore_missing_stub" }}
21
+ extension_descriptions = {".pyi" : "stub" , ".py" : ".py" }
19
22
20
23
21
- def assert_stubs_only (directory : Path , allowed : set [str ]) -> None :
22
- """Check that given directory contains only valid stub files."""
24
+ def assert_consistent_filetypes (directory : Path , * , kind : str , allowed : set [str ]) -> None :
25
+ """Check that given directory contains only valid Python files of a certain kind ."""
23
26
allowed_paths = {Path (f ) for f in allowed }
24
27
contents = list (directory .iterdir ())
25
28
while contents :
@@ -28,15 +31,16 @@ def assert_stubs_only(directory: Path, allowed: set[str]) -> None:
28
31
# Note if a subdirectory is allowed, we will not check its contents
29
32
continue
30
33
if entry .is_file ():
31
- assert entry .stem .isidentifier (), f"Files must be valid modules, got: { entry } "
32
- assert entry .suffix == ".pyi" , f"Only stub files allowed, got: { entry } "
34
+ assert entry .stem .isidentifier (), f'Files must be valid modules, got: "{ entry } "'
35
+ bad_filetype = f'Only { extension_descriptions [kind ]!r} files allowed in the "{ directory } " directory; got: { entry } '
36
+ assert entry .suffix == kind , bad_filetype
33
37
else :
34
38
assert entry .name .isidentifier (), f"Directories must be valid packages, got: { entry } "
35
39
contents .extend (entry .iterdir ())
36
40
37
41
38
42
def check_stdlib () -> None :
39
- assert_stubs_only (Path ("stdlib" ), allowed = {"_typeshed/README.md" , "VERSIONS" })
43
+ assert_consistent_filetypes (Path ("stdlib" ), kind = ".pyi" , allowed = {"_typeshed/README.md" , "VERSIONS" })
40
44
41
45
42
46
def check_stubs () -> None :
@@ -46,14 +50,21 @@ def check_stubs() -> None:
46
50
valid_dist_name = "^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$" # courtesy of PEP 426
47
51
assert re .fullmatch (
48
52
valid_dist_name , dist .name , re .IGNORECASE
49
- ), f"Directory name must have valid distribution name: { dist } "
53
+ ), f"Directory name must be a valid distribution name: { dist } "
50
54
assert not dist .name .startswith ("types-" ), f"Directory name not allowed to start with 'types-': { dist } "
51
55
52
56
allowed = {"METADATA.toml" , "README" , "README.md" , "README.rst" , "@tests" }
53
- assert_stubs_only (dist , allowed )
57
+ assert_consistent_filetypes (dist , kind = ".pyi" , allowed = allowed )
54
58
55
59
56
- def check_same_files () -> None :
60
+ def check_test_cases () -> None :
61
+ assert_consistent_filetypes (Path ("test_cases" ), kind = ".py" , allowed = {"README.md" })
62
+ bad_test_case_filename = 'Files in the `test_cases` directory must have names starting with "test_"; got "{}"'
63
+ for file in Path ("test_cases" ).rglob ("*.py" ):
64
+ assert file .stem .startswith ("test_" ), bad_test_case_filename .format (file )
65
+
66
+
67
+ def check_no_symlinks () -> None :
57
68
files = [os .path .join (root , file ) for root , _ , files in os .walk ("." ) for file in files ]
58
69
no_symlink = "You cannot use symlinks in typeshed, please copy {} to its link."
59
70
for file in files :
@@ -65,12 +76,16 @@ def check_same_files() -> None:
65
76
_VERSIONS_RE = re .compile (r"^([a-zA-Z_][a-zA-Z0-9_.]*): [23]\.\d{1,2}-(?:[23]\.\d{1,2})?$" )
66
77
67
78
79
+ def strip_comments (text : str ) -> str :
80
+ return text .split ("#" )[0 ].strip ()
81
+
82
+
68
83
def check_versions () -> None :
69
84
versions = set ()
70
85
with open ("stdlib/VERSIONS" ) as f :
71
86
data = f .read ().splitlines ()
72
87
for line in data :
73
- line = line . split ( "#" )[ 0 ]. strip ( )
88
+ line = strip_comments ( line )
74
89
if line == "" :
75
90
continue
76
91
m = _VERSIONS_RE .match (line )
@@ -126,10 +141,48 @@ def check_metadata() -> None:
126
141
assert key in tk , f"Unrecognised { tool } key { key } for { distribution } "
127
142
128
143
144
+ def get_txt_requirements () -> dict [str , SpecifierSet ]:
145
+ with open ("requirements-tests.txt" ) as requirements_file :
146
+ stripped_lines = map (strip_comments , requirements_file )
147
+ requirements = map (Requirement , filter (None , stripped_lines ))
148
+ return {requirement .name : requirement .specifier for requirement in requirements }
149
+
150
+
151
+ def get_precommit_requirements () -> dict [str , SpecifierSet ]:
152
+ with open (".pre-commit-config.yaml" ) as precommit_file :
153
+ precommit = precommit_file .read ()
154
+ yam = yaml .load (precommit , Loader = yaml .Loader )
155
+ precommit_requirements = {}
156
+ for repo in yam ["repos" ]:
157
+ hook = repo ["hooks" ][0 ]
158
+ package_name , package_rev = hook ["id" ], repo ["rev" ]
159
+ package_specifier = SpecifierSet (f"=={ package_rev .removeprefix ('v' )} " )
160
+ precommit_requirements [package_name ] = package_specifier
161
+ for additional_req in hook .get ("additional_dependencies" , []):
162
+ req = Requirement (additional_req )
163
+ precommit_requirements [req .name ] = req .specifier
164
+ return precommit_requirements
165
+
166
+
167
+ def check_requirements () -> None :
168
+ requirements_txt_requirements = get_txt_requirements ()
169
+ precommit_requirements = get_precommit_requirements ()
170
+ no_txt_entry_msg = "All pre-commit requirements must also be listed in `requirements-tests.txt` (missing {requirement!r})"
171
+ for requirement , specifier in precommit_requirements .items ():
172
+ assert requirement in requirements_txt_requirements , no_txt_entry_msg .format (requirement )
173
+ specifier_mismatch = (
174
+ f'Specifier "{ specifier } " for { requirement !r} in `.pre-commit-config.yaml` '
175
+ f'does not match specifier "{ requirements_txt_requirements [requirement ]} " in `requirements-tests.txt`'
176
+ )
177
+ assert specifier == requirements_txt_requirements [requirement ], specifier_mismatch
178
+
179
+
129
180
if __name__ == "__main__" :
130
181
assert sys .version_info >= (3 , 9 ), "Python 3.9+ is required to run this test"
131
182
check_stdlib ()
132
183
check_versions ()
133
184
check_stubs ()
134
185
check_metadata ()
135
- check_same_files ()
186
+ check_no_symlinks ()
187
+ check_test_cases ()
188
+ check_requirements ()
0 commit comments