Skip to content

Commit

Permalink
Limit inference to a maximum of 100 results at a time to prevent
Browse files Browse the repository at this point in the history
spot performance issues.

Add new envrionment variable call ASTROID_MAX_INFERABLE to tune
the max inferable amount of values at a time.

Close pylint-dev#579
Close pylint-dev/pylint#2251
  • Loading branch information
brycepg committed Jul 6, 2018
1 parent 0b7638a commit b2555e7
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 1 deletion.
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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?
============================
Expand Down
2 changes: 2 additions & 0 deletions astroid/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*."""
Expand Down
4 changes: 3 additions & 1 deletion astroid/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions astroid/tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import sys
from functools import partial
import unittest
from unittest.mock import patch

import pytest

Expand Down Expand Up @@ -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()
34 changes: 34 additions & 0 deletions astroid/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit b2555e7

Please sign in to comment.