Skip to content

Commit 3a5e9fd

Browse files
committed
STY: check for private imports/lookups
1 parent 6c3c695 commit 3a5e9fd

File tree

3 files changed

+131
-4
lines changed

3 files changed

+131
-4
lines changed

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,14 @@ doc:
2525
cd doc; \
2626
python make.py clean; \
2727
python make.py html
28+
29+
check:
30+
python3 scripts/validate_unwanted_patterns.py \
31+
--validation-type="private_import_across_module" \
32+
--included-file-extensions="py" pandas/ \
33+
--excluded-file-paths=pandas/tests/,asv_bench/
34+
35+
python3 scripts/validate_unwanted_patterns.py \
36+
--validation-type="private_function_across_module" \
37+
--included-file-extensions="py" pandas/ \
38+
--excluded-file-paths=pandas/tests/,asv_bench/

ci/code_checks.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then
116116
fi
117117
RET=$(($RET + $?)) ; echo $MSG "DONE"
118118

119+
MSG='Check for use of private functions across modules' ; echo $MSG
120+
if [[ "$GITHUB_ACTIONS" == "true" ]]; then
121+
$BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --included-file-extensions="py" --format="##[error]{source_path}:{line_number}:{msg}" pandas/
122+
else
123+
$BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --included-file-extensions="py" pandas/
124+
fi
125+
RET=$(($RET + $?)) ; echo $MSG "DONE"
126+
119127
echo "isort --version-number"
120128
isort --version-number
121129

scripts/validate_unwanted_patterns.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,26 @@
1616
import sys
1717
import token
1818
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+
}
2239

2340

2441
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]]:
114131
)
115132

116133

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+
117211
def strings_to_concatenate(file_obj: IO[str]) -> Iterable[Tuple[int, str]]:
118212
"""
119213
This test case is necessary after 'Black' (https://github.com/psf/black),
@@ -293,6 +387,7 @@ def main(
293387
source_path: str,
294388
output_format: str,
295389
file_extensions_to_check: str,
390+
excluded_file_paths: str,
296391
) -> bool:
297392
"""
298393
Main entry point of the script.
@@ -305,6 +400,10 @@ def main(
305400
Source path representing path to a file/directory.
306401
output_format : str
307402
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.
308407
309408
Returns
310409
-------
@@ -325,6 +424,7 @@ def main(
325424
FILE_EXTENSIONS_TO_CHECK: FrozenSet[str] = frozenset(
326425
file_extensions_to_check.split(",")
327426
)
427+
PATHS_TO_IGNORE = frozenset(excluded_file_paths.split(","))
328428

329429
if os.path.isfile(source_path):
330430
file_path = source_path
@@ -362,6 +462,8 @@ def main(
362462
if __name__ == "__main__":
363463
available_validation_types: List[str] = [
364464
"bare_pytest_raises",
465+
"private_function_across_module",
466+
"private_import_across_module",
365467
"strings_to_concatenate",
366468
"strings_with_wrong_placed_whitespace",
367469
]
@@ -387,7 +489,12 @@ def main(
387489
parser.add_argument(
388490
"--included-file-extensions",
389491
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.",
391498
)
392499

393500
args = parser.parse_args()
@@ -398,5 +505,6 @@ def main(
398505
source_path=args.path,
399506
output_format=args.format,
400507
file_extensions_to_check=args.included_file_extensions,
508+
excluded_file_paths=args.excluded_file_paths,
401509
)
402510
)

0 commit comments

Comments
 (0)