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
+ """
2
7
3
8
import enum
4
9
import logging
10
+ import re
5
11
from dataclasses import dataclass
6
12
from functools import wraps
7
13
from typing import ClassVar
16
22
logger = logging .getLogger (__name__ )
17
23
18
24
19
- def _is_python_package (path ):
25
+ STUB_HEADER_COMMENT = "# File generated with docstub"
26
+
27
+
28
+ def is_python_package (path ):
20
29
"""
21
30
Parameters
22
31
----------
@@ -30,8 +39,31 @@ def _is_python_package(path):
30
39
return is_package
31
40
32
41
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.
35
67
36
68
Parameters
37
69
----------
@@ -43,26 +75,24 @@ def walk_source(root_dir):
43
75
source_path : Path
44
76
Either a Python file or a stub file that takes precedence.
45
77
"""
46
- queue = [root_dir ]
47
- while queue :
48
- path = queue .pop (0 )
49
-
78
+ for path in root_dir .iterdir ():
50
79
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 )
53
82
else :
54
- logger .debug ("skipping directory %s" , path )
83
+ logger .debug ("skipping directory %s which isn't a Python package " , path )
55
84
continue
56
85
57
86
assert path .is_file ()
58
-
59
87
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
64
88
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
66
96
67
97
68
98
def walk_source_and_targets (root_dir , target_dir ):
@@ -75,18 +105,14 @@ def walk_source_and_targets(root_dir, target_dir):
75
105
target_dir : Path
76
106
Root directory in which a matching stub package will be created.
77
107
78
- Returns
79
- -------
108
+ Yields
109
+ ------
80
110
source_path : Path
81
111
Either a Python file or a stub file that takes precedence.
82
112
stub_path : Path
83
113
Target stub file.
84
-
85
- Notes
86
- -----
87
- Files starting with "test_" are skipped entirely for now.
88
114
"""
89
- for source_path in walk_source (root_dir ):
115
+ for source_path in walk_python_package (root_dir ):
90
116
stub_path = target_dir / source_path .with_suffix (".pyi" ).relative_to (root_dir )
91
117
yield source_path , stub_path
92
118
0 commit comments