Commit 03ec4fba authored by Kirill Smelkov's avatar Kirill Smelkov

pylint: Fix crash on LazyModules if those modules are not actually available

Pygolang installs import hooks for pytest and ipython, to add
exception-chaining support into them(*) _iff_ (if and only if) those
modules are actually used. This works via Importing[1] which
pre-installs artificial modules into sys.modules that catch
__getattribute__ and try to import corresponding module for real on
first access. Usually everything is fine.

But with pylint/astroid, if the checker code happens to run with those
LazyModules installed, and the checked code has `import sys` somewhere,
astroid eventually delves into processing sys, then sys.modules and
wants to represent that sys.modules dict as dict of constant. Then, when
e.g. sys.modules['_pytest'] is processed, corresponding module object is
checked for .__class__, which raises ImportError if pytest is not
actually available:

    ( https://erp5js.nexedi.net/#/test_result_module/20220127-129289AE2/33 )
    ...
    File ".../eggs/astroid-1.3.8-py2.7.egg/astroid/node_classes.py", line 553, in __init__
        for k, v in items.items()]
      File ".../eggs/astroid-1.3.8-py2.7.egg/astroid/node_classes.py", line 962, in const_factory
        return CONST_CLS[value.__class__](value)
      File ".../eggs/Importing-1.10-py2.7.egg/peak/util/imports.py", line 254, in __getattribute__
        _loadModule(self)
      File ".../eggs/Importing-1.10-py2.7.egg/peak/util/imports.py", line 222, in _loadModule
        reload(module)
    ImportError: No module named _pytest

-> Fix it by detecting those lazy modules and not letting them go through
normal const_factory not to crash.

/cc @jerome, @arnau

[1] https://pypi.org/project/Importing/

(*) see:
    https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/__init__.py
    https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/pytest_py2.py#L48-51
    https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/ipython_py2.py#L45-48
parent dcb873b7
...@@ -39,7 +39,7 @@ OptionsManagerMixIn.read_config_file = lambda *args, **kw: None ...@@ -39,7 +39,7 @@ OptionsManagerMixIn.read_config_file = lambda *args, **kw: None
## Pylint transforms and plugin to generate AST for ZODB Components ## Pylint transforms and plugin to generate AST for ZODB Components
from astroid.builder import AstroidBuilder from astroid.builder import AstroidBuilder
from astroid.exceptions import AstroidBuildingException from astroid.exceptions import AstroidBuildingException
from astroid import MANAGER from astroid import MANAGER, node_classes
try: try:
from astroid.builder import _guess_encoding from astroid.builder import _guess_encoding
...@@ -98,6 +98,32 @@ def string_build(self, data, modname='', path=None): ...@@ -98,6 +98,32 @@ def string_build(self, data, modname='', path=None):
return self._post_build(module, encoding) return self._post_build(module, encoding)
AstroidBuilder.string_build = string_build AstroidBuilder.string_build = string_build
# patch node_classes.const_factory not to fail on LazyModules that e.g.
# pygolang installs for pytest and ipython into sys.modules dict:
#
# https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/__init__.py
# https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/pytest_py2.py#L48-51
# https://lab.nexedi.com/nexedi/pygolang/blob/pygolang-0.1-0-g7b72d41/golang/_patch/ipython_py2.py#L45-48
#
# if we don't patch and the module is not available, upon checking sys->sys.modules
# const_factory will fail with ImportError when accessing value.__class__.
node_classes_const_factory = node_classes.const_factory
def const_factory(value):
typ = type(value)
typename = ('%s.%s' % (typ.__module__, typ.__name__))
if typename == 'peak.util.imports.LazyModule':
# lazy module installed by Importing
# see if we can load it, and return empty placehoder if the module is not available
try:
value.__class__
except ImportError:
node = node_classes.EmptyNode()
node.object = None # not value
return node
# ok the module is available and is now loaded - continue via normal const_factory path
return node_classes_const_factory(value)
node_classes.const_factory = const_factory
from astroid import nodes from astroid import nodes
def erp5_package_transform(node): def erp5_package_transform(node):
"""' """'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment