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

Editable installs are not recognized as having typing when using py.typed (setuptools v64 issue) #13392

Open
cmeyer opened this issue Aug 12, 2022 · 20 comments
Labels
bug mypy got something wrong

Comments

@cmeyer
Copy link

cmeyer commented Aug 12, 2022

Bug Report

mypy does not recognize that libraries installed as editable using pip are fully typed even though the py.typed file is correctly included.

To Reproduce

Working script which installs a NON-editable version of library:

# create python environment with pip
rm -rf nionui
git clone https://github.com/nion-software/nionui.git
pushd nionui
git checkout 0.6.4
python -m pip install mypy numpy imageio types-pytz types-tzlocal types-setuptools
# the next line is the only change between these two scripts
python -m pip install nionutils==0.4.4
mypy --namespace-packages --ignore-missing-imports --follow-imports=silent --install-types --non-interactive --strict --no-warn-redundant-casts --no-warn-unused-ignores -p nion.ui -p nionui_app.nionui_examples
popd
# success

Failing version which installs an editable version of a library:

# create python environment with pip
rm -rf nionui
git clone https://github.com/nion-software/nionui.git
pushd nionui
git checkout 0.6.4
python -m pip install mypy numpy imageio types-pytz types-tzlocal types-setuptools nionutils
# the next line is the only change between these two scripts
python -m pip install -e git+https://github.com/nion-software/[email protected]#egg=nionutils
mypy --namespace-packages --ignore-missing-imports --follow-imports=silent --install-types --non-interactive --strict --no-warn-redundant-casts --no-warn-unused-ignores -p nion.ui -p nionui_app.nionui_examples
popd
# failure

Expected Behavior

These should work the same. The source client and underlying library are the same in both cases except one is installed as an editable install.

Actual Behavior

The failing version gives type errors:

nion/ui/DrawingContext.py:790: error: Returning Any from function declared to return "Optional[str]"
nion/ui/DrawingContext.py:794: error: Returning Any from function declared to return "Optional[str]"
nion/ui/DrawingContext.py:798: error: Returning Any from function declared to return "Optional[str]"
nion/ui/CanvasItem.py:2780: error: Class cannot subclass "Observable" (has type "Any")
nion/ui/UserInterface.py:3217: error: Returning Any from function declared to return "int"
nion/ui/UserInterface.py:3224: error: Returning Any from function declared to return "int"
nion/ui/UserInterface.py:3620: error: Class cannot subclass value of type "Any"
nion/ui/UserInterface.py:3634: error: Class cannot subclass value of type "Any"
nion/ui/Widgets.py:382: error: Returning Any from function declared to return "AbstractSet[int]"
nion/ui/Declarative.py:949: error: Class cannot subclass "Observable" (has type "Any")
nion/ui/Declarative.py:1112: error: Class cannot subclass "Observable" (has type "Any")
nion/ui/GridCanvasItem.py:256: error: Returning Any from function declared to return "int"
Found 12 errors in 6 files (checked 51 source files)

Your Environment

  • Mypy version used: 0.971
  • Mypy command-line flags: see script
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: Python 3.10.5
  • Operating system and version: macOS, Ubuntu 20, didn't try Windows yet

Also note: this behavior seems to have changed around 2022-08-10 to 2022-08-11. Something in the Python ecosystem seems to have changed. Nevertheless, this seems to be a bug in mypy since it is all about whether py.typed is found or not.

@cmeyer cmeyer added the bug mypy got something wrong label Aug 12, 2022
@lawrence-law
Copy link

lawrence-law commented Aug 12, 2022

I also experienced this problem and traced it down to setuptools v64 causing the issue. This version of setuptools seems to cause editable packages to no longer be installed with an egg-link file and now uses another mechanism. See release notes here: https://setuptools.pypa.io/en/latest/history.html#v64-0-0

If the editable package belongs to you, you could modify your pyproject.toml like this:

requires = [
  "setuptools==63.4.3",
  "wheel"
]

Previously I had setuptools without any version locking so I presume it was using setuptools 64.0.1.

This seems to workaround the issue (albeit withholding setuptools versions) although perhaps a fix does sit with Mypy.

@JacobHayes
Copy link

Setting export SETUPTOOLS_ENABLE_FEATURES="legacy-editable" (eg: with direnv or in Dockerfiles) and reinstalling things also worked for me (and should work if you can't update the project's pyproject.toml).

@hauntsaninja
Copy link
Collaborator

Is there documentation on what exactly setuptools is doing now? mypy 0.971 should just be using sys.path, which is, y'know, the standard way of making packages findable

@hauntsaninja hauntsaninja changed the title Editable installs are not recognized as having typing when using py.typed Editable installs are not recognized as having typing when using py.typed (setuptools v64 issue) Aug 12, 2022
@JacobHayes
Copy link

JacobHayes commented Aug 12, 2022

I think the relevant PEP is PEP 660, but I'm not sure if it has all the details. With the new wheels, the site-packages directory ends up with files like this:

__editable__.org_pkg-2.1.0.pth # imports __editable___org_pkg_2_1_0_finder and calls install()
__editable___org_pkg_2_1_0_finder.py # some Finder magic w/ refs to the source path
org_pkg-2.1.0.dist-info/ # dist info like "normal" wheels

instead of an org-pkg.egg-link file (I don't know why this would break mypy though).

I can post back with a minimum reproducing example shortly.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Aug 12, 2022

Oh, so setuptools has switched to using some import hook based approach instead of static entries in pth files? Note that the super standard pth file static directory install that everyone has been using for decades is perfectly valid under PEP 660.

I don't think there's any way for IDEs and static analysis tools to support import hooks. See
https://mail.python.org/archives/list/[email protected]/thread/IIVBPYDZR5T5BGPAWFVYS5ZPYDXGVHQN/#OSWHT5VSRGKPSPYD7PQWR2M4OCSL5WO3 where maintainers of PyCharm, VSCode, Pyright, Pyre, etc are all on the same page about this.

This feels like a very bold move from setuptools. I'd recommend contacting them about this.

@JacobHayes
Copy link

I'd recommend contacting them about this.

Done: pypa/setuptools#3518, though I'm prepared for them to say similar. 😉

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Aug 12, 2022

Yeah, well, so it goes. It's one thing if it was just mypy, but this will break all the tools 🤷

If a setuptools maintainer sees this, I recommend reading and then replying to that thread on typing-sig rather than discussing here.

@JacobHayes
Copy link

For posterity, I made a repro repo here which shows the issues w/ mypy and pylint for these installs.

@rojvv
Copy link

rojvv commented Dec 15, 2022

I think I am facing the exact same problem.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Dec 15, 2022

+1 comments aren't particularly useful here. But if you feel a need to post one, consider posting it on the setuptools issue instead.

I believe their current recommended solution is SETUPTOOLS_ENABLE_FEATURES="legacy-editable" env var, although they may eventually remove it.

@rojvv
Copy link

rojvv commented Dec 16, 2022

Eventually remove it? Like this issue persisting without even having a legacy fix?

@emmatyping
Copy link
Collaborator

I hate to say it but the changes the setuptools maintainers have made make supporting editable installs in mypy a lot more complicated, perhaps intractable in an efficient manner. We previously relied on static path calculation for this, but they no longer guarantee that (see attention note below linked section).

Therefore I regretfully propose that we drop support for editable installs. By that I mean just disable tests, and document that if you really need editable install support, you should use a src/ layout as the setuptools implementation uses a static .pth file in that case and it might work.

For now I suggest we remove the tests for editable installs.

@erictraut
Copy link

The approach we've taken with pyright and pylance is to document some workarounds for those who want to use editable installs. So far, everyone has had success following these suggested workarounds. Feel free to use them in the mypy documentation and tests if you find them useful.

@alexei
Copy link

alexei commented Oct 29, 2023

FWIW --config-settings editable_mode=compat (or strict) did not work for me. However, I found it working if I specify py.typed as package data:

[tool.setuptools.package-data]
"*" = ["py.typed", "*.pyi"]

Edit: I just found future versions of setuptools are going to include these files by default, see pypa/setuptools#4021

saeubank pushed a commit to Infleqtion/client-superstaq that referenced this issue Jan 9, 2024
fixes: #868

this appears to be a known issue stemming from the use of import hooks
to comply with pep 660 - see for example
python/mypy#13392 and
pypa/setuptools#3518. What's happening is that
for editable installs `setuptools` creates files like
`.../site-packages/__editable__.general_superstaq-0.5.5.pth` which are
supposed to point python to the source directory. Previously these would
just be text files containing a single path, e.g.
```
/<...>/client-superstaq/general-superstaq
```
but after switching to pyproject.toml it becomes an executable hook,
e.g.
```python
import __editable___general_superstaq_0_5_5_finder; __editable___general_superstaq_0_5_5_finder.install()
```
where `__editable___general_superstaq_0_5_5_finder.py` is another file
saved in the site-packages directory. this breaks static analysis tools
because they won't execute the required code

afaict this pr seems to be the cleanest workaround - after these changes
setuptools seems to generate the old-style `.pth` files (i.e. text files
containing paths to the source directories), and `mypy` behaves as
expected (at least for me)
ANogin added a commit to ANogin/ofrak that referenced this issue Feb 11, 2024
@earonesty
Copy link

1 when installing, use editable mode

--config-settings editable_mode=strict`
``  
2. your package should export py.typed 

[tool.setuptools.package-data]
"" = ["py.typed", ".pyi"]


both are needed.

if someone winds up here because poetry doesn't support the `build` package config settings:

https://github.com/python-poetry/poetry/issues/8909

... it still doesn't

@akaihola
Copy link

@earonesty, thanks for pointing this out! Also please forgive me for reformatting your comment for readability:

  1. when installing, use editable mode
--config-settings editable_mode=strict
  1. your package should export py.typed
[tool.setuptools.package-data]
"" = ["py.typed", ".pyi"]

Both are needed.

If someone winds up here because poetry doesn't support the build package config settings:
python-poetry/poetry#8909
...it still doesn't

@mohit2152sharma
Copy link

For anyone reaching here and trying the following:

Setting export SETUPTOOLS_ENABLE_FEATURES="legacy-editable" (eg: with direnv or in Dockerfiles) and reinstalling things also worked for me (and should work if you can't update the project's pyproject.toml).

Note that it might not work with latest pip versions as the don't support this behaviour.

Newer versions of pip no longer run the fallback command python setup.py develop when the pyproject.toml file is present. This means that setting the environment variable SETUPTOOLS_ENABLE_FEATURES="legacy-editable" will have no effect when installing a package with pip.

Reference: https://setuptools.pypa.io/en/latest/userguide/development_mode.html#legacy-behavior

@mb6ockatf

This comment was marked as duplicate.

@emmatyping
Copy link
Collaborator

Perhaps we should provide a warning if we see a __editable__*.pth file in any site directory, and link to some documentation about this behavior. We aren't likely to follow pth files with runtime imports, so I think that is the best we can do.

@pelson
Copy link

pelson commented Aug 30, 2024

Perhaps we should provide a warning if we see a editable*.pth file in any site directory, and link to some documentation about this behavior. We aren't likely to follow pth files with runtime imports, so I think that is the best we can do.

This makes sense to me. Honestly, it should perhaps do the same thing whenever sys.meta_hook is set.

Another thing that could also work is to allow the mypy plugin system to emulate the meta_hook behaviour by allowing a plugin to define the source code of an import.

For me personally, I want static analysis to be done on the code that actually gets run, and I'd be willing to pay the price of having plugins that minimally runs the interpreter to resolve that code correctly.

chipx86 added a commit to djblets/djblets that referenced this issue Sep 14, 2024
Djblets has historically been a setuptools-based project, relying
heavily on the dynamic ability of `setup.py`. Over the years, the Python
ecosystem has moved to `pyproject.toml` files, with pluggable build
backends. This has reached a point of maturity, and `pip` will soon
remove support for installing either production or editable installs
from a legacy source tree.

In theory, modernization requires just providing a `pyproject.toml` that
specifies `setuptools` as a build backend. A project can still use
`setup.py` for the project definition and dynamic capabilities. However,
since packages are also now built in a virtualenv, it's become clear
that we needed to address about our packaging.

We now use `pyproject.toml` to define most of the metadata of the
package. The build backend is then a specialization of
`setuptools.build_meta`, living in `build-backend.py` at the root of the
tree. This specializes a few things about our build process:

1. It uses all of Djblets's dependencies as build-system dependencies,
   needed in order to build static media for the package.

2. It builds the static media, including them in both sdist and wheel
   distributions.

3. It writes out a `package-requirements.txt` at build time with the
   dependencies from `djblets/dependencies.py`, which setuptools can
   then consume. This is also bundled in the sdist.

With this, we no longer need `setup.py`, and constrain all custom logic
to `build-backend.py`.

Some things to note:

1. `python -m build .` will default to building an sdist and then a
   wheel from that sdist, whereas `build . -w` will build a wheel
   straight from the tree. Due to differences in the prep stages, and
   what's built from what, we need to micromanage some state (like
   `package-requirements.txt`) in different places.

2. Other build backends (hatch, PDM, flit, and poetry) were evaluated
   and discarded. These are great backends, but don't solve the core
   problems we've had to work around out of the box, and sort of want to
   manage more of the development and build process. `setuptools` is a
   known entity for us, and will be needed for extension packaging
   anyway, so we're sticking with it.

3. `Makefile` installs the package in editable-compat mode. This uses
   standard `.pth` files instead of newer `setuptools`-based import
   hooks, in order to allow `mypy` and `pyright` to find type
   definitions. See python/mypy#13392 and
   pypa/setuptools#3518.

Testing Done:
Tested isolated builds in the following setups:

* `python -m build .` (builds sdist, then a wheel from the sdist)
* `python -m build . -s` (builds sdist)
* `python -m build . -w` (builds wheel)

Tested isolated editable installs using `pip install -e .` and
non-isolated editable installs using `pip install -e --no-build-isolation`
and with `make develop`.

Performed full tree diffs of generated wheels from `./setup.py bdist_wheel`
(prior to this change) and both wheel-producing `python -m build` methods.
Verified that all content was identical, with the exception of differences
in source map paths and some modern metadata for the package.

Reviewed at https://reviews.reviewboard.org/r/14137/
swhmirror pushed a commit to SoftwareHeritage/swh-environment that referenced this issue Nov 26, 2024
As mypy does not support editable installs for projects that have a
pyproject.toml and use a build backend that supports PEP 660, force
the legacy behavior of editable installs by passing the configuration
settings "editable_mode=compat" to the build backend.

It enables to remove the pip version restriction in the swh venv and
avoid bad surprises when the mypy pre-commit hook is called when
committing in a swh repository.

See python/mypy#13392 for more details.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests