11"""Routines for finding the sources that mypy will check"""
22
3- import os .path
3+ import functools
4+ import os
45
5- from typing import List , Sequence , Set , Tuple , Optional , Dict
6+ from typing import List , Sequence , Set , Tuple , Optional
67from typing_extensions import Final
78
8- from mypy .modulefinder import BuildSource , PYTHON_EXTENSIONS
9+ from mypy .modulefinder import BuildSource , PYTHON_EXTENSIONS , mypy_path
910from mypy .fscache import FileSystemCache
1011from mypy .options import Options
1112
@@ -24,7 +25,7 @@ def create_source_list(paths: Sequence[str], options: Options,
2425 Raises InvalidSourceList on errors.
2526 """
2627 fscache = fscache or FileSystemCache ()
27- finder = SourceFinder (fscache )
28+ finder = SourceFinder (fscache , options )
2829
2930 sources = []
3031 for path in paths :
@@ -34,7 +35,7 @@ def create_source_list(paths: Sequence[str], options: Options,
3435 name , base_dir = finder .crawl_up (path )
3536 sources .append (BuildSource (path , name , None , base_dir ))
3637 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 )
3839 if not sub_sources and not allow_empty_dir :
3940 raise InvalidSourceList (
4041 "There are no .py[i] files in directory '{}'" .format (path )
@@ -58,112 +59,141 @@ def keyfunc(name: str) -> Tuple[int, str]:
5859 return (- 1 , name )
5960
6061
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+
6178class SourceFinder :
62- def __init__ (self , fscache : FileSystemCache ) -> None :
79+ def __init__ (self , fscache : FileSystemCache , options : Options ) -> None :
6380 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
9583
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
9887
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 = []
10190
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 )
10493 for name in names :
10594 # Skip certain names altogether
10695 if name == '__pycache__' or name .startswith ('.' ) or name .endswith ('~' ):
10796 continue
108- path = os .path .join (dir_path , name )
97+ subpath = os .path .join (path , name )
10998
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 )
114101 if sub_sources :
115102 seen .add (name )
116103 sources .extend (sub_sources )
117104 else :
118105 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 :
122107 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 ) )
125110
126111 return sources
127112
128113 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.
130115
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.
133127 """
128+ path = os .path .normpath (os .path .abspath (path ))
134129 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 )
141130
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 )
142140 return module , base_dir
143141
144142 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 )
146144
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.
151148
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
167197
168198 def get_init_file (self , dir : str ) -> Optional [str ]:
169199 """Check whether a directory contains a file named __init__.py[i].
@@ -185,8 +215,7 @@ def module_join(parent: str, child: str) -> str:
185215 """Join module ids, accounting for a possibly empty parent."""
186216 if parent :
187217 return parent + '.' + child
188- else :
189- return child
218+ return child
190219
191220
192221def strip_py (arg : str ) -> Optional [str ]:
0 commit comments