Repository: pylint-dev/pylint

Issue:
Azure import try/except over doubles pylint memory usage
### Bug description

We run pylint on a reasonably large codebase, ~900 .py files, ~350k lines.
Recently we've needed to support two similar versions of the Azure SDK and as such made the change below in a single file.

from
```python
from azure.mgmt.network.v2022_01_01 import NetworkManagementClient
from azure.mgmt.network.v2022_01_01.models import Resource as NetworkResource
```
to
```python
try:
    from azure.mgmt.network.v2022_01_01 import NetworkManagementClient
    from azure.mgmt.network.v2022_01_01.models import Resource as NetworkResource
except ImportError:
    from azure.mgmt.network import NetworkManagementClient
    from azure.mgmt.network.v2022_07_01.models import Resource as NetworkResource
```

This change has increased pylint's run time but more importantly, over doubled resident memory usage! I've tried various combinations of disables and module ignores but cannot get anywhere close to previous figures without reverting the code change. Including attempting to ignore the azure module altogether with `--ignored-modules=azure`

Numbers below are taken from `/usr/bin/time -v python3 -m pylint package1 package2 -f colorized -r n -j 1`

Old code:
```
Elapsed (wall clock) time (h:mm:ss or m:ss): 5:13.65
Maximum resident set size (kbytes): 1589112
```

New code:
```
Elapsed (wall clock) time (h:mm:ss or m:ss): 6:30.88
Maximum resident set size (kbytes): 3993300
```

New code with `--ignored-modules=azure`:
```
Elapsed (wall clock) time (h:mm:ss or m:ss): 6:42.64
Maximum resident set size (kbytes): 3994000
```

To make this more generic, I'm running without a pylintrc file using the latest version from PyPI. This has the side effect of producing masses of warning/error output as usually we have a fair number of disables, however the issue described is still present in this state.


### Configuration

_No response_

### Command used

```shell
/usr/bin/time -v python3 -m pylint package1 package2 -f colorized -r n -j 1
and
/usr/bin/time -v python3 -m pylint --ignored-modules=azure package1 package2 -f colorized -r n -j 1
```


### Pylint output

_n/a_

### Expected behavior

Original run times and memory usage

### Pylint version

```shell
pylint 3.0.3
astroid 3.0.3
Python 3.9.18 (main, Aug 25 2023, 13:20:14) 
[GCC 11.4.0]
```


### OS / Environment

Ubuntu 22.04

### Additional dependencies

```python
# non-Azure modules removed
azure-common==1.1.28
azure-core==1.26.4
azure-identity==1.12.0
azure-keyvault-certificates==4.7.0
azure-keyvault-secrets==4.7.0
azure-mgmt-automation==1.0.0
azure-mgmt-compute==29.1.0
azure-mgmt-core==1.4.0
azure-mgmt-keyvault==10.2.1
azure-mgmt-monitor==6.0.0
azure-mgmt-network==21.0.1
azure-mgmt-resource==23.0.0
azure-mgmt-storage==21.0.0
azure-storage-blob==12.16.0
msrestazure==0.6.4
```



Discussion:
Thanks for the report! Sounds reasonable to expect `ignored-modules` to be more useful than it is.

I'm showing this is solved with two tiny patches to pylint and astroid. If you're up for it, would you be willing to confirm and open the PRs? We'd need just a little test and some documentation spruce-ups for the ignored-modules option.
```diff
diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py
index 30250154e..473cd0900 100644
--- a/pylint/lint/pylinter.py
+++ b/pylint/lint/pylinter.py
@@ -1073,6 +1073,7 @@ class PyLinter(
         MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
         MANAGER.max_inferable_values = self.config.limit_inference_results
         MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list)
+        MANAGER.module_denylist.extend(self.config.ignored_modules)
         if self.config.extension_pkg_whitelist:
             MANAGER.extension_package_whitelist.update(
                 self.config.extension_pkg_whitelist
```
```diff
diff --git a/astroid/manager.py b/astroid/manager.py
index c499fe55..386a3838 100644
--- a/astroid/manager.py
+++ b/astroid/manager.py
@@ -59,6 +59,7 @@ class AstroidManager:
         "optimize_ast": False,
         "max_inferable_values": 100,
         "extension_package_whitelist": set(),
+        "module_denylist": [],
         "_transform": TransformVisitor(),
     }
 
@@ -70,6 +71,7 @@ class AstroidManager:
         self.extension_package_whitelist = AstroidManager.brain[
             "extension_package_whitelist"
         ]
+        self.module_denylist = AstroidManager.brain["module_denylist"]
         self._transform = AstroidManager.brain["_transform"]
 
     @property
@@ -200,6 +203,8 @@ class AstroidManager:
         # importing a module with the same name as the file that is importing
         # we want to fallback on the import system to make sure we get the correct
         # module.
+        if modname in self.module_denylist:
+            raise AstroidImportError("Skipping")
         if modname in self.astroid_cache and use_cache:
             return self.astroid_cache[modname]
         if modname == "__main__":
```
Aha ok. I incorrectly assumed ignored-modules did that already.
I checked out the pylint and astroid repos, made those changes locally and ran some benchmarks on my codebase with (this time using all cores to get answers faster).

From what I'm seeing, those changes make no effect to the speed or memory usage when adding `--ignored-modules=azure`. I'm not quite sure why as adding some debug around the modified code, we are hitting the `raise AstroidImportError("Skipping")` line with the `azure` module.

Any pointers?
Make sure you're using the full module name, e.g. `azure.mgmt.network.v2022_07_01.models`
Yep, that helps! Seeing a considerable improvement now. Thanks for that!
Will look to get the changes PRed up.
Wonderful. Feel free to make improvements (e.g. for consistency, that collection should probably be a set...)



Patch:
diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt
index fd4fed00c3..4bcff9c931 100644
--- a/.pyenchant_pylint_custom_dict.txt
+++ b/.pyenchant_pylint_custom_dict.txt
@@ -300,6 +300,7 @@ spammy
 sqlalchemy
 src
 starargs
+stateful
 staticmethod
 stderr
 stdin
diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst
index 94d2c1775e..cef8e2a72b 100644
--- a/doc/user_guide/configuration/all-options.rst
+++ b/doc/user_guide/configuration/all-options.rst
@@ -120,7 +120,7 @@ Standard Checkers
 
 --ignored-modules
 """""""""""""""""
-*List of module names for which member attributes should not be checked (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.*
+*List of module names for which member attributes should not be checked and will not be imported (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.*
 
 **Default:**  ``()``
 
diff --git a/doc/whatsnew/fragments/9442.other b/doc/whatsnew/fragments/9442.other
new file mode 100644
index 0000000000..9523aba7ff
--- /dev/null
+++ b/doc/whatsnew/fragments/9442.other
@@ -0,0 +1,5 @@
+Ignored modules are now not checked at all, instead of being checked and then
+ignored. This should speed up the analysis of large codebases which have
+ignored modules.
+
+Closes #9442 (`#9442 <https://github.com/pylint-dev/pylint/issues/9442>`_)
diff --git a/examples/pylintrc b/examples/pylintrc
index d47800516d..4685415447 100644
--- a/examples/pylintrc
+++ b/examples/pylintrc
@@ -59,10 +59,11 @@ ignore-paths=
 # Emacs file locks
 ignore-patterns=^\.#
 
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis). It
-# supports qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 ignored-modules=
 
 # Python code to execute, usually for sys.path manipulation such as
diff --git a/examples/pyproject.toml b/examples/pyproject.toml
index a8ec9a7ecb..7d9ffe8260 100644
--- a/examples/pyproject.toml
+++ b/examples/pyproject.toml
@@ -49,10 +49,11 @@ ignore = ["CVS"]
 # file locks
 ignore-patterns = ["^\\.#"]
 
-# List of module names for which member attributes should not be checked (useful
-# for modules/projects where namespaces are manipulated during runtime and thus
-# existing member attributes cannot be deduced by static analysis). It supports
-# qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 # ignored-modules =
 
 # Python code to execute, usually for sys.path manipulation such as
diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py
index 3d5ba5d0db..cd354c49db 100644
--- a/pylint/lint/base_options.py
+++ b/pylint/lint/base_options.py
@@ -384,7 +384,8 @@ def _make_linter_options(linter: PyLinter) -> Options:
                 "type": "csv",
                 "metavar": "<module names>",
                 "help": "List of module names for which member attributes "
-                "should not be checked (useful for modules/projects "
+                "should not be checked and will not be imported "
+                "(useful for modules/projects "
                 "where namespaces are manipulated during runtime and "
                 "thus existing member attributes cannot be "
                 "deduced by static analysis). It supports qualified "
diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py
index 30250154e6..f1aca76a4e 100644
--- a/pylint/lint/pylinter.py
+++ b/pylint/lint/pylinter.py
@@ -1073,6 +1073,7 @@ def open(self) -> None:
         MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
         MANAGER.max_inferable_values = self.config.limit_inference_results
         MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list)
+        MANAGER.module_denylist.update(self.config.ignored_modules)
         if self.config.extension_pkg_whitelist:
             MANAGER.extension_package_whitelist.update(
                 self.config.extension_pkg_whitelist
diff --git a/pylintrc b/pylintrc
index 434fa23836..a943b1cfd3 100644
--- a/pylintrc
+++ b/pylintrc
@@ -342,10 +342,11 @@ property-classes=abc.abstractproperty
 # members is set to 'yes'
 mixin-class-rgx=.*MixIn
 
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis). It
-# supports qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 ignored-modules=
 
 # List of class names for which member attributes should not be checked (useful


Test patch:
diff --git a/tests/lint/test_pylinter.py b/tests/lint/test_pylinter.py
index 1e4f40a002..c14d1929af 100644
--- a/tests/lint/test_pylinter.py
+++ b/tests/lint/test_pylinter.py
@@ -10,7 +10,7 @@
 
 from pytest import CaptureFixture
 
-from pylint.lint.pylinter import PyLinter
+from pylint.lint.pylinter import MANAGER, PyLinter
 from pylint.utils import FileState
 
 
@@ -48,3 +48,14 @@ def test_crash_during_linting(
         assert len(files) == 1
         assert "pylint-crash-20" in str(files[0])
         assert any(m.symbol == "astroid-error" for m in linter.reporter.messages)
+
+
+def test_open_pylinter_denied_modules(linter: PyLinter) -> None:
+    """Test PyLinter open() adds ignored modules to Astroid manager deny list."""
+    MANAGER.module_denylist = {"mod1"}
+    try:
+        linter.config.ignored_modules = ["mod2", "mod3"]
+        linter.open()
+        assert MANAGER.module_denylist == {"mod1", "mod2", "mod3"}
+    finally:
+        MANAGER.module_denylist = set()
diff --git a/tests/test_functional.py b/tests/test_functional.py
index ef0a373def..df42767a5c 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -7,13 +7,16 @@
 from __future__ import annotations
 
 import sys
+from collections.abc import Iterator
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 import pytest
 from _pytest.config import Config
 
 from pylint import testutils
 from pylint.constants import PY312_PLUS
+from pylint.lint.pylinter import MANAGER
 from pylint.testutils import UPDATE_FILE, UPDATE_OPTION
 from pylint.testutils.functional import (
     FunctionalTestFile,
@@ -22,6 +25,9 @@
 )
 from pylint.utils import HAS_ISORT_5
 
+if TYPE_CHECKING:
+    from pylint.lint import PyLinter
+
 FUNCTIONAL_DIR = Path(__file__).parent.resolve() / "functional"
 
 
@@ -40,6 +46,14 @@
 ]
 
 
+@pytest.fixture
+def revert_stateful_config_changes(linter: PyLinter) -> Iterator[PyLinter]:
+    yield linter
+    # Revert any stateful configuration changes.
+    MANAGER.brain["module_denylist"] = set()
+
+
+@pytest.mark.usefixtures("revert_stateful_config_changes")
 @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES)
 def test_functional(test_file: FunctionalTestFile, pytestconfig: Config) -> None:
     __tracebackhide__ = True  # pylint: disable=unused-variable