Skip to content

Instantly share code, notes, and snippets.

@TRManderson
Last active February 8, 2017 00:37
Show Gist options
  • Save TRManderson/f04f1b2724a523b5912649bfa0028ac4 to your computer and use it in GitHub Desktop.
Save TRManderson/f04f1b2724a523b5912649bfa0028ac4 to your computer and use it in GitHub Desktop.
Testing the new metaclass support for mypy in Py2
__pycache__/
*.py[cod]
*$py.class
pip-log.txt
pip-delete-this-directory.txt
.venv

mypy Type-checker Metaclass Tests

The mypy type-checker recently merged a pull request for metaclass support in mypy. The goal here is to test how far that support extends in Python 2, given mypy didn't even parse Python 2 metaclass declarations previously.

Test Cases

  • py2_example1.py is testing cases where the metaclass is defined before its instance, then passing in by name to the __metaclass__ attribute
  • py2_example2.py consists of my test cases from python/mypy#2413, modified for python 2
  • py2_example3.py is testing metaclasses defined as a child class named __metaclass__
  • py3_example1.py is the equivalent of py2/example1.py but with Python 3 metaclass declarations
  • py3_example2.py consists of my test cases from python/mypy#2413
  • six.py is testing metclasses defined like in py2/example1.py, but declared using six.with_metaclass

Running tests

If you're not familliar with Github Gist, you can use the URL of this gist to clone it as a git repository.

./setup.sh blah will set up a venv virtualenv at path blah (or .venv if no path is passed) and install mypy at the metaclass support commit. You will have to source into the virtualenv after that. (source .venv/bin/activate)

./run_mypy.sh will typecheck the example files using the current Python environment (make sure you're in a venv with mypy and Python 3).

Conclusions

It's pretty clear that metaclass detection is still not happening in mypy for Python 2 codebases, but if a metaclass is detected (ie in Python 3) then everything seems to work as expected.

Errors to do with the six.py test case are due to the fact that six.with_metaclass needs to be special-cased, and it hasn't been special-cased yet.

All in all, this is still fantastic work, and I'm very grateful for it. Still, it means that I'll have to look into updating the metaclass detection logic to support Python 2.

Update

Since the pull request, several issues on the mypy repository have been updated and/or opened, which will fix all the above issues I found (among others):

  • mypy doesn't understand six.with_metaclass (#1764)
  • Mypy doesn't recognize metaclass implementing getitem (#1771)
  • Add Python 2 support for metaclass (#2823)
  • Metaclass is not properly inherited when using multiple inheritance with int (#2824)
  • Update the behaviour of the type function for metaclasses (#2826)
class MetaA(type):
arg1 = 1 # type: float
@property
def arg2(cls):
# type: () -> int
return 2
name = "metaclass A" # type: str
def return_name_class(cls):
return cls.name
class A(object):
__metaclass__ = MetaA
name = "class A"
def __init__(self, name):
self.name = name
def return_name_instance(self):
return self.name
assert A.return_name_class() == A.name
instance1 = A("instance1")
assert instance1.return_name_instance() == "instance1"
instance2 = A("instance2")
assert type(instance2).return_name_class() == A.name
reveal_type = lambda *x: None
reveal_type(instance_A)
reveal_type(type(instance_A))
reveal_type(A)
reveal_type(A.arg1)
reveal_type(A.arg2)
class MetaA(type):
@property
def prop(cls):
return "Hi!"
class ConcreteA(object):
__metaclass__ = MetaA
pass
print(ConcreteA.prop)
class MetaB(type):
attr = "attr"
def fn(cls):
return "fn"
class ConcreteB(object):
__metaclass__ = MetaB
pass
print(ConcreteB.fn())
print(ConcreteB.attr)
class A(object):
class __metaclass__(type):
arg1 = 1 # type: float
@property
def arg2(cls):
# type: () -> int
return 2
name = "metaclass A" # type: str
def return_name_class(cls):
return cls.name
name = "class A"
def __init__(self, name):
self.name = name
def return_name_instance(self):
return self.name
instance_A = A("instance A") # type; A
assert A.return_name_class() == A.name
instance1 = A("instance1")
assert instance1.return_name_instance() == "instance1"
instance2 = A("instance2")
assert type(instance2).return_name_class() == A.name
reveal_type = lambda *x: None
reveal_type(instance_A)
reveal_type(type(instance_A))
reveal_type(A)
reveal_type(A.arg1)
reveal_type(A.arg2)
class MetaA(type):
arg1 = 1 # type: float
@property
def arg2(cls):
# type: () -> int
return 2
name = "metaclass A" # type: str
def return_name_class(cls):
return cls.name
class A(object, metaclass=MetaA):
name = "class A"
def __init__(self, name):
self.name = name
def return_name_instance(self):
return self.name
assert A.return_name_class() == A.name
instance1 = A("instance1")
assert instance1.return_name_instance() == "instance1"
instance2 = A("instance2")
assert type(instance2).return_name_class() == A.name
reveal_type = lambda *x: None
reveal_type(instance_A)
reveal_type(type(instance_A))
reveal_type(A)
reveal_type(A.arg1)
reveal_type(A.arg2)
class MetaA(type):
@property
def prop(cls):
return "Hi!"
class ConcreteA(object, metaclass=MetaA):
pass
print(ConcreteA.prop)
class MetaB(type):
attr = "attr"
def fn(cls):
return "fn"
class ConcreteB(object, metaclass=MetaB):
pass
print(ConcreteB.fn())
print(ConcreteB.attr)
-e git://github.com/python/mypy.git@cab1e0d1a676f10aa947f35dd07d8fe77532b010#egg=mypy
echo "Python 2 tests"
mypy --show-traceback --fast-parser -2 six.py
mypy --show-traceback --fast-parser -2 py2_example1.py
mypy --show-traceback --fast-parser -2 py2_example2.py
mypy --show-traceback --fast-parser -2 py2_example3.py
echo ""
echo "Python 3 tests"
mypy --show-traceback --fast-parser six.py
mypy --show-traceback --fast-parser py3_example1.py
mypy --show-traceback --fast-parser py3_example2.py
#!/usr/bin/env bash
VENV_NAME="${1:-.venv}"
echo "Setting up virtualenv as $VENV_NAME"
virtualenv $VENV_NAME --python=python3
source .venv_typecheck/bin/activate
pip install -r requirements.txt
from six import with_metaclass
class MetaA(type):
arg1 = 1 # type: float
@property
def arg2(cls):
# type: () -> int
return 2
name = "metaclass A" # type: str
def return_name_class(cls):
return cls.name
class A(with_metaclass(MetaA, object)):
name = "class A"
def __init__(self, name):
self.name = name
def return_name_instance(self):
return self.name
instance_A = A("instance A") # type; A
assert A.return_name_class() == A.name
instance1 = A("instance1")
assert instance1.return_name_instance() == "instance1"
instance2 = A("instance2")
assert type(instance2).return_name_class() == A.name
reveal_type = lambda *x: None
reveal_type(instance_A)
reveal_type(type(instance_A))
reveal_type(A)
reveal_type(A.arg1)
reveal_type(A.arg2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment