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

MyPy third-party requirements support is broken for Python 2 #10819

Closed
Eric-Arellano opened this issue Sep 19, 2020 · 6 comments · Fixed by #10820
Closed

MyPy third-party requirements support is broken for Python 2 #10819

Eric-Arellano opened this issue Sep 19, 2020 · 6 comments · Fixed by #10820
Labels

Comments

@Eric-Arellano
Copy link
Contributor

Eric-Arellano commented Sep 19, 2020

Right now, we expect for the interpreter constraints to be different for the tool_pex and runner_pex than they are for the requirements_pex. This is because MyPy must run with Python 3.5, but the user's code may be anything, including Py27.

We use --pex-path to join all the PEXes. However, this results in error messages like this:

Failed to execute PEX file. Needed macosx_10_15_x86_64-cp-36-cp36m compatible dependencies for:
 2: filelock
    But this pex only contains:
      filelock-3.0.12-py2-none-any.whl

It appears that whatever interpreter is used to run the runner PEX, it expects for every requirement to be compatible with it. So, running MyPy with Py36 complains when it can only find Py27 wheels.

$ pex --interpreter-constraint='CPython==2.7.*' -o requirements.pex filelock
$ pex --interpreter-constraint='CPython==3.6.*' -o runner.pex --pex-path=requirements.pex
$ ./runner.pex
Failed to execute PEX file. Needed macosx_10_15_x86_64-cp-36-cp36m compatible dependencies for:
 1: filelock
    But this pex only contains:
      filelock-3.0.12-py2-none-any.whl

Instead of using --pex-path to join the requirements.pex, we need a mechanism to get the distributions out of the PEX and treat them like normal data. Then, in our MyPy code, we can load up those dists.


Alternatively, we can set PEX_IGNORE_ERRORS...this fixes the issue, but I'm not sure if it's safe to do.

@Eric-Arellano
Copy link
Contributor Author

Eric-Arellano commented Sep 19, 2020

Put up a failing test at #10820. PEX_IGNORE_ERRORS does not work. While the runner Pex no longer errors, it cannot load the Python 2-only wheel, which results in MyPy complaining about missing site packages, even tho the library does have type hints.

Taking a step back..that makes sense. How would MyPy—which uses >=3.5—load a Python 2-only wheel? It can parse Python 2 code using the typed-ast library, but this doesn't necessarily mean it can handle Python 2-only distributions?

I think that this could suggest one of two things:

  1. If you are using Py2, you'll only have type stubs if the wheel is also compatible with Py3. Otherwise, you'll have to ignore that dependency.
  2. If you're running with Py2, perhaps we should include Py3 in the interpreter constraints when resolving requirements. That way, we could have both the Py2 and Py3 wheels for the library.
    • Although, this sounds like a recipe for disaster. For example, if you have a dist that cannot be installed with Py3, now the build is broken.

#1 might be adequate, but it does raise some concerns I have when we're purely talking about Python 3. Imagine you resolve your requirements with Py36, but run MyPy with Py35. Now, none of your requirements will load. Yes, we can use PEX_IGNORE_ERRORS, but you'll get a bunch of MyPy failures about missing requirements.

--

Instead, what I think this is suggesting is that for Py3-only, the tool Pex and requirement Pex need to have the same constraints. Perhaps we need to use something like we do with iPython to determine the constraints:

# Note that we get an intermediate PexRequest here (instead of going straight to a Pex)
# so that we can get the interpreter constraints for use in ipython_request.
requirements_pex_request = await Get(
PexRequest,
PexFromTargetsRequest,
PexFromTargetsRequest.for_requirements(
(tgt.address for tgt in repl.targets), internal_only=True
),
)

Iff we're using Python 2, we set PEX_IGNORE_ERRORS to workaround some of the dists being missing.

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Sep 19, 2020

How would MyPy—which uses >=3.5—load a Python 2-only wheel? It can parse Python 2 code using the typed-ast library, but this doesn't necessarily mean it can handle Python 2-only distributions?

So this makes sense, but I feel like if this was true, it would make it really difficult for people to use mypy on py2-only projects in general! I know pip was py2-compatible for a while, but it also vendored everything it needed. I'm going to see if I can find an example of a project that:

  1. is py2-compatible
  2. uses mypy
  3. pulls in 3rdparty dists

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Sep 19, 2020

Ok, so after continuing your awesome work at pex-tool/pex#1032, I think this line was more insightful than I realized at first:

Instead of using --pex-path to join the requirements.pex, we need a mechanism to get the distributions out of the PEX and treat them like normal data. Then, in our MyPy code, we can load up those dists.

Could we perhaps just unconditionally unzip the requirements pex? And in fact, even better: instead of adding complex round-trip logic to extract requirements out of a pex file after the PEXBuilder has already done all the hard work to pack the distributions into the zip, could we instead invoke the pex library API!

We could make this a separate rule which uses a script that works in isolation, without being able to import anything else from pants -- just like the fabled ipex launcher at https://github.com/pantsbuild/pants/blob/1.25.x-twtr/src/python/pants/backend/python/subsystems/ipex/ipex_launcher.py. That script could call pex.resolver.resolve() to download distributions, but instead of creating a pex file, literally just stop there. This should make the process completely cacheable, and the output digest transferable to the downstream mypy invocation.

This would be much faster (I think) than creating a whole pex file from the dependencies, in addition to avoiding complex work to extract anything out of the pex format. I'll look to extend #10820 now or tomorrow with this idea.

@cosmicexplorer
Copy link
Contributor

It looks like we might be able to remove this lengthy comment and the monkey-patching here too!

# its search to `site-packages`. Since PEX deliberately isolates itself from `site-packages` as
# part of its raison d'être, we monkey-patch `site.getsitepackages` to look inside the scrubbed
# PEX sys.path before handing off to `mypy`.

@cosmicexplorer
Copy link
Contributor

Hmmmm. Before I get too much further into this, I think this might be a duplicate of #9206!

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Sep 21, 2020

I think it's a little different than #9206, although that would be half the battle. In that case, we just needed to shade requirements from tools. In this case, I'm not sure (like @Eric-Arellano mentioned above) whether mypy is able to load python 2 dists even if they're on the sys.path (because that sounds like it would be fraught with issues). We should figure out whether mypy can load python 2 wheels for typing info, or if it can find it if we expose it on sys.path (which would be closer to what we do right now).

However, I want to note that if we can make the current method work, we should be able to generalize it into a solution for #9206, which would be cool.

Eric-Arellano added a commit that referenced this issue Sep 23, 2020
Closes #10819.

Turns out that when using `--pex-path` with PEX, it will expect for every requirement to have a wheel that is compatible with the current interpreter, unless you set `PEX_IGNORE_ERRORS`. This means that it is naively not safe to resolve requirements with Python 2, but run MyPy with Python 3.5+.

Likewise, it is not safe to resolve requirements with Python 3.6 but run MyPy with Python 3.7.

Instead, we generally use the same interpreter constraints for the requirements when creating the tool pex. However, if the requirements have any Python 2 code—or the user set `--mypy-interpreter-constraints`—then we will let the two sets diverge, and we'll set `PEX_IGNORE_ERRORS`.
Eric-Arellano added a commit that referenced this issue Sep 24, 2020
### 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants