16
16
import sys
17
17
import token
18
18
import tokenize
19
- from typing import IO , Callable , FrozenSet , Iterable , List , Tuple
20
-
21
- PATHS_TO_IGNORE : Tuple [str , ...] = ("asv_bench/env" ,)
19
+ from typing import IO , Callable , FrozenSet , Iterable , List , Set , Tuple
20
+
21
+ PRIVATE_IMPORTS_TO_IGNORE : Set [str ] = {
22
+ "_extension_array_shared_docs" ,
23
+ "_index_shared_docs" ,
24
+ "_merge_doc" ,
25
+ "_shared_docs" ,
26
+ "_new_Index" ,
27
+ "_new_PeriodIndex" ,
28
+ "_doc_template" ,
29
+ "_interval_shared_docs" ,
30
+ "_apply_docs" ,
31
+ "_arith_doc_FRAME" ,
32
+ "_flex_comp_doc_FRAME" ,
33
+ "_make_flex_doc" ,
34
+ "_op_descriptions" ,
35
+ "_pipe_template" ,
36
+ "_testing" ,
37
+ "_test_decorators" ,
38
+ }
22
39
23
40
24
41
def _get_literal_string_prefix_len (token_string : str ) -> int :
@@ -114,6 +131,83 @@ def bare_pytest_raises(file_obj: IO[str]) -> Iterable[Tuple[int, str]]:
114
131
)
115
132
116
133
134
+ def private_function_across_module (file_obj : IO [str ]) -> Iterable [Tuple [int , str ]]:
135
+ """
136
+ Checking that a private function is not used across modules.
137
+ Parameters
138
+ ----------
139
+ file_obj : IO
140
+ File-like object containing the Python code to validate.
141
+ Yields
142
+ ------
143
+ line_number : int
144
+ Line number of the private function that is used across modules.
145
+ msg : str
146
+ Explenation of the error.
147
+ """
148
+ contents = file_obj .read ()
149
+ tree = ast .parse (contents )
150
+
151
+ imported_modules : Set [str ] = set ()
152
+
153
+ for node in ast .walk (tree ):
154
+ if isinstance (node , (ast .Import , ast .ImportFrom )):
155
+ for module in node .names :
156
+ module_fqdn = module .name if module .asname is None else module .asname
157
+ imported_modules .add (module_fqdn )
158
+
159
+ if not isinstance (node , ast .Call ):
160
+ continue
161
+
162
+ try :
163
+ module_name = node .func .value .id
164
+ function_name = node .func .attr
165
+ except AttributeError :
166
+ continue
167
+
168
+ # Exception section #
169
+
170
+ # (Debatable) Class case
171
+ if module_name [0 ].isupper ():
172
+ continue
173
+ # (Debatable) Dunder methods case
174
+ elif function_name .startswith ("__" ) and function_name .endswith ("__" ):
175
+ continue
176
+
177
+ if module_name in imported_modules and function_name .startswith ("_" ):
178
+ yield (node .lineno , f"Private function '{ module_name } .{ function_name } '" )
179
+
180
+
181
+ def private_import_across_module (file_obj : IO [str ]) -> Iterable [Tuple [int , str ]]:
182
+ """
183
+ Checking that a private function is not imported across modules.
184
+ Parameters
185
+ ----------
186
+ file_obj : IO
187
+ File-like object containing the Python code to validate.
188
+ Yields
189
+ ------
190
+ line_number : int
191
+ Line number of import statement, that imports the private function.
192
+ msg : str
193
+ Explenation of the error.
194
+ """
195
+ contents = file_obj .read ()
196
+ tree = ast .parse (contents )
197
+
198
+ for node in ast .walk (tree ):
199
+ if not (isinstance (node , ast .Import ) or isinstance (node , ast .ImportFrom )):
200
+ continue
201
+
202
+ for module in node .names :
203
+ module_name = module .name .split ("." )[- 1 ]
204
+ if module_name in PRIVATE_IMPORTS_TO_IGNORE :
205
+ continue
206
+
207
+ if module_name .startswith ("_" ):
208
+ yield (node .lineno , f"Import of internal function { repr (module_name )} " )
209
+
210
+
117
211
def strings_to_concatenate (file_obj : IO [str ]) -> Iterable [Tuple [int , str ]]:
118
212
"""
119
213
This test case is necessary after 'Black' (https://github.com/psf/black),
@@ -293,6 +387,7 @@ def main(
293
387
source_path : str ,
294
388
output_format : str ,
295
389
file_extensions_to_check : str ,
390
+ excluded_file_paths : str ,
296
391
) -> bool :
297
392
"""
298
393
Main entry point of the script.
@@ -305,6 +400,10 @@ def main(
305
400
Source path representing path to a file/directory.
306
401
output_format : str
307
402
Output format of the error message.
403
+ file_extensions_to_check : str
404
+ Coma seperated values of what file extensions to check.
405
+ excluded_file_paths : str
406
+ Coma seperated values of what file paths to exclude during the check.
308
407
309
408
Returns
310
409
-------
@@ -325,6 +424,7 @@ def main(
325
424
FILE_EXTENSIONS_TO_CHECK : FrozenSet [str ] = frozenset (
326
425
file_extensions_to_check .split ("," )
327
426
)
427
+ PATHS_TO_IGNORE = frozenset (excluded_file_paths .split ("," ))
328
428
329
429
if os .path .isfile (source_path ):
330
430
file_path = source_path
@@ -362,6 +462,8 @@ def main(
362
462
if __name__ == "__main__" :
363
463
available_validation_types : List [str ] = [
364
464
"bare_pytest_raises" ,
465
+ "private_function_across_module" ,
466
+ "private_import_across_module" ,
365
467
"strings_to_concatenate" ,
366
468
"strings_with_wrong_placed_whitespace" ,
367
469
]
@@ -387,7 +489,12 @@ def main(
387
489
parser .add_argument (
388
490
"--included-file-extensions" ,
389
491
default = "py,pyx,pxd,pxi" ,
390
- help = "Coma seperated file extensions to check." ,
492
+ help = "Comma seperated file extensions to check." ,
493
+ )
494
+ parser .add_argument (
495
+ "--excluded-file-paths" ,
496
+ default = "asv_bench/env" ,
497
+ help = "Comma separated file extensions to check." ,
391
498
)
392
499
393
500
args = parser .parse_args ()
@@ -398,5 +505,6 @@ def main(
398
505
source_path = args .path ,
399
506
output_format = args .format ,
400
507
file_extensions_to_check = args .included_file_extensions ,
508
+ excluded_file_paths = args .excluded_file_paths ,
401
509
)
402
510
)
0 commit comments