Robot Framework
importer.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import os
17 import sys
18 import importlib
19 import inspect
20 
21 from robot.errors import DataError
22 
23 from .error import get_error_details
24 from .robotpath import abspath, normpath
25 from .robotinspect import is_init
26 from .robottypes import type_name
27 
28 
29 
33 class Importer:
34 
35 
43  def __init__(self, type=None, logger=None):
44  self._type_type = type or ''
45  self._logger_logger = logger or NoLogger()
46  self._importers_importers = (ByPathImporter(logger),
47  NonDottedImporter(logger),
48  DottedImporter(logger))
49  self._by_path_importer_by_path_importer = self._importers_importers[0]
50 
51 
84  def import_class_or_module(self, name_or_path, instantiate_with_args=None,
85  return_source=False):
86  try:
87  imported, source = self._import_import(name_or_path)
88  self._log_import_succeeded_log_import_succeeded(imported, name_or_path, source)
89  imported = self._instantiate_if_needed_instantiate_if_needed(imported, instantiate_with_args)
90  except DataError as err:
91  self._raise_import_failed_raise_import_failed(name_or_path, err)
92  else:
93  return self._handle_return_values_handle_return_values(imported, source, return_source)
94 
95 
110  def import_module(self, name_or_path):
111  try:
112  imported, source = self._import_import(name_or_path, get_class=False)
113  self._log_import_succeeded_log_import_succeeded(imported, name_or_path, source)
114  except DataError as err:
115  self._raise_import_failed_raise_import_failed(name_or_path, err)
116  else:
117  return imported
118 
119  def _import(self, name, get_class=True):
120  for importer in self._importers_importers:
121  if importer.handles(name):
122  return importer.import_(name, get_class)
123 
124  def _handle_return_values(self, imported, source, return_source=False):
125  if not return_source:
126  return imported
127  if source and os.path.exists(source):
128  source = self._sanitize_source_sanitize_source(source)
129  return imported, source
130 
131  def _sanitize_source(self, source):
132  source = normpath(source)
133  if os.path.isdir(source):
134  candidate = os.path.join(source, '__init__.py')
135  elif source.endswith('.pyc'):
136  candidate = source[:-4] + '.py'
137  else:
138  return source
139  return candidate if os.path.exists(candidate) else source
140 
141 
156  def import_class_or_module_by_path(self, path, instantiate_with_args=None):
157  try:
158  imported, source = self._by_path_importer_by_path_importer.import_(path)
159  self._log_import_succeeded_log_import_succeeded(imported, imported.__name__, source)
160  return self._instantiate_if_needed_instantiate_if_needed(imported, instantiate_with_args)
161  except DataError as err:
162  self._raise_import_failed_raise_import_failed(path, err)
163 
164  def _log_import_succeeded(self, item, name, source):
165  import_type = '%s ' % self._type_type.lower() if self._type_type else ''
166  item_type = 'module' if inspect.ismodule(item) else 'class'
167  location = ("'%s'" % source) if source else 'unknown location'
168  self._logger_logger.info("Imported %s%s '%s' from %s."
169  % (import_type, item_type, name, location))
170 
171  def _raise_import_failed(self, name, error):
172  prefix = f'Importing {self._type.lower()}' if self._type_type else 'Importing'
173  raise DataError(f"{prefix} '{name}' failed: {error.message}")
174 
175  def _instantiate_if_needed(self, imported, args):
176  if args is None:
177  return imported
178  if inspect.isclass(imported):
179  return self._instantiate_class_instantiate_class(imported, args)
180  if args:
181  raise DataError("Modules do not take arguments.")
182  return imported
183 
184  def _instantiate_class(self, imported, args):
185  spec = self._get_arg_spec_get_arg_spec(imported)
186  try:
187  positional, named = spec.resolve(args)
188  except ValueError as err:
189  raise DataError(err.args[0])
190  try:
191  return imported(*positional, **dict(named))
192  except:
193  raise DataError('Creating instance failed: %s\n%s' % get_error_details())
194 
195  def _get_arg_spec(self, imported):
196  # Avoid cyclic import. Yuck.
197  from robot.running.arguments import ArgumentSpec, PythonArgumentParser
198 
199  init = getattr(imported, '__init__', None)
200  name = imported.__name__
201  if not is_init(init):
202  return ArgumentSpec(name, self._type_type)
203  return PythonArgumentParser(self._type_type).parse(init, name)
204 
205 
206 class _Importer:
207 
208  def __init__(self, logger):
209  self._logger_logger = logger
210 
211  def _import(self, name, fromlist=None):
212  if name in sys.builtin_module_names:
213  raise DataError('Cannot import custom module with same name as '
214  'Python built-in module.')
215  importlib.invalidate_caches()
216  try:
217  return __import__(name, fromlist=fromlist)
218  except:
219  message, traceback = get_error_details(full_traceback=False)
220  path = '\n'.join(f' {p}' for p in sys.path)
221  raise DataError(f'{message}\n{traceback}\nPYTHONPATH:\n{path}')
222 
223  def _verify_type(self, imported):
224  if inspect.isclass(imported) or inspect.ismodule(imported):
225  return imported
226  raise DataError('Expected class or module, got %s.' % type_name(imported))
227 
228  def _get_class_from_module(self, module, name=None):
229  klass = getattr(module, name or module.__name__, None)
230  return klass if inspect.isclass(klass) else None
231 
232  def _get_source(self, imported):
233  try:
234  source = inspect.getfile(imported)
235  except TypeError:
236  return None
237  return abspath(source) if source else None
238 
239 
241 
244  _valid_import_extensions = ('.py', '')
245 
246  def handles(self, path):
247  return os.path.isabs(path)
248 
249  def import_(self, path, get_class=True):
250  self._verify_import_path_verify_import_path(path)
251  self._remove_wrong_module_from_sys_modules_remove_wrong_module_from_sys_modules(path)
252  imported = self._import_by_path_import_by_path(path)
253  if get_class:
254  imported = self._get_class_from_module_get_class_from_module(imported) or imported
255  return self._verify_type_verify_type(imported), path
256 
257  def _verify_import_path(self, path):
258  if not os.path.exists(path):
259  raise DataError('File or directory does not exist.')
260  if not os.path.isabs(path):
261  raise DataError('Import path must be absolute.')
262  if not os.path.splitext(path)[1] in self._valid_import_extensions_valid_import_extensions:
263  raise DataError('Not a valid file or directory to import.')
264 
266  importing_from, name = self._split_path_to_module_split_path_to_module(path)
267  importing_package = os.path.splitext(path)[1] == ''
268  if self._wrong_module_imported_wrong_module_imported(name, importing_from, importing_package):
269  del sys.modules[name]
270  self._logger_logger.info("Removed module '%s' from sys.modules to import "
271  "fresh module." % name)
272 
273  def _split_path_to_module(self, path):
274  module_dir, module_file = os.path.split(abspath(path))
275  module_name = os.path.splitext(module_file)[0]
276  return module_dir, module_name
277 
278  def _wrong_module_imported(self, name, importing_from, importing_package):
279  if name not in sys.modules:
280  return False
281  source = getattr(sys.modules[name], '__file__', None)
282  if not source: # play safe
283  return True
284  imported_from, imported_package = self._get_import_information_get_import_information(source)
285  return (normpath(importing_from, case_normalize=True) !=
286  normpath(imported_from, case_normalize=True) or
287  importing_package != imported_package)
288 
289  def _get_import_information(self, source):
290  imported_from, imported_file = self._split_path_to_module_split_path_to_module(source)
291  imported_package = imported_file == '__init__'
292  if imported_package:
293  imported_from = os.path.dirname(imported_from)
294  return imported_from, imported_package
295 
296  def _import_by_path(self, path):
297  module_dir, module_name = self._split_path_to_module_split_path_to_module(path)
298  sys.path.insert(0, module_dir)
299  try:
300  return self._import_import(module_name)
301  finally:
302  sys.path.remove(module_dir)
303 
304 
306 
307  def handles(self, name):
308  return '.' not in name
309 
310  def import_(self, name, get_class=True):
311  imported = self._import_import(name)
312  if get_class:
313  imported = self._get_class_from_module_get_class_from_module(imported) or imported
314  return self._verify_type_verify_type(imported), self._get_source_get_source(imported)
315 
316 
318 
319  def handles(self, name):
320  return '.' in name
321 
322  def import_(self, name, get_class=True):
323  parent_name, lib_name = name.rsplit('.', 1)
324  parent = self._import_import(parent_name, fromlist=[str(lib_name)])
325  try:
326  imported = getattr(parent, lib_name)
327  except AttributeError:
328  raise DataError("Module '%s' does not contain '%s'."
329  % (parent_name, lib_name))
330  if get_class:
331  imported = self._get_class_from_module_get_class_from_module(imported, lib_name) or imported
332  return self._verify_type_verify_type(imported), self._get_source_get_source(imported)
333 
334 
335 class NoLogger:
336  error = warn = info = debug = trace = lambda self, *args, **kws: None
def _get_import_information(self, source)
Definition: importer.py:289
def _verify_import_path(self, path)
Definition: importer.py:257
def _wrong_module_imported(self, name, importing_from, importing_package)
Definition: importer.py:278
def import_(self, path, get_class=True)
Definition: importer.py:249
def _split_path_to_module(self, path)
Definition: importer.py:273
def _remove_wrong_module_from_sys_modules(self, path)
Definition: importer.py:265
def import_(self, name, get_class=True)
Definition: importer.py:322
Utility that can import modules and classes based on names and paths.
Definition: importer.py:33
def _log_import_succeeded(self, item, name, source)
Definition: importer.py:164
def __init__(self, type=None, logger=None)
:param type: Type of the thing being imported.
Definition: importer.py:43
def import_class_or_module_by_path(self, path, instantiate_with_args=None)
Import a Python module or class using a file system path.
Definition: importer.py:156
def _sanitize_source(self, source)
Definition: importer.py:131
def _raise_import_failed(self, name, error)
Definition: importer.py:171
def _import(self, name, get_class=True)
Definition: importer.py:119
def _get_arg_spec(self, imported)
Definition: importer.py:195
def _instantiate_if_needed(self, imported, args)
Definition: importer.py:175
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.
Definition: importer.py:85
def _instantiate_class(self, imported, args)
Definition: importer.py:184
def _handle_return_values(self, imported, source, return_source=False)
Definition: importer.py:124
def import_module(self, name_or_path)
Imports Python module based on the given name or path.
Definition: importer.py:110
def import_(self, name, get_class=True)
Definition: importer.py:310
def _verify_type(self, imported)
Definition: importer.py:223
def __init__(self, logger)
Definition: importer.py:208
def _get_class_from_module(self, module, name=None)
Definition: importer.py:228
def _get_source(self, imported)
Definition: importer.py:232
def _import(self, name, fromlist=None)
Definition: importer.py:211
def info(msg, html=False, also_console=False)
Writes the message to the log file using the INFO level.
Definition: logger.py:113
def get_error_details(full_traceback=True, exclude_robot_traces=EXCLUDE_ROBOT_TRACES)
Returns error message and details of the last occurred exception.
Definition: error.py:39
def abspath(path, case_normalize=False)
Replacement for os.path.abspath with some enhancements and bug fixes.
Definition: robotpath.py:65
def normpath(path, case_normalize=False)
Replacement for os.path.normpath with some enhancements.
Definition: robotpath.py:46
def type_name(item, capitalize=False)
Return "non-technical" type name for objects and types.
Definition: robottypes.py:86