diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 8901efad56f79..8e47b1ffb5b76 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -116,6 +116,19 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then fi RET=$(($RET + $?)) ; echo $MSG "DONE" + MSG='Check for use of private functions across modules' ; echo $MSG + if [[ "$GITHUB_ACTIONS" == "true" ]]; then + $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --format="##[error]{source_path}:{line_number}:{msg}" pandas/core/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --format="##[error]{source_path}:{line_number}:{msg}" pandas/tseries/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + else + $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" pandas/core/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" pandas/tseries/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + fi + echo "isort --version-number" isort --version-number diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index b476ab5a818c5..d49c3c3246b77 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -16,7 +16,7 @@ import sys import token import tokenize -from typing import IO, Callable, Iterable, List, Tuple +from typing import IO, Callable, Iterable, List, Set, Tuple FILE_EXTENSIONS_TO_CHECK: Tuple[str, ...] = (".py", ".pyx", ".pxi.ini", ".pxd") PATHS_TO_IGNORE: Tuple[str, ...] = ("asv_bench/env",) @@ -115,6 +115,55 @@ def bare_pytest_raises(file_obj: IO[str]) -> Iterable[Tuple[int, str]]: ) +def private_function_across_module(file_obj: IO[str]) -> Iterable[Tuple[int, str]]: + """ + Checking that a private function is not used across modules. + + Parameters + ---------- + file_obj : IO + File-like object containing the Python code to validate. + + Yields + ------ + line_number : int + Line number of the private function that is used across modules. + msg : str + Explenation of the error. + """ + contents = file_obj.read() + tree = ast.parse(contents) + + imported_modules: Set[str] = set() + + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + for module in node.names: + module_fqdn = module.name if module.asname is None else module.asname + imported_modules.add(module_fqdn) + + if not isinstance(node, ast.Call): + continue + + try: + module_name = node.func.value.id + function_name = node.func.attr + except AttributeError: + continue + + ### Exception section ### + + # (Debatable) Class case + if module_name[0].isupper(): + continue + # (Debatable) Dunder methods case + elif function_name.startswith("__") and function_name.endswith("__"): + continue + + if module_name in imported_modules and function_name.startswith("_"): + yield (node.lineno, f"Private function '{module_name}.{function_name}'") + + def strings_to_concatenate(file_obj: IO[str]) -> Iterable[Tuple[int, str]]: """ This test case is necessary after 'Black' (https://github.com/psf/black), @@ -358,6 +407,7 @@ def main( if __name__ == "__main__": available_validation_types: List[str] = [ "bare_pytest_raises", + "private_function_across_module", "strings_to_concatenate", "strings_with_wrong_placed_whitespace", ]