Repository: quora/pyanalyze
Pull request #354 (2021-12-20 03:46:56)

runtime positional-only parameters


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index c2b60e9..7349805 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Support positional-only arguments with the `__` prefix
+  outside of stubs (#353)
 - Add basic support for `ParamSpec` (#352)
 - Fix error on use of `AbstractAsyncContextManager` (#350)
 - Check `with` and `async with` statements (#344)
diff --git a/pyanalyze/analysis_lib.py b/pyanalyze/analysis_lib.py
index f98d7d0..a08948e 100644
--- a/pyanalyze/analysis_lib.py
+++ b/pyanalyze/analysis_lib.py
@@ -99,3 +99,13 @@ def make_module(
     exec(code_str, scope)
     sys.modules[module_name] = mod
     return mod
+
+
+def is_positional_only_arg_name(name: str, class_name: Optional[str] = None) -> bool:
+    # https://www.python.org/dev/peps/pep-0484/#positional-only-arguments
+    # Work around Python's name mangling
+    if class_name is not None:
+        prefix = f"_{class_name}"
+        if name.startswith(prefix):
+            name = name[len(prefix) :]
+    return name.startswith("__") and not name.endswith("__")
diff --git a/pyanalyze/arg_spec.py b/pyanalyze/arg_spec.py
index d9a5739..1ddce6a 100644
--- a/pyanalyze/arg_spec.py
+++ b/pyanalyze/arg_spec.py
@@ -4,6 +4,7 @@ Implementation of extended argument specifications used by test_scope.
 
 """
 
+from .analysis_lib import is_positional_only_arg_name
 from .extensions import get_overloads
 from .annotations import Context, type_from_runtime
 from .config import Config
@@ -242,12 +243,16 @@ class ArgSpecCache:
             default = None
         else:
             default = KnownValue(parameter.default)
-        return SigParameter(
-            parameter.name,
-            ParameterKind(parameter.kind),
-            default=default,
-            annotation=typ,
-        )
+        if (
+            parameter.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
+            and is_positional_only_arg_name(
+                parameter.name, _get_class_name(function_object)
+            )
+        ):
+            kind = ParameterKind.POSITIONAL_ONLY
+        else:
+            kind = ParameterKind(parameter.kind)
+        return SigParameter(parameter.name, kind, default=default, annotation=typ)
 
     def _get_type_for_parameter(
         self,
@@ -681,3 +686,11 @@ def _get_fully_qualified_name(obj: object) -> Optional[str]:
     if hasattr(obj, "__module__") and hasattr(obj, "__qualname__"):
         return f"{obj.__module__}.{obj.__qualname__}"
     return None
+
+
+def _get_class_name(obj: object) -> Optional[str]:
+    if hasattr(obj, "__qualname__"):
+        pieces = obj.__qualname__.split(".")
+        if len(pieces) >= 2:
+            return pieces[-2]
+    return None
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index 3e0420f..5147cf5 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -4,6 +4,7 @@ Code for getting annotations from typeshed (and from third-party stubs generally
 
 """
 
+from .analysis_lib import is_positional_only_arg_name
 from .annotations import Context, type_from_value, value_from_ast
 from .error_code import ErrorCode
 from .safe import is_typing_name
@@ -745,8 +746,9 @@ class TypeshedFinder:
                     typ = TypedValue(objclass)
 
         name = arg.arg
-        # Arguments that start with __ are positional-only in typeshed
-        if kind is ParameterKind.POSITIONAL_OR_KEYWORD and name.startswith("__"):
+        if kind is ParameterKind.POSITIONAL_OR_KEYWORD and is_positional_only_arg_name(
+            name
+        ):
             kind = ParameterKind.POSITIONAL_ONLY
             name = name[2:]
         # Mark self as positional-only. objclass should be given only if we believe
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index d182b38..30e4a15 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -1959,7 +1959,7 @@ IterableValue = GenericValue(collections.abc.Iterable, [TypeVarValue(T)])
 
 
 class GetItemProto(Protocol[T]):
-    def __getitem__(self, i: int) -> T:
+    def __getitem__(self, __i: int) -> T:
         raise NotImplementedError
 
 


Test patch:
diff --git a/pyanalyze/test_arg_spec.py b/pyanalyze/test_arg_spec.py
index 6b27098..34c2155 100644
--- a/pyanalyze/test_arg_spec.py
+++ b/pyanalyze/test_arg_spec.py
@@ -249,6 +249,43 @@ def test_get_argspec():
         ) == ArgSpecCache(config).get_argspec(NT)
 
 
+def test_positional_only():
+    def f(__x, _f__x):
+        pass
+
+    class Y:
+        def f(self, __x):
+            pass
+
+        class X:
+            def f(self, __x, _Y__x):
+                pass
+
+    asc = ArgSpecCache(ConfiguredNameCheckVisitor.config)
+    assert asc.get_argspec(f) == Signature.make(
+        [
+            SigParameter("__x", ParameterKind.POSITIONAL_ONLY),
+            SigParameter("_f__x", ParameterKind.POSITIONAL_OR_KEYWORD),
+        ],
+        callable=f,
+    )
+    assert asc.get_argspec(Y.f) == Signature.make(
+        [
+            SigParameter("self", ParameterKind.POSITIONAL_OR_KEYWORD),
+            SigParameter("_Y__x", ParameterKind.POSITIONAL_ONLY),
+        ],
+        callable=Y.f,
+    )
+    assert asc.get_argspec(Y.X.f) == Signature.make(
+        [
+            SigParameter("self", ParameterKind.POSITIONAL_OR_KEYWORD),
+            SigParameter("_X__x", ParameterKind.POSITIONAL_ONLY),
+            SigParameter("_Y__x", ParameterKind.POSITIONAL_OR_KEYWORD),
+        ],
+        callable=Y.X.f,
+    )
+
+
 def test_is_dot_asynq_function():
     assert not is_dot_asynq_function(async_function)
     assert is_dot_asynq_function(async_function.asynq)
diff --git a/pyanalyze/test_value.py b/pyanalyze/test_value.py
index 75ba882..c5fe638 100644
--- a/pyanalyze/test_value.py
+++ b/pyanalyze/test_value.py
@@ -541,8 +541,8 @@ def test_concrete_values_from_iterable() -> None:
     )
 
     class HasGetItem:
-        def __getitem__(self, i: int) -> str:
-            return str(i)
+        def __getitem__(self, some_random_name: int) -> str:
+            return str(some_random_name)
 
     assert concrete_values_from_iterable(TypedValue(HasGetItem), CTX) == TypedValue(str)