Repository: quora/pyanalyze
Pull request #306 (2021-12-03 16:44:59)

fix false positive in asynq checker


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 619bd91..5f64947 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Fix false positive where `multiprocessing.Pool.map_async`
+  was identified as an asynq method (#306)
 - Fix handling of nested classes (#305)
 - Support Protocols for runtime types that are also defined in stubs (#297)
 - Better detect signatures of methods in stub files (#304)
diff --git a/pyanalyze/asynq_checker.py b/pyanalyze/asynq_checker.py
index fe8cdc2..7a3f446 100644
--- a/pyanalyze/asynq_checker.py
+++ b/pyanalyze/asynq_checker.py
@@ -14,9 +14,9 @@ import inspect
 import types
 from typing import Sequence, Any, Callable, List, Optional, Iterable
 
-
 from .config import Config
 from .error_code import ErrorCode
+from .safe import safe_getattr, safe_hasattr
 from .stacked_scopes import Composite
 from .value import AnnotatedValue, Value, KnownValue, TypedValue, UnboundMethodValue
 
@@ -98,21 +98,28 @@ class AsynqChecker:
             and isinstance(value.composite.value, TypedValue)
         ):
             inner_type = value.composite.value.typ
-            if hasattr(inner_type, value.attr_name + "_async"):
-                if isinstance(node.func, ast.Attribute):
-                    func_node = ast.Attribute(
-                        value=node.func.value, attr=value.attr_name + "_async"
-                    )
-                    call_node = replace_func_on_call_node(node, func_node)
-                    replacement_node = ast.Yield(value=call_node)
-                else:
-                    replacement_node = None
-                self._show_impure_async_error(
-                    node,
-                    replacement_call="%s.%s_async"
-                    % (_stringify_obj(inner_type), value.attr_name),
-                    replacement_node=replacement_node,
+            if not safe_hasattr(inner_type, value.attr_name + "_async"):
+                return
+            module = safe_getattr(inner_type, "__module__", None)
+            if (
+                isinstance(module, str)
+                and module.split(".")[0] in self.config.NON_ASYNQ_MODULES
+            ):
+                return
+            if isinstance(node.func, ast.Attribute):
+                func_node = ast.Attribute(
+                    value=node.func.value, attr=value.attr_name + "_async"
                 )
+                call_node = replace_func_on_call_node(node, func_node)
+                replacement_node = ast.Yield(value=call_node)
+            else:
+                replacement_node = None
+            self._show_impure_async_error(
+                node,
+                replacement_call="%s.%s_async"
+                % (_stringify_obj(inner_type), value.attr_name),
+                replacement_node=replacement_node,
+            )
 
     def record_attribute_access(
         self, root_value: Value, attr_name: str, node: ast.Attribute
diff --git a/pyanalyze/config.py b/pyanalyze/config.py
index 64e5ec6..14d0460 100644
--- a/pyanalyze/config.py
+++ b/pyanalyze/config.py
@@ -230,6 +230,9 @@ class Config(object):
     # BASE_CLASSES_CHECKED_FOR_ASYNQ
     METHODS_NOT_CHECKED_FOR_ASYNQ = set()
 
+    # We ignore _async methdos in these modules.
+    NON_ASYNQ_MODULES = {"multiprocessing"}
+
     #
     # Used by method_return_type.py
     #


Test patch:
diff --git a/pyanalyze/test_asynq_checker.py b/pyanalyze/test_asynq_checker.py
index 98aabaf..73163ea 100644
--- a/pyanalyze/test_asynq_checker.py
+++ b/pyanalyze/test_asynq_checker.py
@@ -355,7 +355,8 @@ class TestAsyncMethods(TestNameCheckVisitorBase):
 
     @assert_passes()
     def test_pure_asynq_method(self):
-        from asynq import asynq, ConstFuture
+        from asynq import asynq
+        import multiprocessing
 
         class Capybara(object):
             def __init__(self, x):
@@ -372,6 +373,9 @@ class TestAsyncMethods(TestNameCheckVisitorBase):
         def use_capybara(x):
             cap = Capybara(x)
             assert_is_value((yield cap.get_async()), TypedValue(int))
+            pool = multiprocessing.Pool()
+            # should not try to use map_async
+            pool.map(len, [(1,)])
 
 
 def test_stringify_async_fn():