Help testing PEP 660 support in setuptools

Does the strict behaviour works well for your use case?
(Despite the fact that it is not very handy for incrementally adding/splitting/renaming/removing files).

Yes, it’s Odoo. The main project has a that does the pkgutil.extend_path dance. This automatically detects addon modules that the community distributes on pypi (those have no in their odoo directory, example). This mechanism is somehow broken by _TopLevelFinder - I have not investigated why precisely yet. Feel free to PM me if you’d like to dig further, as the details of how Odoo works may not be of interest to followers of this thread.

I assume other projects which use this approach for distributing and locating plugins may suffer of the same problem. I don’t know how prevalent they are, though.

If we introduce a new option, we have to move very carefully to not re-ignite discussions in which we don’t have consensus.

Yes, I’am aware to the complex nature of this topic. I also understand that setuptools would take this opportunity to use a better default than the static pth approach.

Meanwhile, I added SETUPTOOLS_ENABLE_FEATURES=legacy-editable (env var) as a escape hatch to recover the python develop behaviour.


Does the strict behaviour works well for your use case?

Not entirely. The pkgutil.extend_path addon detection mechanism works, as expected. But in editable mode Odoo relies on the presence of an addons directory next to the odoo directory, which does not exist in the strict link farm. So that would require some tweaks too. BTW, would there be a mechanism for an in-tree backend to customize or override the link farm construction ?

Thank you very much @sbidoul, I will try to have a look on this.

That said I am very open to explore solutions to support the majority of use cases that currently work well with python develop (I just want to avoid growing even more in complexity, it seems that every time I implement a PEP in setuptools the amount of lines of code just grow a lot :sweat_smile:).

Do you have any hint for me why that is not the case? I had a look on the example and it seems that it does have a directory, but maybe because of the symlinks something I haven’t considered is happening…

Uuummm, I haven’t developed the feature considering the users would like to piggyback directly into the link farm generation… However, I added some ways of influencing it if the package uses a custom build step (or overwrites build_py/build_ext). It is possible to add a get_output_mapping() method to a build subcommand (this is not 100% future proof yet, I just thought it would be useful for plugin developers, so I also appreciate any feedbacks).

I totally understand what you mean :slight_smile: That said, in this case setuptools is growing three different ways to implement editable installs so I guess the code size increase is unavoidable.

My naive suggestion would be to expose the 3 modes to users via the config setting and document the tradeoffs of each, as well as when each becomes the default. That would grow the docs a little bit, but probably not that much the code size.

Ah yes, that is not a bug, don’t worry. It’s simply because, for historical reasons, the addons directory is outside of the main Odoo package and not known to Not much you can do here, and that’s not related to the core of the issue I raised here.


That sounds very useful, I’ll investigate that too.

Thank you very much @sbidoul. Before I dive into adding another option to force the static .pth file, I would like to understand a bit better why the strict mode is not working for this use case (in theory the strict mode should work identically to a regular installation in a different “portion” of sys.path, right? So it should work for all the scenarios, including pkgutil namespaces, as long as the file system support either symlinks or hard links, which should cover almost everything).

I tried to investigate directly with odoo but it is not as trivial to install as I would have hopped, so I decided to try a very simplified experiment as shown bellow:

rm -rf /tmp/staging
mkdir -p /tmp/staging/base/pkg

cat <<EOF > /tmp/staging/base/pkg/
import pkgutil
import os.path
__path__ = [
    for path in pkgutil.extend_path(__path__, __name__)

cat <<EOF > /tmp/staging/base/pyproject.toml
requires = ["setuptools @ git+"]
build-backend = "setuptools.build_meta"

mkdir -p /tmp/staging/addon1/pkg/addons/addon1
cp /tmp/staging/base/pyproject.toml /tmp/staging/addon1/pyproject.toml
echo -e "[project]\nname = 'addon1'\nversion = '0.42'" >> /tmp/staging/addon1/pyproject.toml
cp /tmp/staging/base/pkg/ /tmp/staging/addon1/pkg/
cp /tmp/staging/base/pkg/ /tmp/staging/addon1/pkg/addons/
echo "addon = 1" > /tmp/staging/addon1/pkg/addons/addon1/

mkdir -p /tmp/staging/addon2/pkg/addons/addon2
cp /tmp/staging/base/pyproject.toml /tmp/staging/addon2/pyproject.toml
echo -e "[project]\nname = 'addon2'\nversion = '0.42'" >> /tmp/staging/addon2/pyproject.toml
cp /tmp/staging/base/pkg/ /tmp/staging/addon2/pkg/
cp /tmp/staging/base/pkg/ /tmp/staging/addon2/pkg/addons/
echo "addon = 2" > /tmp/staging/addon2/pkg/addons/addon2/

cd /tmp/staging
virtualenv -p py38 .venv
.venv/bin/python -m pip install -U 'pip==22.1.2'
.venv/bin/python -m pip install base
.venv/bin/python -m pip install -e addon1 --config-setting editable-mode=strict
.venv/bin/python -m pip install -e addon2 --config-setting editable-mode=strict

cd /tmp
/tmp/staging/.venv/bin/python -I -c 'import pkg.addons.addon1; print(pkg.addons.addon1.addon)'  # => 1
/tmp/staging/.venv/bin/python -I -c 'import pkg.addons.addon2; print(pkg.addons.addon2.addon)'  # => 2
/tmp/staging/.venv/bin/python -I -c 'import pkg.addons; print(pkg.addons.__path__)'
# ['/tmp/staging/addon1/build/__editable__.addon1-0.42-cp38-cp38-linux_x86_64/pkg/addons',
#  '/tmp/staging/addon2/build/__editable__.addon2-0.42-cp38-cp38-linux_x86_64/pkg/addons']

In this example it does not matter if the original base project has a addons folder or not (with the corresponding pkgutil trick in… Everything seems to be working fine… Am I missing some details?

That would also be the case with static .pth files, wouldn’t it?

(Unless we add support for links inside wheels as mentioned by Paul, I think that would be a limitation of all forms of editable installs we have available nowadays).

Looks like the stable version 63.x also has PEP 660 support now, so this still works.
I didn’t see it mentioned in the changelog, but it probably should be?

My experience is mostly like what others have said already.
I converted some simple projects from setup.cfg + to pyproject.toml only, and it worked perfectly well (the only non-standard thing was setuptools_scm and package_data).
I only tried the “normal” non-strict editable install and it works as expected.

The only thing I don’t like is that it still puts the egg-info folder beside the package folder when doing editable install, but that was a problem in the old way too and probably unrelated to the PEP 660 support.

Sorry @saaketp, I think the stable version 63 does not include PEP 660 support. In my calculations there would be no breaking change in setuptools until we landed editable support, but during the weekend there was another change that got in first, requiring a major bump.

Since setuptools uses an automatic version bump system, sometimes it is a but hard to precise when a feature under development will be able to get in. When PEP 660 finally lands, there will be an entry in the changelog.

Hmm, okay it does fail now when I try in a new venv, not sure what was wrong in the other venv when I tried it earlier, but that time editable install worked with requires = ["setuptools>=63.0"].

So now we need requires = ["setuptools==63.0.0b1"] in the [build-system] section, >= gets the stable one and fails.

Sorry for the disruption

I think that for test purposes, the best would be to stick with "setuptools @ git+" for now, this way we avoid any problems with eventual intermediary releases.

The strict mode does work perfectly with regards to pkgutil.extends_path. No worries about that - sorry if I was unclear previously.

Odoo is basically trying to locate. Path(__file__).parent.parent / "addons" as a convenience when running from a git checkout. In strict editable mode it would have to do Path(__file__).parent.parent.parent / "addons", or resolve the symlink.

That’s a subtle side effect of importing symlinked code.

Note that that would not be a problem case with the static .pth.

For the case of Odoo I used for testing, there are workarounds. An in-tree backend is needed anyway and it can enforce the use of the strict mode as well as doing other tweaks.

So, to sum up, there are two separate and independent topics here:

  • pkgutil not compatible with the top level finder mode (as alluded to in the limitations, and forcing to use src layout or strict mode)
  • the fact the code is imported from a symlink farm may leak through one way or another

I’ll leave it up to you, of course, to decide whether these are worth further clarifying in the documentation, or whether the issues would be prevalent enough to warrant exposing the 3 modes via the config setting to provide more control to users.

Thanks again for this effort!

Hi @sbidoul, I think I understand a little bit better now, thank you very much for the explanation. Sorry for being so persistent, I really wanted to have a clear idea on what are the shortcomings of the selected approaches :sweat_smile:.

We definitely need to improve the documentation and I will work on that[1].

Would this be a general problem, or would it only happen when the project make assumptions on how the package is installed without considering all possibilities offered by importlib? In the example from last comment, it looks like some parts of the code are relying on circumstances external to the editable installation per se, but I don’t think PEP 660 guarantees anything about the specific installation layout[2].

As long as one of the supported approaches can work for the use cases that don’t make assumptions external to PEP 660, I would prefer keeping only editable_mode=strict (or any bike-shedding equivalent names) as user facing interface[3]. But I don’t feel like I should make this decision alone.

Would you like to create a discussion on setuptools repo and try to involve the other maintainers? I would also welcome any opinions of the other members of the community that have more experience on this topic.

  1. In my mind, something that would really help here is if we could display some warnings to the users after pip install -e . (not all the backend logs, I understand they might be very verbose, but only the warnings).
    In the implementation for all editable strategies, I added a short paragraph trying to express the their main limitations (that can be expanded to cover pkgutil namespaces). But currently these messages are hidden by default… I am not sure what would be the best UI for this and I also understand that this is difficult to implement from pip’s point of view. ↩︎

  2. Could this be considered a backwards incompatible change in setuptools? Under Hyrum’s law, probably. But I don’t think setuptools make many explicit promises about develop (the existing docs do mention .egg-link and links to your project’s source code but the specifics on how this work are left for the user’s imagination - the only guarantee seems to be that the package will be available on sys.path). Nevertheless the existence of a new PEP should be a good reason to introduce some level of backward incompatibility (which I wish to minimise as much as possible). ↩︎

  3. My interpretation of everything I read when working on this, is that there is some level of consensus in the PEP 660 discussions: in general people seem to agree that simply adding the root project folder to sys.path is not the best idea (understandably, because it leads to effectively installing unintentional packages like docs, tests, examples, or single modules like noxfile, tasks, setup as a side effect), and that a MetaPathFinder or links (as long as they work on Windows) are the way to go. ↩︎

I have tried testing this on a project where I use versioningit along with its onbuild feature

The onbuild feature is meant to update a version string in the package when building a wheel/sdist but not when performing an editable install. When performing the editable install I expect the code to remain unchanged and __version__ to be assigned the output of a function that calls git describe under the hood to get the version from git tags and hashes. E.g. my code contains __version__ = _get_version() which should be overwritten with __version__ = 0.0.1 or similar when installing the package (except as an editable install)

The feature is implemented using cmdclass with a that looks like this:

from setuptools import setup
from versioningit import get_cmdclasses

if __name__ == "__main__":

This works fine with the legacy editable install but with this branch I am seeing an error when installing as editable. (Also works as expected using build to produce sdist/bdist and using pip install .

❯ pip install -e .
Obtaining file:///C:/Users/jenielse/source/repos/debugproject
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Installing backend dependencies ... done
  Preparing editable metadata (pyproject.toml) ... done
Requirement already satisfied: versioningit>=1.1.0 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from debugproject==0.0.1.post3+g1aea130) (2.0.0)
Requirement already satisfied: tomli<3.0,>=1.2 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from versioningit>=1.1.0->debugproject==0.0.1.post3+g1aea130) (2.0.1)
Requirement already satisfied: packaging>=17.1 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from versioningit>=1.1.0->debugproject==0.0.1.post3+g1aea130) (21.3)
Requirement already satisfied: importlib-metadata>=3.6 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from versioningit>=1.1.0->debugproject==0.0.1.post3+g1aea130) (4.12.0)
Requirement already satisfied: zipp>=0.5 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from importlib-metadata>=3.6->versioningit>=1.1.0->debugproject==0.0.1.post3+g1aea130) (3.8.1)
Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in c:\users\jenielse\miniconda3\envs\qcodespip38\lib\site-packages (from packaging>=17.1->versioningit>=1.1.0->debugproject==0.0.1.post3+g1aea130) (3.0.9)
Building wheels for collected packages: debugproject
  Building editable for debugproject (pyproject.toml) ... error
  error: subprocess-exited-with-error

  × Building editable for debugproject (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [68 lines of output]
      running editable_wheel
      creating C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info
      writing C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\PKG-INFO
      writing dependency_links to C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\dependency_links.txt
      writing requirements to C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\requires.txt
      writing top-level names to C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\top_level.txt
      writing manifest file 'C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\SOURCES.txt'
      reading manifest file 'C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\SOURCES.txt'
      adding license file 'LICENSE'
      writing manifest file 'C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject.egg-info\SOURCES.txt'
      creating 'C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject-0.0.1.post3+g1aea130.dist-info'
      adding license file "LICENSE" (matched pattern "LICEN[CS]E*")
      creating C:\Users\jenielse\AppData\Local\Temp\pip-wheel-g3c7z1_y\tmppzq2ikd2\debugproject-0.0.1.post3+g1aea130.dist-info\WHEEL
      running build
      running build_py
      C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\normal\Lib\site-packages\wheel\ RuntimeWarning: Config variable 'Py_DEBUG' is unset, Python ABI tag may be incorrect
        if get_flag('Py_DEBUG',
      C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\command\ SetuptoolsDeprecationWarning: install is deprecated. Use build and pip and other standards-based tools.
      Traceback (most recent call last):
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\command\", line 101, in run
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\command\", line 247, in _create_wheel_file
          files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\command\", line 220, in _run_build_commands
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\_distutils\", line 317, in run_command
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\", line 1217, in run_command
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\_distutils\", line 987, in run_command

        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\command\", line 33, in run
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\_distutils\command\", line 131, in run
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\_distutils\", line 317, in run_command
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\", line 1217, in run_command
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\setuptools\_distutils\", line 987, in run_command

        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\versioningit\", line 69, in run
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\versioningit\", line 593, in run_onbuild
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\versioningit\", line 442, in do_onbuild
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\versioningit\", line 160, in __call__
          return self.method(params=self.params, **kwargs)
        File "C:\Users\jenielse\AppData\Local\Temp\pip-build-env-0x_b5npw\overlay\Lib\site-packages\versioningit\", line 64, in replace_version_onbuild
          lines = path.read_text(encoding=encoding).splitlines(keepends=True)
        File "C:\Users\jenielse\Miniconda3\envs\qcodespip38\lib\", line 1236, in read_text
          with'r', encoding=encoding, errors=errors) as f:
        File "C:\Users\jenielse\Miniconda3\envs\qcodespip38\lib\", line 1222, in open
          return, mode, buffering, encoding, errors, newline,
        File "C:\Users\jenielse\Miniconda3\envs\qcodespip38\lib\", line 1078, in _opener
          return, flags, mode)
      FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\jenielse\\AppData\\Local\\Temp\\\\debugproject\\'
      error: Support for editable installs via PEP 660 was recently introduced
      in `setuptools`. If you are seeing this error, please report to:

      Meanwhile you can try the legacy behavior by setting an
      environment variable and trying to install again:

      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building editable for debugproject
Failed to build debugproject
ERROR: Could not build wheels for debugproject, which is required to install pyproject.toml-based projects

I am unsure if this is a bug or a feature that is no longer supported (cmdclass).

Ideally an editable install should not try to perform this replace at all and should not care that cannot be found.

A sample project showing the error can be seen here

Thank you very much for the feedback @jenshnielsen, I have contacted the plugin author to discuss what can be done to solve this issue.

The TLDR for why this is happening is the following:

  • Previously the develop command in setuptools would only run build_ext and not build_py. versioningit seems to be taking advantage of this behaviour.
  • In the changes I have implemented in pypa/setuptools@feature/pep660, every build command (this generalisation includes build_py) has the chance to decide to run (or not) on editable installs[1]. This happens inside the run method, which is overwritten by versioningit.

My proposal for versioningit is based on this procedure for implementing custom build steps.

  1. The motivation for this is to improve support for custom build steps. ↩︎

Hi @jenshnielsen, it seems that versioningit v2.0.1 is out now and it should support your use case with PEP 660.

Thanks a lot @abravalheri for the quick response. I can confirm that this resolves the issue.

Hi @sbidoul, sorry for the delay in getting back to this topic.

After some discussion about the topic, I have implemented a compat mode that is meant as a temporary workaround during the transition period, so people have time to adapt to the changes. I am currently considering a period of 6 months, after which I would like to remove this compat mode.

The compat mode will try to emulate the behaviour of the existing develop command (i.e. use a static .pth file).

I have also updated the docs with a more detailed list of limitations and a brief summary of how the editable installation is performed:

I hope this is useful.

Mypy does not find py.typed in non-strict mode

While testing the new pep660 feature I have found that mypy does not work as expected. This seems to be a regression compared to the legacy mode.

In short, I have a project in a flat layout containing a py.typed file within the toplevel folder of the source
e.g. debugproject\debugproject\ contains and an empty py.typed file.

The file is included as package data with the following config in setup.cfg

zip_safe = False
packages = find:

* =

I have verified that the py.typed file gets installed as expected using pip install . and is a part of the bdist/sdists produced by python -m build

Now I run mypy a simple script that imports a class from this project.

mypy .\

where contains a single import

from import Foo

This raises an error when the package is installed in editable mode using the default non strict config. error: Cannot find implementation or library stub for module named "" note: See
Found 1 error in 1 file (checked 1 source file)

With legacy editable install this type checks correctly.
The same is true using the compat and strict mode.

A sample project with this issue can be found here (Updated compared to my last comment)

Thank you very much for the help testing Jens, and the well thought reproducer.

I checked your example and I think I have an idea what is going on:

It would seem that mypy is not taking into consideration the existence of finders defined outside of the stdlib.

Maybe I am wrong here, but I reached this conclusion after running importlib_resources.files(debugproject).glob("*") and checking that py.typed is indeed listed as part of debugproject.

I believe that this limitation is closely related to the discussion in a different topic about PEP 660, IDEs and static code analysis. If we look at that thread and replace the occurrences of “IDE” with “static code analysis tool”, everything is pretty much applicable for this use case.

The TL;DR that I got from this other thread is that the packaging community is open to discuss different mechanisms to facilitate interoperating with static analysis tools, but this topic of conversation has not been explored yet.

On the bright side, if you install your project using the strict mode, mypy seems to work well:

pip install -e . --config-settings editable-mode=strict
echo "from import Foo" >
# Success: no issues found in 1 source file

I also believe that projects using the src-layout should work as normally.

Thanks @abravalheri I also just discovered Add support for PEP-610 editable packages (#12313) by hashstat · Pull Request #12315 · python/mypy · GitHub / Support finding "editable" type annotated packages made editable through direct_url.json · Issue #12313 · python/mypy · GitHub which looks like it may be trying to solve the issue from the mypy side.

I can also confirm that strict mode does indeed resolve the issue.

1 Like