Format: text/x-python
⬇ Download File (10.1 KB)
   1  import sys
   2  from typing import List
   3  from pathlib import Path
   4  
   5  from parso.tree import search_ancestor
   6  from jedi.inference.cache import inference_state_method_cache
   7  from jedi.inference.imports import goto_import, load_module_from_path
   8  from jedi.inference.filters import ParserTreeFilter
   9  from jedi.inference.base_value import NO_VALUES, ValueSet
  10  from jedi.inference.helpers import infer_call_of_leaf
  11  
  12  _PYTEST_FIXTURE_MODULES = [
  13      ('_pytest', 'monkeypatch'),
  14      ('_pytest', 'capture'),
  15      ('_pytest', 'logging'),
  16      ('_pytest', 'tmpdir'),
  17      ('_pytest', 'pytester'),
  18  ]
  19  
  20  
  21  def execute(callback):
  22      def wrapper(value, arguments):
  23          # This might not be necessary anymore in pytest 4/5, definitely needed
  24          # for pytest 3.
  25          if value.py__name__() == 'fixture' \
  26                  and value.parent_context.py__name__() == '_pytest.fixtures':
  27              return NO_VALUES
  28  
  29          return callback(value, arguments)
  30      return wrapper
  31  
  32  
  33  def infer_anonymous_param(func):
  34      def get_returns(value):
  35          if value.tree_node.annotation is not None:
  36              result = value.execute_with_values()
  37              if any(v.name.get_qualified_names(include_module_names=True)
  38                     == ('typing', 'Generator')
  39                     for v in result):
  40                  return ValueSet.from_sets(
  41                      v.py__getattribute__('__next__').execute_annotation()
  42                      for v in result
  43                  )
  44              return result
  45  
  46          # In pytest we need to differentiate between generators and normal
  47          # returns.
  48          # Parameters still need to be anonymous, .as_context() ensures that.
  49          function_context = value.as_context()
  50          if function_context.is_generator():
  51              return function_context.merge_yield_values()
  52          else:
  53              return function_context.get_return_values()
  54  
  55      def wrapper(param_name):
  56          # parameters with an annotation do not need special handling
  57          if param_name.annotation_node:
  58              return func(param_name)
  59          is_pytest_param, param_name_is_function_name = \
  60              _is_a_pytest_param_and_inherited(param_name)
  61          if is_pytest_param:
  62              module = param_name.get_root_context()
  63              fixtures = _goto_pytest_fixture(
  64                  module,
  65                  param_name.string_name,
  66                  # This skips the current module, because we are basically
  67                  # inheriting a fixture from somewhere else.
  68                  skip_own_module=param_name_is_function_name,
  69              )
  70              if fixtures:
  71                  return ValueSet.from_sets(
  72                      get_returns(value)
  73                      for fixture in fixtures
  74                      for value in fixture.infer()
  75                  )
  76          return func(param_name)
  77      return wrapper
  78  
  79  
  80  def goto_anonymous_param(func):
  81      def wrapper(param_name):
  82          is_pytest_param, param_name_is_function_name = \
  83              _is_a_pytest_param_and_inherited(param_name)
  84          if is_pytest_param:
  85              names = _goto_pytest_fixture(
  86                  param_name.get_root_context(),
  87                  param_name.string_name,
  88                  skip_own_module=param_name_is_function_name,
  89              )
  90              if names:
  91                  return names
  92          return func(param_name)
  93      return wrapper
  94  
  95  
  96  def complete_param_names(func):
  97      def wrapper(context, func_name, decorator_nodes):
  98          module_context = context.get_root_context()
  99          if _is_pytest_func(func_name, decorator_nodes):
 100              names = []
 101              for module_context in _iter_pytest_modules(module_context):
 102                  names += FixtureFilter(module_context).values()
 103              if names:
 104                  return names
 105          return func(context, func_name, decorator_nodes)
 106      return wrapper
 107  
 108  
 109  def _goto_pytest_fixture(module_context, name, skip_own_module):
 110      for module_context in _iter_pytest_modules(module_context, skip_own_module=skip_own_module):
 111          names = FixtureFilter(module_context).get(name)
 112          if names:
 113              return names
 114  
 115  
 116  def _is_a_pytest_param_and_inherited(param_name):
 117      """
 118      Pytest params are either in a `test_*` function or have a pytest fixture
 119      with the decorator @pytest.fixture.
 120  
 121      This is a heuristic and will work in most cases.
 122      """
 123      funcdef = search_ancestor(param_name.tree_name, 'funcdef')
 124      if funcdef is None:  # A lambda
 125          return False, False
 126      decorators = funcdef.get_decorators()
 127      return _is_pytest_func(funcdef.name.value, decorators), \
 128          funcdef.name.value == param_name.string_name
 129  
 130  
 131  def _is_pytest_func(func_name, decorator_nodes):
 132      return func_name.startswith('test') \
 133          or any('fixture' in n.get_code() for n in decorator_nodes)
 134  
 135  
 136  def _find_pytest_plugin_modules() -> List[List[str]]:
 137      """
 138      Finds pytest plugin modules hooked by setuptools entry points
 139  
 140      See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points
 141      """
 142      if sys.version_info >= (3, 8):
 143          from importlib.metadata import entry_points
 144  
 145          if sys.version_info >= (3, 10):
 146              pytest_entry_points = entry_points(group="pytest11")
 147          else:
 148              pytest_entry_points = entry_points().get("pytest11", ())
 149  
 150          if sys.version_info >= (3, 9):
 151              return [ep.module.split(".") for ep in pytest_entry_points]
 152          else:
 153              # Python 3.8 doesn't have `EntryPoint.module`. Implement equivalent
 154              # to what Python 3.9 does (with additional None check to placate `mypy`)
 155              matches = [
 156                  ep.pattern.match(ep.value)
 157                  for ep in pytest_entry_points
 158              ]
 159              return [x.group('module').split(".") for x in matches if x]
 160  
 161      else:
 162          from pkg_resources import iter_entry_points
 163          return [ep.module_name.split(".") for ep in iter_entry_points(group="pytest11")]
 164  
 165  
 166  @inference_state_method_cache()
 167  def _iter_pytest_modules(module_context, skip_own_module=False):
 168      if not skip_own_module:
 169          yield module_context
 170  
 171      file_io = module_context.get_value().file_io
 172      if file_io is not None:
 173          folder = file_io.get_parent_folder()
 174          sys_path = module_context.inference_state.get_sys_path()
 175  
 176          # prevent an infinite loop when reaching the root of the current drive
 177          last_folder = None
 178  
 179          while any(folder.path.startswith(p) for p in sys_path):
 180              file_io = folder.get_file_io('conftest.py')
 181              if Path(file_io.path) != module_context.py__file__():
 182                  try:
 183                      m = load_module_from_path(module_context.inference_state, file_io)
 184                      conftest_module = m.as_context()
 185                      yield conftest_module
 186  
 187                      plugins_list = m.tree_node.get_used_names().get("pytest_plugins")
 188                      if plugins_list:
 189                          name = conftest_module.create_name(plugins_list[0])
 190                          yield from _load_pytest_plugins(module_context, name)
 191                  except FileNotFoundError:
 192                      pass
 193              folder = folder.get_parent_folder()
 194  
 195              # prevent an infinite for loop if the same parent folder is return twice
 196              if last_folder is not None and folder.path == last_folder.path:
 197                  break
 198              last_folder = folder  # keep track of the last found parent name
 199  
 200      for names in _PYTEST_FIXTURE_MODULES + _find_pytest_plugin_modules():
 201          for module_value in module_context.inference_state.import_module(names):
 202              yield module_value.as_context()
 203  
 204  
 205  def _load_pytest_plugins(module_context, name):
 206      from jedi.inference.helpers import get_str_or_none
 207  
 208      for inferred in name.infer():
 209          for seq_value in inferred.py__iter__():
 210              for value in seq_value.infer():
 211                  fq_name = get_str_or_none(value)
 212                  if fq_name:
 213                      names = fq_name.split(".")
 214                      for module_value in module_context.inference_state.import_module(names):
 215                          yield module_value.as_context()
 216  
 217  
 218  class FixtureFilter(ParserTreeFilter):
 219      def _filter(self, names):
 220          for name in super()._filter(names):
 221              # look for fixture definitions of imported names
 222              if name.parent.type == "import_from":
 223                  imported_names = goto_import(self.parent_context, name)
 224                  if any(
 225                      self._is_fixture(iname.parent_context, iname.tree_name)
 226                      for iname in imported_names
 227                      # discard imports of whole modules, that have no tree_name
 228                      if iname.tree_name
 229                  ):
 230                      yield name
 231  
 232              elif self._is_fixture(self.parent_context, name):
 233                  yield name
 234  
 235      def _is_fixture(self, context, name):
 236          funcdef = name.parent
 237          # Class fixtures are not supported
 238          if funcdef.type != "funcdef":
 239              return False
 240          decorated = funcdef.parent
 241          if decorated.type != "decorated":
 242              return False
 243          decorators = decorated.children[0]
 244          if decorators.type == 'decorators':
 245              decorators = decorators.children
 246          else:
 247              decorators = [decorators]
 248          for decorator in decorators:
 249              dotted_name = decorator.children[1]
 250              # A heuristic, this makes it faster.
 251              if 'fixture' in dotted_name.get_code():
 252                  if dotted_name.type == 'atom_expr':
 253                      # Since Python3.9 a decorator does not have dotted names
 254                      # anymore.
 255                      last_trailer = dotted_name.children[-1]
 256                      last_leaf = last_trailer.get_last_leaf()
 257                      if last_leaf == ')':
 258                          values = infer_call_of_leaf(
 259                              context, last_leaf, cut_own_trailer=True
 260                          )
 261                      else:
 262                          values = context.infer_node(dotted_name)
 263                  else:
 264                      values = context.infer_node(dotted_name)
 265                  for value in values:
 266                      if value.name.get_qualified_names(include_module_names=True) \
 267                              == ('_pytest', 'fixtures', 'fixture'):
 268                          return True
 269          return False
 270