1
1
"""Routines for finding the sources that mypy will check"""
2
2
3
- import os .path
3
+ import functools
4
+ import os
4
5
5
- from typing import List , Sequence , Set , Tuple , Optional , Dict
6
+ from typing import List , Sequence , Set , Tuple , Optional
6
7
from typing_extensions import Final
7
8
8
- from mypy .modulefinder import BuildSource , PYTHON_EXTENSIONS
9
+ from mypy .modulefinder import BuildSource , PYTHON_EXTENSIONS , mypy_path
9
10
from mypy .fscache import FileSystemCache
10
11
from mypy .options import Options
11
12
@@ -24,7 +25,7 @@ def create_source_list(paths: Sequence[str], options: Options,
24
25
Raises InvalidSourceList on errors.
25
26
"""
26
27
fscache = fscache or FileSystemCache ()
27
- finder = SourceFinder (fscache )
28
+ finder = SourceFinder (fscache , options )
28
29
29
30
sources = []
30
31
for path in paths :
@@ -34,7 +35,7 @@ def create_source_list(paths: Sequence[str], options: Options,
34
35
name , base_dir = finder .crawl_up (path )
35
36
sources .append (BuildSource (path , name , None , base_dir ))
36
37
elif fscache .isdir (path ):
37
- sub_sources = finder .find_sources_in_dir (path , explicit_package_roots = None )
38
+ sub_sources = finder .find_sources_in_dir (path )
38
39
if not sub_sources and not allow_empty_dir :
39
40
raise InvalidSourceList (
40
41
"There are no .py[i] files in directory '{}'" .format (path )
@@ -58,112 +59,141 @@ def keyfunc(name: str) -> Tuple[int, str]:
58
59
return (- 1 , name )
59
60
60
61
62
+ def normalise_package_base (root : str ) -> str :
63
+ if not root :
64
+ root = os .curdir
65
+ root = os .path .normpath (os .path .abspath (root ))
66
+ if root .endswith (os .sep ):
67
+ root = root [:- 1 ]
68
+ return root
69
+
70
+
71
+ def get_explicit_package_bases (options : Options ) -> Optional [List [str ]]:
72
+ if not options .explicit_package_bases :
73
+ return None
74
+ roots = mypy_path () + options .mypy_path + [os .getcwd ()]
75
+ return [normalise_package_base (root ) for root in roots ]
76
+
77
+
61
78
class SourceFinder :
62
- def __init__ (self , fscache : FileSystemCache ) -> None :
79
+ def __init__ (self , fscache : FileSystemCache , options : Options ) -> None :
63
80
self .fscache = fscache
64
- # A cache for package names, mapping from directory path to module id and base dir
65
- self .package_cache = {} # type: Dict[str, Tuple[str, str]]
66
-
67
- def find_sources_in_dir (
68
- self , path : str , explicit_package_roots : Optional [List [str ]]
69
- ) -> List [BuildSource ]:
70
- if explicit_package_roots is None :
71
- mod_prefix , root_dir = self .crawl_up_dir (path )
72
- else :
73
- mod_prefix = os .path .basename (path )
74
- root_dir = os .path .dirname (path ) or "."
75
- if mod_prefix :
76
- mod_prefix += "."
77
- return self .find_sources_in_dir_helper (path , mod_prefix , root_dir , explicit_package_roots )
78
-
79
- def find_sources_in_dir_helper (
80
- self , dir_path : str , mod_prefix : str , root_dir : str ,
81
- explicit_package_roots : Optional [List [str ]]
82
- ) -> List [BuildSource ]:
83
- assert not mod_prefix or mod_prefix .endswith ("." )
84
-
85
- init_file = self .get_init_file (dir_path )
86
- # If the current directory is an explicit package root, explore it as such.
87
- # Alternatively, if we aren't given explicit package roots and we don't have an __init__
88
- # file, recursively explore this directory as a new package root.
89
- if (
90
- (explicit_package_roots is not None and dir_path in explicit_package_roots )
91
- or (explicit_package_roots is None and init_file is None )
92
- ):
93
- mod_prefix = ""
94
- root_dir = dir_path
81
+ self .explicit_package_bases = get_explicit_package_bases (options )
82
+ self .namespace_packages = options .namespace_packages
95
83
96
- seen = set () # type: Set[str]
97
- sources = []
84
+ def is_explicit_package_base (self , path : str ) -> bool :
85
+ assert self .explicit_package_bases
86
+ return normalise_package_base (path ) in self .explicit_package_bases
98
87
99
- if init_file :
100
- sources . append ( BuildSource ( init_file , mod_prefix . rstrip ( "." ), None , root_dir ))
88
+ def find_sources_in_dir ( self , path : str ) -> List [ BuildSource ] :
89
+ sources = []
101
90
102
- names = self . fscache . listdir ( dir_path )
103
- names . sort ( key = keyfunc )
91
+ seen = set () # type: Set[str]
92
+ names = sorted ( self . fscache . listdir ( path ), key = keyfunc )
104
93
for name in names :
105
94
# Skip certain names altogether
106
95
if name == '__pycache__' or name .startswith ('.' ) or name .endswith ('~' ):
107
96
continue
108
- path = os .path .join (dir_path , name )
97
+ subpath = os .path .join (path , name )
109
98
110
- if self .fscache .isdir (path ):
111
- sub_sources = self .find_sources_in_dir_helper (
112
- path , mod_prefix + name + '.' , root_dir , explicit_package_roots
113
- )
99
+ if self .fscache .isdir (subpath ):
100
+ sub_sources = self .find_sources_in_dir (subpath )
114
101
if sub_sources :
115
102
seen .add (name )
116
103
sources .extend (sub_sources )
117
104
else :
118
105
stem , suffix = os .path .splitext (name )
119
- if stem == '__init__' :
120
- continue
121
- if stem not in seen and '.' not in stem and suffix in PY_EXTENSIONS :
106
+ if stem not in seen and suffix in PY_EXTENSIONS :
122
107
seen .add (stem )
123
- src = BuildSource ( path , mod_prefix + stem , None , root_dir )
124
- sources .append (src )
108
+ module , base_dir = self . crawl_up ( subpath )
109
+ sources .append (BuildSource ( subpath , module , None , base_dir ) )
125
110
126
111
return sources
127
112
128
113
def crawl_up (self , path : str ) -> Tuple [str , str ]:
129
- """Given a .py[i] filename, return module and base directory
114
+ """Given a .py[i] filename, return module and base directory.
130
115
131
- We crawl up the path until we find a directory without
132
- __init__.py[i], or until we run out of path components.
116
+ For example, given "xxx/yyy/foo/bar.py", we might return something like:
117
+ ("foo.bar", "xxx/yyy")
118
+
119
+ If namespace packages is off, we crawl upwards until we find a directory without
120
+ an __init__.py
121
+
122
+ If namespace packages is on, we crawl upwards until the nearest explicit base directory.
123
+ Failing that, we return one past the highest directory containing an __init__.py
124
+
125
+ We won't crawl past directories with invalid package names.
126
+ The base directory returned is an absolute path.
133
127
"""
128
+ path = os .path .normpath (os .path .abspath (path ))
134
129
parent , filename = os .path .split (path )
135
- module_name = strip_py (filename ) or os .path .basename (filename )
136
- module_prefix , base_dir = self .crawl_up_dir (parent )
137
- if module_name == '__init__' or not module_name :
138
- module = module_prefix
139
- else :
140
- module = module_join (module_prefix , module_name )
141
130
131
+ module_name = strip_py (filename ) or filename
132
+ if not module_name .isidentifier ():
133
+ return module_name , parent
134
+
135
+ parent_module , base_dir = self .crawl_up_dir (parent )
136
+ if module_name == "__init__" :
137
+ return parent_module , base_dir
138
+
139
+ module = module_join (parent_module , module_name )
142
140
return module , base_dir
143
141
144
142
def crawl_up_dir (self , dir : str ) -> Tuple [str , str ]:
145
- """Given a directory name, return the corresponding module name and base directory
143
+ return self . _crawl_up_helper ( dir ) or ( "" , dir )
146
144
147
- Use package_cache to cache results.
148
- """
149
- if dir in self .package_cache :
150
- return self .package_cache [dir ]
145
+ @functools .lru_cache ()
146
+ def _crawl_up_helper (self , dir : str ) -> Optional [Tuple [str , str ]]:
147
+ """Given a directory, maybe returns module and base directory.
151
148
152
- parent_dir , base = os .path .split (dir )
153
- if not dir or not self .get_init_file (dir ) or not base :
154
- module = ''
155
- base_dir = dir or '.'
156
- else :
157
- # Ensure that base is a valid python module name
158
- if base .endswith ('-stubs' ):
159
- base = base [:- 6 ] # PEP-561 stub-only directory
160
- if not base .isidentifier ():
161
- raise InvalidSourceList ('{} is not a valid Python package name' .format (base ))
162
- parent_module , base_dir = self .crawl_up_dir (parent_dir )
163
- module = module_join (parent_module , base )
164
-
165
- self .package_cache [dir ] = module , base_dir
166
- return module , base_dir
149
+ We return a non-None value if we were able to find something clearly intended as a base
150
+ directory (as adjudicated by being an explicit base directory or by containing a package
151
+ with __init__.py).
152
+
153
+ This distinction is necessary for namespace packages, so that we know when to treat
154
+ ourselves as a subpackage.
155
+ """
156
+ # stop crawling if we're an explicit base directory
157
+ if self .explicit_package_bases is not None and self .is_explicit_package_base (dir ):
158
+ return "" , dir
159
+
160
+ # stop crawling if we've exhausted path components
161
+ parent , name = os .path .split (dir )
162
+ if not name or not parent :
163
+ return None
164
+ if name .endswith ('-stubs' ):
165
+ name = name [:- 6 ] # PEP-561 stub-only directory
166
+
167
+ # recurse if there's an __init__.py
168
+ init_file = self .get_init_file (dir )
169
+ if init_file is not None :
170
+ if not name .isidentifier ():
171
+ # in most cases the directory name is invalid, we'll just stop crawling upwards
172
+ # but if there's an __init__.py in the directory, something is messed up
173
+ raise InvalidSourceList ("{} is not a valid Python package name" .format (name ))
174
+ # we're definitely a package, so we always return a non-None value
175
+ mod_prefix , base_dir = self .crawl_up_dir (parent )
176
+ return module_join (mod_prefix , name ), base_dir
177
+
178
+ # stop crawling if our name is an invalid identifier
179
+ if not name .isidentifier ():
180
+ return None
181
+
182
+ # stop crawling if namespace packages is off (and we don't have an __init__.py)
183
+ if not self .namespace_packages :
184
+ return None
185
+
186
+ # at this point: namespace packages is on, we don't have an __init__.py and we're not an
187
+ # explicit base directory
188
+ result = self ._crawl_up_helper (parent )
189
+ if result is None :
190
+ # we're not an explicit base directory and we don't have an __init__.py
191
+ # and none of our parents are either, so return
192
+ return None
193
+ # one of our parents was an explicit base directory or had an __init__.py, so we're
194
+ # definitely a subpackage! chain our name to the module.
195
+ mod_prefix , base_dir = result
196
+ return module_join (mod_prefix , name ), base_dir
167
197
168
198
def get_init_file (self , dir : str ) -> Optional [str ]:
169
199
"""Check whether a directory contains a file named __init__.py[i].
@@ -185,8 +215,7 @@ def module_join(parent: str, child: str) -> str:
185
215
"""Join module ids, accounting for a possibly empty parent."""
186
216
if parent :
187
217
return parent + '.' + child
188
- else :
189
- return child
218
+ return child
190
219
191
220
192
221
def strip_py (arg : str ) -> Optional [str ]:
0 commit comments