Skip to content

Static and run-time tests for stubs #917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ matrix:
env: TEST_CMD="./tests/mypy_test.py"
- python: "2.7"
env: TEST_CMD="./tests/pytype_test.py --num-parallel=4"
- python: "3.5"
env: TEST_CMD="./tests/runtime_test.py"
- python: "2.7"
env: TEST_CMD="./tests/runtime_test.py"

install:
# pytype needs py-2.7, mypy needs py-3.2+. Additional logic in runtests.py
- if [[ $TRAVIS_PYTHON_VERSION == '3.6-dev' ]]; then pip install -U flake8==3.2.1 flake8-bugbear>=16.12.2 flake8-pyi>=16.12.2; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install -U git+git://github.com/python/mypy git+git://github.com/dropbox/typed_ast; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -U git+git://github.com/google/pytype; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install -U pytest git+git://github.com/python/mypy git+git://github.com/dropbox/typed_ast; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -U pytest git+git://github.com/google/pytype; fi

script:
- $TEST_CMD
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ are important to the project's success.
* [Contact us](#discussion) before starting significant work.
* IMPORTANT: For new libraries, [get permission from the library owner first](#adding-a-new-library).
* Create your stubs [conforming to the coding style](#stub-file-coding-style).
* Optionally, [add some unit tests for your stubs](#adding-tests-for-stubs).
* Make sure `runtests.sh` passes cleanly on Mypy, pytype, and flake8.
4. [Submit your changes](#submitting-changes):
* Open a pull request
Expand Down Expand Up @@ -213,6 +214,69 @@ documentation. Whenever you find them disagreeing, model the type
information after the actual implementation and file an issue on the
project's tracker to fix their documentation.

### Adding tests for stubs

If you're fixing a stub file due to some false errors reported by your type
checker, consider adding a unit test that would show that your changes actually
fix these false errors.

For example, Mypy is showing false errors about a hypothetical `example`
module:

```python
import example

example.foo() # error: "module" has no attribute "foo"
example.bar(10) # error: Argument 1 to "bar" has incompatible type "int"
```

You might start with a pytest test that passes at run-time, but fails
(incorrectly) during type checking. For a third-party library `example` that
runs on both Python 2 and 3 put it into
`test_data/third_party/2and3/example_test.py`:

```python
def test_foo_bar():
import example

example.foo()
assert example.bar(10) - 10 == 0
```

Since `example` is a third-party library, create `example_requirements.txt`
with the requirements specs for your test next to your test file:

```python
example==1.2.0
```

pytest will install the requirements automatically via a test fixture.

You should put your third-party imports inside test functions in your test file,
so the imports are not executed until pytype installs your requirements.

Then, run both static and run-time tests for your test data. Alternatively, run
them for all the test data:

```python
./tests/mypy_test.py
./tests/runtime_test.py
```

The first one should fail with a false error (since you haven't fixed it yet)
while the second one should pass, showing that you're using the API of `example`
correctly.

Next, add the stub file `third_party/2and3/example.pyi` that actually fixes
these false errors:

```python
def foo() -> None: ...
def bar(x: int) -> int: ...
```

Finally, re-run the tests to make sure the problem has gone.

## Issue-tracker conventions

We aim to reply to all new issues promptly. We'll assign a milestone
Expand Down
12 changes: 12 additions & 0 deletions test_data/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import re
import pip
import pytest


@pytest.fixture(scope='module')
def requirements(request):
requirements_path = re.sub(r'(.*)_test\.py', r'\1_requirements.txt',
request.module.__file__)
pip.main(['install', '-r', requirements_path])
yield
# We could uninstall everything here after the module tests finish
2 changes: 2 additions & 0 deletions test_data/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
usefixtures = requirements
14 changes: 14 additions & 0 deletions test_data/stdlib/2and3/collections_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def test_namedtuple():
from collections import namedtuple

Point = namedtuple('Point', 'x y')
p = Point(1, 2)

assert p == Point(1, 2)
assert p == (1, 2)
assert p._replace(y=3.14).y == 3.14
assert p._asdict()['x'] == 1
assert (p.x, p.y) == (1, 2)
assert p[0] + p[1] == 3
assert p.index(1) == 0
assert Point._make([1, 3.14]).y == 3.14
1 change: 1 addition & 0 deletions test_data/third_party/2and3/six_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
six==1.10.0
13 changes: 13 additions & 0 deletions test_data/third_party/2and3/six_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def test_python_checks():
from six import PY2, PY3

assert PY2 ^ PY3


def test_xrange():
from six.moves import xrange

xs = xrange(5)
assert xs.__iter__
assert xs[0] == 0
assert sum(xs) == 10
3 changes: 2 additions & 1 deletion tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def libpath(major, minor):
versions.append('2and3')
paths = []
for v in versions:
for top in ['stdlib', 'third_party']:
for top in ['stdlib', 'third_party', 'test_data/stdlib', 'test_data/third_party']:
p = os.path.join(top, v)
if os.path.isdir(p):
paths.append(p)
Expand Down Expand Up @@ -124,6 +124,7 @@ def main():
runs += 1
flags = ['--python-version', '%d.%d' % (major, minor)]
flags.append('--strict-optional')
flags.append('--check-untyped-defs')
if (major, minor) >= (3, 6):
flags.append('--fast-parser')
# flags.append('--warn-unused-ignores') # Fast parser and regular parser disagree.
Expand Down
29 changes: 29 additions & 0 deletions tests/runtime_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python

import pytest
import sys
import os


def main():
if sys.version_info < (3, 0):
version_dirs = [
'2and3',
'2',
]
else:
version_dirs = [
'2and3',
'3',
'%d.%d' % (sys.version_info[0], sys.version_info[1]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should actually be all subdirectories from sys.version_info[1] upwards: when you're running 3.4 for example type checkers will look at the subdirectories for 3.3 and 3.4 but not 3.5.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JelleZijlstra I believe it doesn't apply to run-time checks since we run them on a particular version of the interpreter and it may not have some module from older Python versions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in typeshed directories like 3.4 apply also to newer versions. For example, asyncio is in stdlib/3.4 because it was introduced in 3.4, but the stubs are also used for 3.5 and 3.6. I think tests should behave the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there was some backwards incompatible change in the API of a module in older Python 3, the tests for the current version may fail. If it is absolutely necessary to run tests for some older Python 3.N module, we will set up another Travis environment with Python 3.N.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be true, but it's certainly confusing, since you are using the same directory naming scheme as for the stubs, and for stubs the rule that Jelle explains is essential (otherwise e.g. asyncio would not be found for Python 3.5 or higher).

It also points, again, to yet another reason why I don't believe these runtime tests are going to help much (it's not checking that the sys.version_info checks in the stubs are correct).

]
top_dirs = ['stdlib', 'third_party']
possible_paths = [os.path.join('test_data', t, v)
for t in top_dirs
for v in version_dirs]
paths = [path for path in possible_paths if os.path.exists(path)]
pytest.main(paths)


if __name__ == '__main__':
main()