Skip to content

Capture and log messages emitted by C modules when importing them #1514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Release date: TBA

Closes #1512

* Capture and log messages emitted by C extensions when importing them.
This prevents contaminating programmatic output, e.g. pylint's JSON reporter.

Closes PyCQA/pylint#3518

* Remove dependency on ``pkg_resources`` from ``setuptools``.

Closes #1103
Expand Down
26 changes: 25 additions & 1 deletion astroid/modutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@
import importlib
import importlib.machinery
import importlib.util
import io
import itertools
import logging
import os
import sys
import sysconfig
import types
from contextlib import redirect_stderr, redirect_stdout
from functools import lru_cache
from pathlib import Path

from astroid.const import IS_JYTHON, IS_PYPY
from astroid.interpreter._import import spec, util

logger = logging.getLogger(__name__)


if sys.platform.startswith("win"):
PY_SOURCE_EXTS = ("py", "pyw")
PY_COMPILED_EXTS = ("dll", "pyd")
Expand Down Expand Up @@ -171,7 +177,25 @@ def load_module_from_name(dotted_name: str) -> types.ModuleType:
except KeyError:
pass

return importlib.import_module(dotted_name)
# Capture and log anything emitted during import to avoid
# contaminating JSON reports in pylint
with redirect_stderr(io.StringIO()) as stderr, redirect_stdout(
io.StringIO()
) as stdout:
module = importlib.import_module(dotted_name)

stderr_value = stderr.getvalue()
if stderr_value:
logger.error(
"Captured stderr while importing %s:\n%s", dotted_name, stderr_value
)
stdout_value = stdout.getvalue()
if stdout_value:
logger.info(
"Captured stdout while importing %s:\n%s", dotted_name, stdout_value
)

return module


def load_module_from_modpath(parts):
Expand Down
37 changes: 36 additions & 1 deletion tests/unittest_modutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
unit tests for module modutils (module manipulation utilities)
"""
import email
import logging
import os
import shutil
import sys
Expand All @@ -16,6 +17,8 @@
from xml import etree
from xml.etree import ElementTree

from pytest import CaptureFixture, LogCaptureFixture

import astroid
from astroid import modutils
from astroid.interpreter._import import spec
Expand Down Expand Up @@ -57,7 +60,7 @@ def test_find_egg_module(self) -> None:


class LoadModuleFromNameTest(unittest.TestCase):
"""load a python module from it's name"""
"""load a python module from its name"""

def test_known_values_load_module_from_name_1(self) -> None:
self.assertEqual(modutils.load_module_from_name("sys"), sys)
Expand All @@ -71,6 +74,38 @@ def test_raise_load_module_from_name_1(self) -> None:
)


def test_import_dotted_library(
capsys: CaptureFixture,
caplog: LogCaptureFixture,
) -> None:
caplog.set_level(logging.INFO)
original_module = sys.modules.pop("xml.etree.ElementTree")
expected_out = "INFO (TEST): Welcome to cElementTree!"
expected_err = "WARNING (TEST): Monkey-patched version of cElementTree"

def function_with_stdout_and_stderr(expected_out, expected_err):
def mocked_function(*args, **kwargs):
print(f"{expected_out} args={args} kwargs={kwargs}")
print(expected_err, file=sys.stderr)

return mocked_function

try:
with unittest.mock.patch(
"importlib.import_module",
side_effect=function_with_stdout_and_stderr(expected_out, expected_err),
):
modutils.load_module_from_name("xml.etree.ElementTree")

out, err = capsys.readouterr()
assert expected_out in caplog.text
assert expected_err in caplog.text
assert not out
assert not err
finally:
sys.modules["xml.etree.ElementTree"] = original_module


class GetModulePartTest(unittest.TestCase):
"""given a dotted name return the module part of the name"""

Expand Down