From b2555e7118b424593a69e64a7d5e03250a30b218 Mon Sep 17 00:00:00 2001 From: Bryce Guinta Date: Thu, 5 Jul 2018 15:51:36 -0600 Subject: [PATCH] Limit inference to a maximum of 100 results at a time to prevent spot performance issues. Add new envrionment variable call ASTROID_MAX_INFERABLE to tune the max inferable amount of values at a time. Close #579 Close PyCQA/pylint#2251 --- ChangeLog | 6 +++++ astroid/manager.py | 2 ++ astroid/node_classes.py | 4 +++- astroid/tests/unittest_inference.py | 30 +++++++++++++++++++++++++ astroid/util.py | 34 +++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index cd4d76eeb1..211fbb75e5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -157,6 +157,12 @@ Release Date: Unknown Close PyCQA/pylint#2159 + * Limit the maximum amount of interable result in an NodeNG.infer() call to + 100 by default for performance issues with variables with large amounts of + possible values. + The max inferable value can be tuned by setting the ASTROID_MAX_INFERABLE environment + variable at start up. + What's New in astroid 1.6.0? ============================ diff --git a/astroid/manager.py b/astroid/manager.py index c1caf15146..c755366172 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -52,6 +52,8 @@ def __init__(self): # Export these APIs for convenience self.register_transform = self._transform.register_transform self.unregister_transform = self._transform.unregister_transform + self.max_inferable = int( + os.environ.get("ASTROID_MAX_INFERABLE", 100)) def visit_transforms(self, node): """Visit the transforms and apply them to the given *node*.""" diff --git a/astroid/node_classes.py b/astroid/node_classes.py index a17af1a22e..a3c02fcf66 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -323,7 +323,9 @@ def infer(self, context=None, **kwargs): if key in context.inferred: return iter(context.inferred[key]) - return context.cache_generator(key, self._infer(context, **kwargs)) + gen = context.cache_generator( + key, self._infer(context, **kwargs)) + return util.limit_inference(gen, MANAGER.max_inferable) def _repr_name(self): """Get a name for nice representation. diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index 5ee53d1f0c..5f9194c387 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -15,6 +15,7 @@ import sys from functools import partial import unittest +from unittest.mock import patch import pytest @@ -4656,5 +4657,34 @@ def f(**kwargs): assert next(extract_node(code).infer()).as_string() == "{'f': 1}" +def test_limit_inference_result_amount(): + """Test setting limit inference result amount""" + code = """ + args = [] + + if True: + args += ['a'] + + if True: + args += ['b'] + + if True: + args += ['c'] + + if True: + args += ['d'] + + args #@ + """ + result = extract_node(code).inferred() + assert len(result) == 16 + with patch('astroid.node_classes.MANAGER.max_inferable', 4): + result_limited = extract_node(code).inferred() + # Can't guarentee exact size + assert len(result_limited) < 16 + # Will not always be at the end + assert util.Uninferable in result_limited + + if __name__ == '__main__': unittest.main() diff --git a/astroid/util.py b/astroid/util.py index a53109707a..daf088eec2 100644 --- a/astroid/util.py +++ b/astroid/util.py @@ -5,6 +5,7 @@ # For details: https://github.com/PyCQA/astroid/blob/master/COPYING.LESSER import warnings +from itertools import islice import importlib import lazy_object_proxy @@ -126,5 +127,38 @@ def proxy_alias(alias_name, node_type): return proxy(lambda: node_type) +def limit_inference(iterable): + count = 0 + for result in iterable: + count += 1 + if count > MAX_INFERABLE: + yield Uninferable + return + yield result + + +def limit_inference(iterator, size): + """Limit inference amount. + + Limit inference amount to help with performance issues with + exponentially exploding possible results. + + :param iterator: Inference generator to limit + :type iterator: Iterator(NodeNG) + + :param size: Maximum mount of nodes yielded plus an + Uninferable at the end if limit reached + :type size: int + + :yields: A possibly modified generator + :rtype param: Iterable + """ + yield from islice(iterator, size) + has_more = next(iterator, False) + if has_more is not False: + yield Uninferable + return + + # Backwards-compatibility aliases YES = Uninferable