Skip to content

Commit 70d6751

Browse files
Capture and log messages emitted by C modules when importing them (#1514)
Prevent contaminating programmatic output, e.g. pylint's JSON reporter. Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent b08b811 commit 70d6751

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ Release date: TBA
1515

1616
Closes #1512
1717

18+
* Capture and log messages emitted by C extensions when importing them.
19+
This prevents contaminating programmatic output, e.g. pylint's JSON reporter.
20+
21+
Closes PyCQA/pylint#3518
22+
1823
* Remove dependency on ``pkg_resources`` from ``setuptools``.
1924

2025
Closes #1103

astroid/modutils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@
1919
import importlib
2020
import importlib.machinery
2121
import importlib.util
22+
import io
2223
import itertools
24+
import logging
2325
import os
2426
import sys
2527
import sysconfig
2628
import types
29+
from contextlib import redirect_stderr, redirect_stdout
2730
from functools import lru_cache
2831
from pathlib import Path
2932

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

36+
logger = logging.getLogger(__name__)
37+
38+
3339
if sys.platform.startswith("win"):
3440
PY_SOURCE_EXTS = ("py", "pyw")
3541
PY_COMPILED_EXTS = ("dll", "pyd")
@@ -171,7 +177,25 @@ def load_module_from_name(dotted_name: str) -> types.ModuleType:
171177
except KeyError:
172178
pass
173179

174-
return importlib.import_module(dotted_name)
180+
# Capture and log anything emitted during import to avoid
181+
# contaminating JSON reports in pylint
182+
with redirect_stderr(io.StringIO()) as stderr, redirect_stdout(
183+
io.StringIO()
184+
) as stdout:
185+
module = importlib.import_module(dotted_name)
186+
187+
stderr_value = stderr.getvalue()
188+
if stderr_value:
189+
logger.error(
190+
"Captured stderr while importing %s:\n%s", dotted_name, stderr_value
191+
)
192+
stdout_value = stdout.getvalue()
193+
if stdout_value:
194+
logger.info(
195+
"Captured stdout while importing %s:\n%s", dotted_name, stdout_value
196+
)
197+
198+
return module
175199

176200

177201
def load_module_from_modpath(parts):

tests/unittest_modutils.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
unit tests for module modutils (module manipulation utilities)
77
"""
88
import email
9+
import logging
910
import os
1011
import shutil
1112
import sys
@@ -16,6 +17,8 @@
1617
from xml import etree
1718
from xml.etree import ElementTree
1819

20+
from pytest import CaptureFixture, LogCaptureFixture
21+
1922
import astroid
2023
from astroid import modutils
2124
from astroid.interpreter._import import spec
@@ -57,7 +60,7 @@ def test_find_egg_module(self) -> None:
5760

5861

5962
class LoadModuleFromNameTest(unittest.TestCase):
60-
"""load a python module from it's name"""
63+
"""load a python module from its name"""
6164

6265
def test_known_values_load_module_from_name_1(self) -> None:
6366
self.assertEqual(modutils.load_module_from_name("sys"), sys)
@@ -71,6 +74,38 @@ def test_raise_load_module_from_name_1(self) -> None:
7174
)
7275

7376

77+
def test_import_dotted_library(
78+
capsys: CaptureFixture,
79+
caplog: LogCaptureFixture,
80+
) -> None:
81+
caplog.set_level(logging.INFO)
82+
original_module = sys.modules.pop("xml.etree.ElementTree")
83+
expected_out = "INFO (TEST): Welcome to cElementTree!"
84+
expected_err = "WARNING (TEST): Monkey-patched version of cElementTree"
85+
86+
def function_with_stdout_and_stderr(expected_out, expected_err):
87+
def mocked_function(*args, **kwargs):
88+
print(f"{expected_out} args={args} kwargs={kwargs}")
89+
print(expected_err, file=sys.stderr)
90+
91+
return mocked_function
92+
93+
try:
94+
with unittest.mock.patch(
95+
"importlib.import_module",
96+
side_effect=function_with_stdout_and_stderr(expected_out, expected_err),
97+
):
98+
modutils.load_module_from_name("xml.etree.ElementTree")
99+
100+
out, err = capsys.readouterr()
101+
assert expected_out in caplog.text
102+
assert expected_err in caplog.text
103+
assert not out
104+
assert not err
105+
finally:
106+
sys.modules["xml.etree.ElementTree"] = original_module
107+
108+
74109
class GetModulePartTest(unittest.TestCase):
75110
"""given a dotted name return the module part of the name"""
76111

0 commit comments

Comments
 (0)