Skip to content
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

Add support for .pyi type stubs (including for first-party) #10759

Merged
merged 12 commits into from Sep 28, 2020

Conversation

Eric-Arellano
Copy link
Contributor

@Eric-Arellano Eric-Arellano commented Sep 11, 2020

There are two use cases for writing .pyi type stubs:

  1. Writing bindings for third party libraries.
  2. Writing stubs for your own code because you prefer it over using type hints/comments.

This PR adds support for both use cases. We allow Python targets to now include .pyi files, and update the default globs for python_library and python_tests to include them.

We also teach dependency inference about .pyi files. For third party reqs, we infer a dep on both the req and the .pyi file. For first party stubs, we infer a dep on both the .py and .pyi files.

Most of our Python functionality works correctly without changes for .pyi files, but we have to teach Pytest to not try running over .pyi files if they show up in python_tests targets, just like we special case conftest.py. We also update how we call MyPy; if there is a .pyi file and .py file, the .pyi file takes precedence; otherwise, MyPy would error.

[ci skip-rust]
[ci skip-build-wheels]

@Eric-Arellano Eric-Arellano changed the title WIP: Add support for .pyi type stubs WIP: Add support for .pyi type stubs (including for first-party) Sep 11, 2020
@Eric-Arellano
Copy link
Contributor Author

Eric-Arellano commented Sep 15, 2020

Hey @cristianmatache, if you have the time, it'd be helpful to add tests that Black, Docformatter, and Isort all correctly format .pyi files. I don't think you'll need to make changes to the actual implementations; the tests are only for us to ensure we do the right thing.

First, check out https://www.pantsbuild.org/v2.0/docs/contributor-overview and then https://www.pantsbuild.org/v2.0/docs/contributor-setup for how to get set up. Warning that compiling Rust is slow, but cached.

Then, add a new test to each of {black,isort,docformatter}/rules_integration_test.py. The tests will all be extremely similar to

def test_passing_source(rule_runner: RuleRunner) -> None:
target = make_target_with_origin(rule_runner, [GOOD_SOURCE])
lint_results, fmt_result = run_isort(rule_runner, [target])
assert len(lint_results) == 1
assert lint_results[0].exit_code == 0
assert lint_results[0].stderr == ""
assert fmt_result.stdout == ""
assert fmt_result.output == get_digest(rule_runner, [GOOD_SOURCE])
assert fmt_result.did_change is False
def test_failing_source(rule_runner: RuleRunner) -> None:
target = make_target_with_origin(rule_runner, [BAD_SOURCE])
lint_results, fmt_result = run_isort(rule_runner, [target])
assert len(lint_results) == 1
assert lint_results[0].exit_code == 1
assert "bad.py Imports are incorrectly sorted" in lint_results[0].stderr
assert fmt_result.stdout == "Fixing bad.py\n"
assert fmt_result.output == get_digest(rule_runner, [FIXED_BAD_SOURCE])
assert fmt_result.did_change is True

I think everything will be the same, except you'll set up a different file that ends in .pyi rather than .py. You might want to do a "failing" file, as that's better verification that the tool is actually doing something.

Doesn't need to be super exhaustive - testing one file should be adequate, and avoids slowing down CI too much.

Although, maybe we should do a more robust test that includes a .py and .pyi file for the same module? Ensure that it doesn't trip up the formatters? I see no reason why it would - they don't care if two files are the same module. But maybe a good idea, regardless.

Feel free to push directly to this branch! (Lmk if I need to give you permissions.)

DONE:
* Loosen target types
* Teach Pytest to skip type stubs
* Update dep inference

TODO:
* Add tests to MyPy

