1- """Transform Python source files to typed stub files."""
1+ """Transform Python source files to typed stub files.
2+
3+ Attributes
4+ ----------
5+ STUB_HEADER_COMMENT : Final[str]
6+ """
27
38import enum
49import logging
10+ import re
511from dataclasses import dataclass
612from functools import wraps
713from typing import ClassVar
1622logger = logging .getLogger (__name__ )
1723
1824
19- def _is_python_package (path ):
25+ STUB_HEADER_COMMENT = "# File generated with docstub"
26+
27+
28+ def is_python_package (path ):
2029 """
2130 Parameters
2231 ----------
@@ -30,8 +39,31 @@ def _is_python_package(path):
3039 return is_package
3140
3241
33- def walk_source (root_dir ):
34- """Iterate modules in a Python package and its target stub files.
42+ def is_docstub_generated (path ):
43+ """Check if the stub file was generated by docstub.
44+
45+ Parameters
46+ ----------
47+ path : Path
48+
49+ Returns
50+ -------
51+ is_generated : bool
52+ """
53+ assert path .suffix == ".pyi"
54+ with path .open ("r" ) as fo :
55+ content = fo .read ()
56+ if re .match (f"^{ re .escape (STUB_HEADER_COMMENT )} " , content ):
57+ return True
58+ return False
59+
60+
61+ def walk_python_package (root_dir ):
62+ """Iterate source files in a Python package.
63+
64+ Given a Python package, yield the path of contained Python modules. If an
65+ alternate stub file already exists and isn't generated by docstub, it is
66+ returned instead.
3567
3668 Parameters
3769 ----------
@@ -43,26 +75,24 @@ def walk_source(root_dir):
4375 source_path : Path
4476 Either a Python file or a stub file that takes precedence.
4577 """
46- queue = [root_dir ]
47- while queue :
48- path = queue .pop (0 )
49-
78+ for path in root_dir .iterdir ():
5079 if path .is_dir ():
51- if _is_python_package (path ):
52- queue . extend (path . iterdir () )
80+ if is_python_package (path ):
81+ yield from walk_python_package (path )
5382 else :
54- logger .debug ("skipping directory %s" , path )
83+ logger .debug ("skipping directory %s which isn't a Python package " , path )
5584 continue
5685
5786 assert path .is_file ()
58-
5987 suffix = path .suffix .lower ()
60- if suffix not in {".py" , ".pyi" }:
61- continue
62- if suffix == ".py" and path .with_suffix (".pyi" ).exists ():
63- continue # Stub file already exists and takes precedence
6488
65- yield path
89+ if suffix == ".py" :
90+ stub = path .with_suffix (".pyi" )
91+ if stub .exists () and not is_docstub_generated (stub ):
92+ # Non-generated stub file already exists and takes precedence
93+ yield stub
94+ else :
95+ yield path
6696
6797
6898def walk_source_and_targets (root_dir , target_dir ):
@@ -75,18 +105,14 @@ def walk_source_and_targets(root_dir, target_dir):
75105 target_dir : Path
76106 Root directory in which a matching stub package will be created.
77107
78- Returns
79- -------
108+ Yields
109+ ------
80110 source_path : Path
81111 Either a Python file or a stub file that takes precedence.
82112 stub_path : Path
83113 Target stub file.
84-
85- Notes
86- -----
87- Files starting with "test_" are skipped entirely for now.
88114 """
89- for source_path in walk_source (root_dir ):
115+ for source_path in walk_python_package (root_dir ):
90116 stub_path = target_dir / source_path .with_suffix (".pyi" ).relative_to (root_dir )
91117 yield source_path , stub_path
92118
0 commit comments