3
3
Code adapted from the find-imports project, currently in graveyard archive.
4
4
"""
5
5
import ast
6
+ from inspect import Attribute
7
+ import os
8
+ import pkgutil
6
9
from pathlib import Path
7
- from collections import namedtuple
8
- from threading import local
10
+ from typing import Union
11
+ from collections import namedtuple , defaultdict
12
+ from itertools import filterfalse , chain
9
13
10
14
from ._supported_packages import PACKAGE_RENAMES , STANDARD_LIBRARY , PYODIDE_PACKAGES
11
15
@@ -14,58 +18,146 @@ class UnsupportedFileType(Exception):
14
18
pass
15
19
16
20
17
- Environment = namedtuple ("Environment" , ["packages" , "paths" ])
21
+ ImportInfo = namedtuple ("ImportInfo" , ["packages" , "paths" ])
22
+
23
+
24
+ class NamespaceInfo :
25
+ def __init__ (self , source_fpath : Path ) -> None :
26
+ # expanding base_folder to absolute as pkgutils.FileFinder will do so - easier for later purging
27
+ self .base_folder = str (source_fpath .parent .absolute ())
28
+ self .source_mod_name = source_fpath .stem
29
+ self ._collect ()
30
+ # storing this as it will be useful for multiple lookups
31
+ self ._all_namespace = set (chain (self .modules , self .packages ))
32
+
33
+ def _collect (self ):
34
+ iter_modules_paths = [self .base_folder ]
35
+ for root , dirs , files in os .walk (self .base_folder ):
36
+ for dirname in dirs :
37
+ iter_modules_paths .append (os .path .join (root , dirname ))
38
+
39
+ # need to consume generator as I will iterate two times for packages, and modules
40
+ pkg_mods = tuple (pkgutil .iter_modules (iter_modules_paths ))
41
+ modules = map (
42
+ lambda mi : os .path .join (mi .module_finder .path , mi .name ),
43
+ filterfalse (
44
+ lambda mi : mi .ispkg or mi .name == self .source_mod_name , pkg_mods
45
+ ),
46
+ )
47
+ packages = map (
48
+ lambda mi : os .path .join (mi .module_finder .path , mi .name ),
49
+ filter (lambda mi : mi .ispkg , pkg_mods ),
50
+ )
51
+ self .modules = set (map (self ._dotted_path , modules ))
52
+ self .packages = set (map (self ._dotted_path , packages ))
53
+
54
+ def _dotted_path (self , p : str ):
55
+ p = p .replace (self .base_folder , "" ).replace (os .path .sep , "." )
56
+ if p .startswith ("." ):
57
+ p = p [1 :]
58
+ return p
59
+
60
+ def __contains__ (self , item : str ) -> bool :
61
+ return item in self ._all_namespace
62
+
63
+ def __str__ (self ) -> str :
64
+ return f"NameSpace info for { self .base_folder } \n \t Modules: { self .modules } \n \t Packages: { self .packages } "
65
+
66
+ def __repr__ (self ) -> str :
67
+ return str (self )
68
+
69
+
70
+ class FinderResult :
71
+ def __init__ (self ) -> None :
72
+ self .packages = set ()
73
+ self .locals = set ()
74
+ self .unsupported = defaultdict (set )
75
+
76
+ def add_package (self , pkg_name : str ) -> None :
77
+ self .packages .add (pkg_name )
78
+
79
+ def add_locals (self , pkg_name : str ) -> None :
80
+ self .locals .add (pkg_name )
81
+
82
+ def add_unsupported_external_package (self , pkg_name : str ) -> None :
83
+ self .unsupported ["external" ].add (pkg_name )
84
+
85
+ def add_unsupported_local_package (self , pkg_name : str ) -> None :
86
+ self .unsupported ["local" ].add (pkg_name )
87
+
88
+ @property
89
+ def has_warnings (self ):
90
+ return len (self .unsupported ) > 0
91
+
92
+ @property
93
+ def unsupported_packages (self ):
94
+ return self .unsupported ["external" ]
95
+
96
+ @property
97
+ def unsupported_paths (self ):
98
+ return self .unsupported ["local" ]
99
+
18
100
19
101
# https://stackoverflow.com/a/58847554
20
102
class ModuleFinder (ast .NodeVisitor ):
21
- def __init__ (self , * args , ** kwargs ):
22
- self .packages = set ()
23
- self .other_modules = set ()
103
+ def __init__ (self , context : NamespaceInfo , * args , ** kwargs ):
104
+ # list of all potential local imports
105
+ self .context = context
106
+ self .results = FinderResult ()
24
107
super ().__init__ (* args , ** kwargs )
25
108
26
109
def visit_Import (self , node ):
27
110
for name in node .names :
28
- imported = name .name .split ("." )[0 ]
29
- self ._import_name (imported )
111
+ # need to check for absolute module import here as they won't work in PyScript
112
+ # absolute package imports will be found later in _import_name
113
+ if len (name .name .split ("." )) > 1 and name .name in self .context :
114
+ self .results .add_unsupported_local_package (name .name )
115
+ else :
116
+ self ._import_name (name .name )
30
117
31
118
def visit_ImportFrom (self , node ):
32
119
# if node.module is missing it's a "from . import ..." statement
33
120
# if level > 0 it's a "from .submodule import ..." statement
34
- if node .module is not None and node .level == 0 :
35
- imported = node .module .split ("." )[0 ]
36
- self ._import_name (imported )
121
+ if node .module is not None :
122
+ self ._import_name (node .module )
37
123
38
124
def _import_name (self , imported ):
39
- pkg_name = PACKAGE_RENAMES .get (imported , imported )
40
- if pkg_name not in STANDARD_LIBRARY :
41
- if pkg_name in PYODIDE_PACKAGES :
42
- self .packages .add (pkg_name )
125
+ if imported in self .context :
126
+ if imported not in self .context .packages :
127
+ self .results .add_locals (imported )
43
128
else :
44
- self .other_modules .add (pkg_name )
45
-
46
-
47
- def _find_modules (source : str , source_fpath : Path ) -> Environment :
129
+ self .results .add_unsupported_local_package (imported )
130
+ else :
131
+ imported = imported .split ("." )[0 ]
132
+ pkg_name = PACKAGE_RENAMES .get (imported , imported )
133
+ if pkg_name not in STANDARD_LIBRARY :
134
+ if pkg_name in PYODIDE_PACKAGES :
135
+ self .results .add_package (pkg_name )
136
+ else :
137
+ self .results .add_unsupported_external_package (pkg_name )
138
+
139
+
140
+ def _find_modules (source : str , source_fpath : Path ):
48
141
fname = source_fpath .name
142
+ # importing all local modules from source_fpath
143
+ namespace_info = NamespaceInfo (source_fpath = source_fpath )
49
144
# passing mode='exec' just in case defaults will change in the future
50
145
nodes = ast .parse (source , fname , mode = "exec" )
51
146
52
- finder = ModuleFinder ()
147
+ finder = ModuleFinder (context = namespace_info )
53
148
finder .visit (nodes )
54
- print ("Found modules: " , finder .packages , finder .other_modules )
55
- source_basefolder = source_fpath .parent
56
- local_mods = set (
57
- map (
58
- lambda p : Path (* p .parts [1 :]),
59
- filter (
60
- lambda d : d .stem in finder .other_modules
61
- and ((d .is_file () and d .suffix == ".py" ) or d .is_dir ()),
62
- source_basefolder .iterdir (),
63
- ),
64
- )
149
+ report = finder .results
150
+ pyenv_paths = map (
151
+ lambda l : "{}.py" .format (l .replace ("." , os .path .sep )), report .locals
152
+ )
153
+ pyenv = ImportInfo (packages = report .packages , paths = set (pyenv_paths ))
154
+ if not report .has_warnings :
155
+ return pyenv , None
156
+
157
+ warnings = ImportInfo (
158
+ packages = report .unsupported_packages , paths = report .unsupported_paths
65
159
)
66
- print ("local modules" , local_mods )
67
- print ("external mods" , finder .packages )
68
- return Environment (packages = finder .packages , paths = local_mods )
160
+ return pyenv , warnings
69
161
70
162
71
163
def _convert_notebook (source_fpath : Path ) -> str :
@@ -77,9 +169,11 @@ def _convert_notebook(source_fpath: Path) -> str:
77
169
return source
78
170
79
171
80
- def find_imports (source_fpath : Path ) -> Environment :
172
+ def find_imports (
173
+ source_fpath : Path ,
174
+ ) -> Union [ImportInfo , tuple [ImportInfo , ImportInfo ]]:
81
175
"""
82
- Parse the input source, and returns its dependencies, as organised in
176
+ Parse the input source, and returns its dependencies, as organised in
83
177
the sets of external packages, and local modules, respectively.
84
178
Any modules or package with the same name found in the local
85
179
@@ -90,8 +184,11 @@ def find_imports(source_fpath: Path) -> Environment:
90
184
91
185
Returns
92
186
-------
93
- tuple[set[str], set[str]]
94
- Pair of external modules, and local modules
187
+ Union[ImportInfo, tuple[ImportInfo, ImportInfo]]
188
+ The function returns an instance of `ImportInfo` containing the
189
+ environment with packages and paths to include in py-env.
190
+ Optionally, if the parsing detected unsupported packages and local modules,
191
+ this will be returned as well (still as `ImportInfo` instance)
95
192
"""
96
193
fname , extension = source_fpath .name , source_fpath .suffix
97
194
if extension == ".py" :
0 commit comments