[ci skip-rust]
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
@Eric-Arellano Eric-Arellano changed the title WIP: Add support for .pyi type stubs (including for first-party) Add support for .pyi type stubs (including for first-party) Sep 26, 2020
@Eric-Arellano Eric-Arellano marked this pull request as ready for review September 26, 2020 00:50
[ci skip-rust]
[ci skip-build-wheels]
@@ -129,20 +129,20 @@ def test_generate_chroot(chroot_rule_runner: RuleRunner) -> None:
"src/python/foo/bar/baz",
textwrap.dedent(
"""
python_distribution(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This only fixes indentation.

Comment on lines +253 to +259
"test_*.py",
"*_test.py",
"tests.py",
"conftest.py",
"test_*.pyi",
"*_test.pyi",
"tests.pyi",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't love this. But I think it's important to include .pyi in python_tests too. If a user for some reason writes type stubs for their tests, then we don't want it to show up in, say, setup-py. That is, we don't want the test type stubs defaulting to a python_library.

(Even though it's really weird to write a type stub file for your test)

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love this.

Is it just the number of lines to express it? Or you don't like how much it globs?

I might recommend putting the .pyi globs right after the .py globs, I think it might make it easier to read. But it's fine either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like how many globs we now have. But I do think that this is the correct behavior. Just clunky.

@Eric-Arellano
Copy link
Contributor Author

This still needs tests for the formatters, Flake8, and Pylint, which Cristian and I working on. But otherwise, this is good to go.

# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
Copy link
Contributor Author

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

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

Thank you Cristian!

@@ -1,6 +1,6 @@
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Other reviewers: credit to @cristianmatache for writing this file. Thank you!)

@@ -1,6 +1,7 @@
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Ditto on Cristian adding this change)

@coveralls
Copy link

coveralls commented Sep 27, 2020

Coverage Status

Coverage remained the same at 0.0% when pulling 8657bfc on Eric-Arellano:python-type-stubs into f968daf on pantsbuild:master.

Copy link
Contributor

@cosmicexplorer cosmicexplorer left a comment

Choose a reason for hiding this comment

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

I'm leaving it as "comment" in case you want to make any of the changes e.g. to add any of the comments I suggested, but to be clear, I think you can safely consider all of these suggestions as mostly optional. I tried to be useful and precise with the text suggestions, but those should also be optional. Once you mark each of them as resolved I will approve.

I wasn't trying to be pedantic here, but I am very unfamiliar with how type stubs work and only a little more familiar with dependency inference. Anything that seems like it should be common enough knowledge, you can feel free to ignore.

def addresses_for_module(self, module: str) -> Tuple[Address, ...]:
targets = self.mapping.get(module)
if targets:
return targets
# If the module is not found, try the parent, if any. This is to accommodate `from`
# imports, where we don't care about the specific symbol, but only the module. For example,
# with `from typing import List`, we only care about `typing`.
# Unlike with third party modules, we do not look past the direct parent.
Copy link
Contributor

Choose a reason for hiding this comment

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

We say "unlike with third party modules", but then provide an example with typing above -- could we provide a more relevant example? It's also not clear to me why the parent module is only significant for from imports (wouldn't we need to check the parent for import pants.x.y too?).

Comment on lines +253 to +259
"test_*.py",
"*_test.py",
"tests.py",
"conftest.py",
"test_*.pyi",
"*_test.pyi",
"tests.pyi",
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love this.

Is it just the number of lines to express it? Or you don't like how much it globs?

I might recommend putting the .pyi globs right after the .py globs, I think it might make it easier to read. But it's fine either way.

@@ -119,6 +119,25 @@ def config_path_globs(mypy: MyPy) -> PathGlobs:
)


def determine_python_files(files: Iterable[str]) -> Tuple[str, ...]:
"""We run over all .py and .pyi files, but .pyi files take precedence.
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, why is this? Why can we do py_file = f[:-1] below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why can we do py_file = f[:-1] below?

This gets the filename with the i removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, why is this?

MyPy will error if we say to run over the same module with both its .py and .pyi files, so we
must be careful to only use the .pyi stub.

MyPy only lets you choose one. Type stubs are supposed to take precedence over implementation files.

…place

# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
Copy link
Contributor Author

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

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

Thanks for the great suggestions, Danny!

return FirstPartyModuleToAddressMapping(
FrozenDict(
{
module: tuple(sorted(addresses))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's correct. sorted() returns a list, so we have to convert into a hashable/immutable type.

# requirement. Note that `colors.pyi` is at the top-level of the source root so that it strips
# to the module `colors`.
rule_runner.create_file("source_root1/colors.pyi")
rule_runner.add_to_build_file("source_root1", "python_library()\n")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doesn't look like it. It would be necessary if we wanted to call this method again on the same BUILD file though.

@@ -51,6 +51,7 @@ def test_infer_python_imports() -> None:
)

rule_runner.create_file("src/python/str_import/subdir/f.py")
rule_runner.create_file("src/python/str_import/subdir/f.pyi")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, particularly when we have .py and .pyi for the same module name. I can add a comment.

Comment on lines +253 to +259
"test_*.py",
"*_test.py",
"tests.py",
"conftest.py",
"test_*.pyi",
"*_test.pyi",
"tests.pyi",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like how many globs we now have. But I do think that this is the correct behavior. Just clunky.

@@ -119,6 +119,25 @@ def config_path_globs(mypy: MyPy) -> PathGlobs:
)


def determine_python_files(files: Iterable[str]) -> Tuple[str, ...]:
"""We run over all .py and .pyi files, but .pyi files take precedence.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why can we do py_file = f[:-1] below?

This gets the filename with the i removed.

@@ -119,6 +119,25 @@ def config_path_globs(mypy: MyPy) -> PathGlobs:
)


def determine_python_files(files: Iterable[str]) -> Tuple[str, ...]:
"""We run over all .py and .pyi files, but .pyi files take precedence.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, why is this?

MyPy will error if we say to run over the same module with both its .py and .pyi files, so we
must be careful to only use the .pyi stub.

MyPy only lets you choose one. Type stubs are supposed to take precedence over implementation files.

[ci skip-rust]
[ci skip-build-wheels]
[ci skip-rust]
[ci skip-build-wheels]
@Eric-Arellano
Copy link
Contributor Author

All the tests are now added: this is now ready for final reviews. Thanks!

Copy link
Sponsor Member

@stuhood stuhood left a comment

Choose a reason for hiding this comment

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

Looks great, thanks!

Too much copy pasta

[ci skip-rust]
[ci skip-build-wheels]
@Eric-Arellano Eric-Arellano merged commit f90b982 into pantsbuild:master Sep 28, 2020
@Eric-Arellano Eric-Arellano deleted the python-type-stubs branch September 28, 2020 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants