diff --git a/modules/python/src2/copy_typings_stubs_on_success.py b/modules/python/src2/copy_typings_stubs_on_success.py index efcf84ee82..eb5a2e4e6c 100644 --- a/modules/python/src2/copy_typings_stubs_on_success.py +++ b/modules/python/src2/copy_typings_stubs_on_success.py @@ -13,6 +13,21 @@ else: from distutils.dir_util import copy_tree +def _remove_stale_pyi_files(directory): + """Remove .pyi files and py.typed markers from the directory tree. + + During incremental builds, disabling a previously enabled module leaves + stale typing stubs in the loader directory from a previous copy. Since + copy_tree merges rather than replaces, those stale files persist. + Removing all stub files before copying ensures only stubs for currently + enabled modules are present. Runtime .py files are not affected. + """ + for dirpath, dirnames, filenames in os.walk(directory): + for fname in filenames: + if fname.endswith('.pyi') or fname == 'py.typed': + os.remove(os.path.join(dirpath, fname)) + + def main(): args = parse_arguments() py_typed_path = os.path.join(args.stubs_dir, 'py.typed') @@ -24,6 +39,8 @@ def main(): 'generation phase.'.format(py_typed_path) ) return + if os.path.isdir(args.output_dir): + _remove_stale_pyi_files(args.output_dir) copy_tree(args.stubs_dir, args.output_dir) diff --git a/modules/python/src2/typing_stubs_generation/generation.py b/modules/python/src2/typing_stubs_generation/generation.py index 07de26c0af..f27f64df0a 100644 --- a/modules/python/src2/typing_stubs_generation/generation.py +++ b/modules/python/src2/typing_stubs_generation/generation.py @@ -3,6 +3,7 @@ __all__ = ("generate_typing_stubs", ) from io import StringIO from pathlib import Path import re +import shutil from typing import (Callable, NamedTuple, Union, Set, Dict, Collection, Tuple, List) import warnings @@ -24,6 +25,22 @@ from .nodes.type_node import (TypeNode, AliasTypeNode, AliasRefTypeNode, ConditionalAliasTypeNode, PrimitiveTypeNode) +def _clean_stale_stubs_dirs(stubs_root: Path) -> None: + """Remove all subdirectories under stubs_root. + + During incremental builds, disabling a previously enabled module leaves + behind its typing stub directory (e.g. cv2/gapi/). Removing all + subdirectories before regeneration ensures only stubs for currently + enabled modules are present. Top-level files (py.typed, __init__.pyi) + are kept because they are managed separately. + """ + if not stubs_root.is_dir(): + return + for item in stubs_root.iterdir(): + if item.is_dir(): + shutil.rmtree(item) + + def generate_typing_stubs(root: NamespaceNode, output_path: Path): """Generates typing stubs for the AST with root `root` and outputs created files tree to directory pointed by `output_path`. @@ -88,6 +105,12 @@ def generate_typing_stubs(root: NamespaceNode, output_path: Path): # The whole process should fail !only! when all possible scopes are # checked and at least 1 node is still unresolved. root.resolve_type_nodes() + # Remove stale typing stub subdirectories from previous builds. + # In incremental builds, disabling a module (e.g. -DBUILD_opencv_gapi=OFF) + # no longer generates its stubs, but leftover directories from a previous + # build persist and propagate through the copy/install steps, causing + # type-checker errors for stubs referencing unavailable modules. + _clean_stale_stubs_dirs(Path(output_path) / root.export_name) _generate_typing_module(root, output_path) _populate_reexported_symbols(root) _generate_typing_stubs(root, output_path)