forked from robotframework/robotframework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimporter.py
More file actions
316 lines (262 loc) · 12.4 KB
/
Copy pathimporter.py
File metadata and controls
316 lines (262 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import inspect
from importlib import invalidate_caches as invalidate_import_caches
from robot.errors import DataError
from .encoding import system_decode
from .error import get_error_details
from .robotpath import abspath, normpath
from .robotinspect import is_init
from .robottypes import type_name, is_unicode
class Importer:
"""Utility that can import modules and classes based on names and paths.
Imported classes can optionally be instantiated automatically.
"""
def __init__(self, type=None, logger=None):
"""
:param type:
Type of the thing being imported. Used in error and log messages.
:param logger:
Logger to be notified about successful imports and other events.
Currently only needs the ``info`` method, but other level specific
methods may be needed in the future. If not given, logging is disabled.
"""
self._type = type or ''
self._logger = logger or NoLogger()
self._importers = (ByPathImporter(logger),
NonDottedImporter(logger),
DottedImporter(logger))
self._by_path_importer = self._importers[0]
def import_class_or_module(self, name_or_path, instantiate_with_args=None,
return_source=False):
"""Imports Python class or module based on the given name or path.
:param name_or_path:
Name or path of the module or class to import.
:param instantiate_with_args:
When arguments are given, imported classes are automatically initialized
using them.
:param return_source:
When true, returns a tuple containing the imported module or class
and a path to it. By default returns only the imported module or class.
The class or module to import can be specified either as a name, in which
case it must be in the module search path, or as a path to the file or
directory implementing the module. See :meth:`import_class_or_module_by_path`
for more information about importing classes and modules by path.
Classes can be imported from the module search path using name like
``modulename.ClassName``. If the class name and module name are same, using
just ``CommonName`` is enough. When importing a class by a path, the class
name and the module name must match.
Optional arguments to use when creating an instance are given as a list.
Starting from Robot Framework 4.0, both positional and named arguments are
supported (e.g. ``['positional', 'name=value']``) and arguments are converted
automatically based on type hints and default values.
If arguments needed when creating an instance are initially embedded into
the name or path like ``Example:arg1:arg2``, separate
:func:`~robot.utils.text.split_args_from_name_or_path` function can be
used to split them before calling this method.
"""
try:
imported, source = self._import_class_or_module(name_or_path)
self._log_import_succeeded(imported, name_or_path, source)
imported = self._instantiate_if_needed(imported, instantiate_with_args)
except DataError as err:
self._raise_import_failed(name_or_path, err)
else:
return self._handle_return_values(imported, source, return_source)
def _import_class_or_module(self, name):
for importer in self._importers:
if importer.handles(name):
return importer.import_(name)
def _handle_return_values(self, imported, source, return_source=False):
if not return_source:
return imported
if source and os.path.exists(source):
source = self._sanitize_source(source)
return imported, source
def _sanitize_source(self, source):
source = normpath(source)
if os.path.isdir(source):
candidate = os.path.join(source, '__init__.py')
elif source.endswith('.pyc'):
candidate = source[:-4] + '.py'
else:
return source
return candidate if os.path.exists(candidate) else source
def import_class_or_module_by_path(self, path, instantiate_with_args=None):
"""Import a Python module or class using a file system path.
:param path:
Path to the module or class to import.
:param instantiate_with_args:
When arguments are given, imported classes are automatically initialized
using them.
When importing a Python file, the path must end with :file:`.py` and the
actual file must also exist.
Use :meth:`import_class_or_module` to support importing also using name,
not only path. See the documentation of that function for more information
about creating instances automatically.
"""
try:
imported, source = self._by_path_importer.import_(path)
self._log_import_succeeded(imported, imported.__name__, source)
return self._instantiate_if_needed(imported, instantiate_with_args)
except DataError as err:
self._raise_import_failed(path, err)
def _log_import_succeeded(self, item, name, source):
import_type = '%s ' % self._type.lower() if self._type else ''
item_type = 'module' if inspect.ismodule(item) else 'class'
location = ("'%s'" % source) if source else 'unknown location'
self._logger.info("Imported %s%s '%s' from %s."
% (import_type, item_type, name, location))
def _raise_import_failed(self, name, error):
import_type = '%s ' % self._type.lower() if self._type else ''
msg = "Importing %s'%s' failed: %s" % (import_type, name, error.message)
if not error.details:
raise DataError(msg)
msg = [msg, error.details]
msg.extend(self._get_items_in('PYTHONPATH', sys.path))
raise DataError('\n'.join(msg))
def _get_items_in(self, type, items):
yield '%s:' % type
for item in items:
if item:
yield ' %s' % (item if is_unicode(item)
else system_decode(item))
def _instantiate_if_needed(self, imported, args):
if args is None:
return imported
if inspect.isclass(imported):
return self._instantiate_class(imported, args)
if args:
raise DataError("Modules do not take arguments.")
return imported
def _instantiate_class(self, imported, args):
spec = self._get_arg_spec(imported)
try:
positional, named = spec.resolve(args)
except ValueError as err:
raise DataError(err.args[0])
try:
return imported(*positional, **dict(named))
except:
raise DataError('Creating instance failed: %s\n%s' % get_error_details())
def _get_arg_spec(self, imported):
# Avoid cyclic import. Yuck.
from robot.running.arguments import ArgumentSpec, PythonArgumentParser
init = getattr(imported, '__init__', None)
name = imported.__name__
if not is_init(init):
return ArgumentSpec(name, self._type)
return PythonArgumentParser(self._type).parse(init, name)
class _Importer:
def __init__(self, logger):
self._logger = logger
def _import(self, name, fromlist=None):
if name in sys.builtin_module_names:
raise DataError('Cannot import custom module with same name as '
'Python built-in module.')
invalidate_import_caches()
try:
return __import__(name, fromlist=fromlist)
except:
raise DataError(*get_error_details())
def _verify_type(self, imported):
if inspect.isclass(imported) or inspect.ismodule(imported):
return imported
raise DataError('Expected class or module, got %s.'
% type_name(imported))
def _get_class_from_module(self, module, name=None):
klass = getattr(module, name or module.__name__, None)
return klass if inspect.isclass(klass) else None
def _get_source(self, imported):
try:
source = inspect.getfile(imported)
except TypeError:
return None
return abspath(source) if source else None
class ByPathImporter(_Importer):
_valid_import_extensions = ('.py', '')
def handles(self, path):
return os.path.isabs(path)
def import_(self, path):
self._verify_import_path(path)
self._remove_wrong_module_from_sys_modules(path)
module = self._import_by_path(path)
imported = self._get_class_from_module(module) or module
return self._verify_type(imported), path
def _verify_import_path(self, path):
if not os.path.exists(path):
raise DataError('File or directory does not exist.')
if not os.path.isabs(path):
raise DataError('Import path must be absolute.')
if not os.path.splitext(path)[1] in self._valid_import_extensions:
raise DataError('Not a valid file or directory to import.')
def _remove_wrong_module_from_sys_modules(self, path):
importing_from, name = self._split_path_to_module(path)
importing_package = os.path.splitext(path)[1] == ''
if self._wrong_module_imported(name, importing_from, importing_package):
del sys.modules[name]
self._logger.info("Removed module '%s' from sys.modules to import "
"fresh module." % name)
def _split_path_to_module(self, path):
module_dir, module_file = os.path.split(abspath(path))
module_name = os.path.splitext(module_file)[0]
return module_dir, module_name
def _wrong_module_imported(self, name, importing_from, importing_package):
if name not in sys.modules:
return False
source = getattr(sys.modules[name], '__file__', None)
if not source: # play safe
return True
imported_from, imported_package = self._get_import_information(source)
return (normpath(importing_from, case_normalize=True) !=
normpath(imported_from, case_normalize=True) or
importing_package != imported_package)
def _get_import_information(self, source):
imported_from, imported_file = self._split_path_to_module(source)
imported_package = imported_file == '__init__'
if imported_package:
imported_from = os.path.dirname(imported_from)
return imported_from, imported_package
def _import_by_path(self, path):
module_dir, module_name = self._split_path_to_module(path)
sys.path.insert(0, module_dir)
try:
return self._import(module_name)
finally:
sys.path.remove(module_dir)
class NonDottedImporter(_Importer):
def handles(self, name):
return '.' not in name
def import_(self, name):
module = self._import(name)
imported = self._get_class_from_module(module) or module
return self._verify_type(imported), self._get_source(imported)
class DottedImporter(_Importer):
def handles(self, name):
return '.' in name
def import_(self, name):
parent_name, lib_name = name.rsplit('.', 1)
parent = self._import(parent_name, fromlist=[str(lib_name)])
try:
imported = getattr(parent, lib_name)
except AttributeError:
raise DataError("Module '%s' does not contain '%s'."
% (parent_name, lib_name))
imported = self._get_class_from_module(imported, lib_name) or imported
return self._verify_type(imported), self._get_source(imported)
class NoLogger:
error = warn = info = debug = trace = lambda self, *args, **kws: None