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

Improve support of MyPy requirements when Python 2 used #10853

Merged
merged 5 commits into from
Sep 24, 2020

Conversation

Eric-Arellano
Copy link
Contributor

Problem

The wheels used by third-party code might not use the same constraints as what the MyPy Pex uses. This was causing errors (#10819), which we fixed in #10820.

However, that fix was not perfect. Even if there were valid typed Python 2 wheels, we would ignore them. WIth Python 3, we had to be careful that mypy.pex used the exact same constraints as requirements.pex, so that, for example, we don't run MyPy with Py36 but resolve requirements with Py37.

Solution

Stop using --pex-path to join the requirements.pex, which has the effect of the wheels being put on PYTHONPATH. (We were then teaching MyPy to read from this PYTHONPATH through a custom launcher script.)

Instead, we extract the wheel folders from requirements.pex, and teach our custom launcher script to look in these extracted .deps/ folders through a new EXTRACTED_WHEELS env var. MyPy will use this to resolve the requirements, without us actually including the wheels in the final PEX or modifying PYTHONPATH.

Result

Our Python 2 test now fully works, even though one of the wheels is not compatible with Python 3.

We no longer need to align mypy.pex with requirements.pex.

Caveat: MyPy plugins must be installed with --mypy-extra-requirements

An initial implementation did not use EXTRACTED_WHEELS and instead loaded the wheels via PYTHONPATH. This allowed for you to specify MyPy plugins via normal dependencies on python_requirement_library targets, because that requirement would get loaded via PYTHONPATH. Now, you cannot do this; you must specify the requirement via --mypy-extra-requirements.

This caveat seems acceptable. For Pytest, it's convenient that we allow you to load plugins via normal requirements, as it allows you to only sometimes use the plugin. MyPy is much more uniform, however; MyPy runs in a single batch over the entire transitive closure, unlike Pytest running one test file at-a-time. Likewise, without Pants, it's hard to imagine how you could only sometimes use a MyPy plugin, given that MyPy only works with a single config file.

# 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]
# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
…via `--mypy-extra-requirements`

[ci skip-build-wheels]

# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]
Comment on lines +174 to +183
if not mypy.options.is_default("interpreter_constraints"):
tool_interpreter_constraints = mypy.interpreter_constraints
elif code_interpreter_constraints.requires_python38_or_newer():
tool_interpreter_constraints = ("CPython>=3.8",)
elif code_interpreter_constraints.requires_python37_or_newer():
tool_interpreter_constraints = ("CPython>=3.7",)
elif code_interpreter_constraints.requires_python36_or_newer():
tool_interpreter_constraints = ("CPython>=3.6",)
else:
tool_interpreter_constraints = mypy.interpreter_constraints
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 will get simplified soon with batching by interpreter constraints, because we will start to set MyPy's python_version ourselves.

For now, we have it this way to

a) ensure Py38 code uses Py38 to run MyPy, which is necessary to work properly, and
b) you don't have to specify python_version in mypy.ini because Pants will run with the correct Python interpreter, which is used to set the default

See #10750 for more about this.

Comment on lines -95 to -98
PYTHONPATH = frozenset(
os.path.realpath(p)
for p in os.environ.get('PYTHONPATH', '').split(os.pathsep)
)
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 removed this part because I don't think it's relevant in a v2 world with hermetic environments. Users can't set PYTHONPATH manually anymore. I think this was leftover from the original v1 script.

Lmk though if this needs to be added back.

else code_interpreter_constraints
)

if not mypy.options.is_default("interpreter_constraints"):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this block mypy-specific? Or could it be factored into a method on PythonToolBase?

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, very MyPy specific. And also going to change super soon to instead be:

tool_interpreter_constraints = (
  ("CPython>=3.8",)
  if mypy.options.is_default("interpreter_constraints") and code_interpreter_constraints.requires_python38_or_newer()
  else mypy.interpreter_constraints
)

(This is possible because we're going to set python_version automatically, so we no longer care about what interpreter is used to run MyPy. We only care that if there's Py38, we use Py38 because otherwise MyPy can't parse the AST due to typed-ast not working with Py38.)

async def extract_distributions(pex: Pex, unzip_binary: UnzipBinary) -> ExtractedPexDistributions:
# We only unzip the `.deps` folder to avoid unnecessary work. Note that this will cause the
# process to fail if there is no `.deps` folder, so we need to use `FallibleProcessResult`.
argv = (*unzip_binary.extract_argv(archive_path=pex.name, output_dir="."), ".deps/*")
Copy link
Contributor

Choose a reason for hiding this comment

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

This is honestly so unreasonably slick.

[ci skip-build-wheels]

# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]
# 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]
@coveralls
Copy link

Coverage Status

Coverage remained the same at 0.0% when pulling d43564d on Eric-Arellano:fix-mypy-py2-round2 into c722104 on pantsbuild:master.

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.

Great stuff!

@Eric-Arellano Eric-Arellano merged commit ee98ef6 into pantsbuild:master Sep 24, 2020
@Eric-Arellano Eric-Arellano deleted the fix-mypy-py2-round2 branch September 24, 2020 17:20
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.

4 participants