Repository: quora/pyanalyze
Pull request #341 (2021-12-15 06:10:25)

various cleanup
- Drop unnecessary dependencies
- Add another resource on overloads
- Remove some unused imports


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 93566a5..787241f 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,9 @@
 
 ## Unreleased
 
+- The first argument to `__new__` and `__init_subclass`
+  does not need to be `self` (#342)
+- Drop dependencies on `attrs` and `mypy_extensions` (#341)
 - Correct location of error for incompatible parameter (#339)
 
 ## Version 0.5.0 (December 12, 2021)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index 3c85034..b742a2e 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -31,7 +31,6 @@ import ast
 from typed_ast import ast3
 import builtins
 from collections.abc import Callable, Iterable
-from typed_ast import ast3
 from typing import (
     Any,
     Container,
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 776a95c..bf5bc13 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -149,6 +149,7 @@ ExceptionValue = TypedValue(BaseException) | SubclassValue(TypedValue(BaseExcept
 ExceptionOrNone = ExceptionValue | KnownNone
 FunctionNode = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda]
 
+IMPLICIT_CLASSMETHODS = ("__init_subclass__", "__new__")
 
 BINARY_OPERATION_TO_DESCRIPTION_AND_METHOD = {
     ast.Add: ("addition", "__add__", "__iadd__", "__radd__"),
@@ -1750,9 +1751,9 @@ class NameCheckVisitor(
                             )
                 elif is_self:
                     assert self.current_class is not None
-                    if function_info.is_classmethod or getattr(node, "name", None) in (
-                        "__init_subclass__",
-                        "__new__",
+                    if (
+                        function_info.is_classmethod
+                        or getattr(node, "name", None) in IMPLICIT_CLASSMETHODS
                     ):
                         value = SubclassValue(TypedValue(self.current_class))
                     else:
@@ -1831,6 +1832,8 @@ class NameCheckVisitor(
         # try to confirm that it's actually a method
         if not hasattr(node, "name") or not hasattr(self.current_class, node.name):
             return
+        if node.name in IMPLICIT_CLASSMETHODS:
+            return
         first_must_be = "cls" if function_info.is_classmethod else "self"
 
         if len(node.args.args) < 1 or len(node.args.defaults) == len(node.args.args):
diff --git a/pyanalyze/safe.py b/pyanalyze/safe.py
index 8ec31b0..889350d 100644
--- a/pyanalyze/safe.py
+++ b/pyanalyze/safe.py
@@ -8,7 +8,11 @@ import sys
 from typing import Any, Dict, Sequence, Tuple, Union, Container, NewType, Type, TypeVar
 import typing
 import typing_extensions
-import mypy_extensions
+
+try:
+    import mypy_extensions
+except ImportError:
+    mypy_extensions = None
 
 T = TypeVar("T")
 
@@ -138,6 +142,8 @@ def _fill_typing_name_cache(name: str) -> Tuple[Tuple[Any, ...], Tuple[str, ...]
         objs = []
         names = []
         for mod in (typing, typing_extensions, mypy_extensions):
+            if mod is None:
+                continue
             try:
                 objs.append(getattr(mod, name))
                 names.append(f"{mod}.{name}")
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 209d0b5..ca60c56 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -1546,6 +1546,8 @@ class OverloadedSignature:
           of mypy's behavior.
         - Eric Traut's `discussion <https://github.com/microsoft/pyright/issues/2521#issuecomment-956823577>`_
           of pyright's behavior.
+        - The `documentation <https://github.com/microsoft/pyright/blob/main/docs/type-concepts.md#overloads>`_
+          for pyright's behavior.
 
         Our behavior is closer to mypy. The general rule is to pick the first overload that matches
         and return an error otherwise, but there are two twists: ``Any`` and unions.
diff --git a/requirements.txt b/requirements.txt
index 4f20c0c..a32e644 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,4 @@
 # Needed for ReadTheDocs
-attrs
 asynq
 dataclasses; python_version < '3.7'
 qcore>=0.5.1
@@ -7,7 +6,6 @@ ast_decompiler>=0.4.0
 typeshed_client>=1.0.0
 typing_inspect>=0.5.0
 typing_extensions
-mypy_extensions
 aenum>=2.2.3
 codemod
 myst-parser==0.15.1
diff --git a/setup.py b/setup.py
index 4c6c7db..d6a6846 100644
--- a/setup.py
+++ b/setup.py
@@ -48,7 +48,6 @@ if __name__ == "__main__":
         keywords="quora static analysis",
         packages=["pyanalyze"],
         install_requires=[
-            "attrs",
             "asynq",
             "dataclasses; python_version < '3.7'",
             "qcore>=0.5.1",
diff --git a/tox.ini b/tox.ini
index 80d75f1..293fbd7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,7 @@ skip_missing_interpreters = True
 [testenv]
 deps =
     pytest
+    mypy_extensions
 
 commands =
     pytest pyanalyze/


Test patch:
diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py
index 92459e0..a519c69 100644
--- a/pyanalyze/test_annotations.py
+++ b/pyanalyze/test_annotations.py
@@ -913,7 +913,7 @@ class TestTypeVar(TestNameCheckVisitorBase):
 
     @assert_passes()
     def test_callable_compatibility(self):
-        from typing import TypeVar, Callable, Union, Iterable, Any
+        from typing import TypeVar, Callable, Union, Iterable
         from typing_extensions import Protocol
 
         AnyStr = TypeVar("AnyStr", bytes, str)
diff --git a/pyanalyze/test_attributes.py b/pyanalyze/test_attributes.py
index 362271a..6a59a1e 100644
--- a/pyanalyze/test_attributes.py
+++ b/pyanalyze/test_attributes.py
@@ -235,7 +235,6 @@ class TestAttributes(TestNameCheckVisitorBase):
     @assert_passes()
     def test_module_annotations(self):
         from pyanalyze import test_attributes
-        from pyanalyze.type_object import TypeObject
         from typing import Optional
 
         annotated_global: Optional[str] = None
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 7187682..c4e8757 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -504,12 +504,9 @@ def run():
             def __new__(cls):
                 assert_is_value(cls, SubclassValue(TypedValue(OldStyle)))
 
-    @assert_passes()
-    def test_cls_type_inference(self):
-        class OldStyle:
             @classmethod
             def capybara(cls):
-                pass
+                assert_is_value(cls, SubclassValue(TypedValue(OldStyle)))
 
     @assert_passes()
     def test_display_type_inference(self):
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index 47bca8e..17742cb 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -1082,7 +1082,7 @@ class TestOverload(TestNameCheckVisitorBase):
     @assert_passes()
     def test_any_and_union(self):
         from pyanalyze.extensions import overload
-        from typing import List, Any, Union
+        from typing import Any, Union
         from typing_extensions import Literal
 
         @overload