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

Presence of a pyproject.toml file causing unexpected behavior #9738

Closed
1 task done
omry opened this issue Mar 26, 2021 · 25 comments
Closed
1 task done

Presence of a pyproject.toml file causing unexpected behavior #9738

omry opened this issue Mar 26, 2021 · 25 comments
Labels
C: build logic Stuff related to metadata generation / wheel generation type: support User Support

Comments

@omry
Copy link
Contributor

omry commented Mar 26, 2021

pip version

21.0.1

Python version

3.8.0

OS

Ubuntu 18.04

Additional information

No response

Description

Somehow, the presence of a pyproject.toml file is causing pip install from a directory to fail importing of installed modules.

Expected behavior

The presence of pyproject.toml does not impact the behavior of pip install.

How to Reproduce

This is a bit complicated to explain minimal repro here should be enough.

Structure:

$ tree
.
├── build_helper
│   ├── build_helper
│   │   └── __init__.py
│   └── setup.py
├── example
│   └── __init__.py
├── README.md
└── setup.py

Content of files in repro repository.

Output

Installing build_helper:

$ pip install build_helper/
Processing ./build_helper
Building wheels for collected packages: build-helper
  Building wheel for build-helper (setup.py) ... done
  Created wheel for build-helper: filename=build_helper-0.0.0-py3-none-any.whl size=1261 sha256=aa0ee67ef3385cba13e32659fb3635047631d0466ef0861c71badd5d3d67f7fd
  Stored in directory: /tmp/pip-ephem-wheel-cache-_dpz2jwm/wheels/36/26/ae/14e3035420d431e16878ed266d823b24627dc0fe56af0b53b3
Successfully built build-helper
Installing collected packages: build-helper
  Attempting uninstall: build-helper
    Found existing installation: build-helper 0.0.0
    Uninstalling build-helper-0.0.0:
      Successfully uninstalled build-helper-0.0.0
Successfully installed build-helper-0.0.0

Successfully installing primary project:

$ pip install .
Processing /home/omry/dev/test_pip
Building wheels for collected packages: example
  Building wheel for example (setup.py) ... done
  Created wheel for example: filename=example-1.0.3-py3-none-any.whl size=991 sha256=21a95c7e50946865cb6f4565842e455ab9a972ceae1800e534d391e2032286c0
  Stored in directory: /tmp/pip-ephem-wheel-cache-k02av0q_/wheels/57/c2/45/2447e9fc2acaa90e70563527f393553ccc1297fb8d529f0c92
Successfully built example
Installing collected packages: example
  Attempting uninstall: example
    Found existing installation: example 1.0.3
    Uninstalling example-1.0.3:
      Successfully uninstalled example-1.0.3
Successfully installed example-1.0.3

Creating an empty pyproject.yaml, after which installation fails:

$ touch pyproject.toml
$ pip install .
Processing /home/omry/dev/test_pip
  Installing build dependencies ... done
  Getting requirements to build wheel ... error
  ERROR: Command errored out with exit status 1:
   command: /home/omry/miniconda3/envs/test-pip/bin/python /home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py get_requires_for_build_wheel /tmp/tmpybcsa03y
       cwd: /tmp/pip-req-build-_31oxf1l
  Complete output (18 lines):
  Traceback (most recent call last):
    File "/home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py", line 280, in <module>
      main()
    File "/home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py", line 263, in main
      json_out['return_val'] = hook(**hook_input['kwargs'])
    File "/home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py", line 114, in get_requires_for_build_wheel
      return hook(config_settings)
    File "/tmp/pip-build-env-m265pb85/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 154, in get_requires_for_build_wheel
      return self._get_build_requires(
    File "/tmp/pip-build-env-m265pb85/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 135, in _get_build_requires
      self.run_setup()
    File "/tmp/pip-build-env-m265pb85/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 258, in run_setup
      super(_BuildMetaLegacyBackend,
    File "/tmp/pip-build-env-m265pb85/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 150, in run_setup
      exec(compile(code, __file__, 'exec'), locals())
    File "setup.py", line 3, in <module>
      from build_helper import get_version
  ImportError: cannot import name 'get_version' from 'build_helper' (unknown location)
  ----------------------------------------
WARNING: Discarding file:///home/omry/dev/test_pip. Command errored out with exit status 1: /home/omry/miniconda3/envs/test-pip/bin/python /home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py get_requires_for_build_wheel /tmp/tmpybcsa03y Check the logs for full command output.
ERROR: Command errored out with exit status 1: /home/omry/miniconda3/envs/test-pip/bin/python /home/omry/miniconda3/envs/test-pip/lib/python3.8/site-packages/pip/_vendor/pep517/_in_process.py get_requires_for_build_wheel /tmp/tmpybcsa03y Check the logs for full command output.

Code of Conduct

  • I agree to follow the PSF Code of Conduct
@omry omry added S: needs triage Issues/PRs that need to be triaged type: bug A confirmed bug or unintended behavior labels Mar 26, 2021
@pfmoore
Copy link
Member

pfmoore commented Mar 26, 2021

If you add a pyproject.toml, you need to include build_helper as a build dependency in that file. Projects with pyproject.toml present are processed using PEP 517 rules, which means that they are built in an isolated environment with only the dependencies mentioned as build requirements in pyproject.toml available.

@pradyunsg pradyunsg added C: build logic Stuff related to metadata generation / wheel generation type: support User Support and removed S: needs triage Issues/PRs that need to be triaged type: bug A confirmed bug or unintended behavior labels Mar 26, 2021
@omry
Copy link
Contributor Author

omry commented Mar 26, 2021

I see.
This caught me off guard because I had pyproject.toml (for towncrier) - and things didn't behave as I expected.
It took some binary on which files I should delete to make things work. pyproject.toml was the last file I tried.

Is this behavior documented in the pip docs? I actually tried to look for build time dependencies and failed to find anything helpful.

@omry
Copy link
Contributor Author

omry commented Mar 26, 2021

I tried this:

$ cat pyproject.toml 
[build-system]
requires = ["build_helper"]

But I couldn't figure out how to specify a local dependency.

Based on this SO question, it was not supported 8 months ago.

As a fallback, is there a way to get pip to ignore the pyproject.toml file?

@pfmoore
Copy link
Member

pfmoore commented Mar 26, 2021

It's mostly documented in PEPs 517 and 518, as those are the relevant standards and pip implements standard defined behaviour. There's no standard-compliant way to specify a build dependency as a local file relative to the source directory, no.

I'm assuming that putting your towncrier configuration in a file other than pyproject.toml isn't possible? That would be the simplest solution until you're ready to transition to pyproject.toml for your build configuration.

But assuming not, then there are some short-term alternatives. I describe these as "short term", but there's no immediate plan for pip to remove them - it's just that ultimately, pip (and the packaging ecosystem in general) will only support PEP 517/518, and at that point you'll need to have found a standard way of implementing your workflow. But for now, you can probably use --no-build-isolation to get the behaviour you want. Alternatively, --no-use-pep517 will disable all PEP 517 processing - but that definitely will be getting removed at some point, as it's a transition mechanism.

@omry
Copy link
Contributor Author

omry commented Mar 26, 2021

Thanks for your answers @pfmoore.

Black is also using pyproject.toml for configuration. It does support specifying a path in the command line but it's not very friendly to people running black manually.
I don't think towncrier supports an alternative config name though.

Using one of the two plugins is reasonable workaround for now.
Is there a way to make pip use them by default in a project? (maybe something in pyproject.toml? :)
Otherwise I am pretty sure I will receive many complaints about people telling me my project doesn't install from source.

It looks like pip is using the presence of pyprojects.toml as the gate for the pep517 logic.
Is there a possibility of using the presence of any build specific config section in pyprojects.toml to trigger that logic instead?
pyproject.toml is gaining adoptions by many other tools. This will probably become a bigger problem in the medium term (until the ecosystem have transitioned to the new way of configuring the build).

My use case is as follows:
I have a repository that contains a core section and a plugins section (the core and each of the plugins are packages and distributed).
I want to have some shared logic in the build of the plugins and the core.
That build_helper is my attempt at implementing such reuse. I would rather not be forced to distribute build_helper as it can make things pretty complicated.

  1. Are there plans to eventually support local build dependencies?
  2. Are there better practices / examples I can follow to achieve something like this?

@pradyunsg
Copy link
Member

pradyunsg commented Mar 26, 2021

It looks like pip is using the presence of pyprojects.toml as the gate for the pep517 logic.
Is there a possibility of using the presence of any build specific config section in pyprojects.toml to trigger that logic instead?

No, there isn't. This was an intentional design choice to make downstream packages to start adopting the newer packaging practices, such as build isolation and PEP 517, when they try to use the newer standards for the same.

There have been requests for adding such an escape hatch for a while now, and the answer to those is usually to figure out how to make that package work within the model for package builds that we want in the future. As it stands, you can't have the nice things provided by PEP 518 and PEP 517 (pyproject.toml) without spending some effort to make sure your package is compatible with how the newer build process works (build isolation, PEP 517 backends etc). We haven't so far seen a compelling reason to allow using the niceties of the new standards without moving away from practices that we're pushing people away from.

That build_helper is my attempt at implementing such reuse. I would rather not be forced to distribute build_helper as it can make things pretty complicated.

You can use https://www.python.org/dev/peps/pep-0517/#in-tree-build-backends, if what you want to do is have code used only for performing builds. Note that you'd need to wrap an existing PEP 517 build backend for this and I don't think the setuptools backend would be easy to wrap.

@pfmoore
Copy link
Member

pfmoore commented Mar 26, 2021

Is there a way to make pip use them by default in a project?

No there isn't, sorry.

It looks like pip is using the presence of pyprojects.toml as the gate for the pep517 logic.

The wording from PEP 517 is as follows:

If the pyproject.toml file is absent, or the build-backend key is missing, the source tree is not using this specification, and tools should revert to the legacy behaviour of running setup.py (either directly, or by implicitly invoking the setuptools.build_meta:legacy backend).

What pip is doing if the build-backend key is missing, is to use the setuptools.build_meta:__legacy__ backend, which is not 100% compatible with running setup.py directly (as you've found).

Using --no-build-isolation still uses the backend, but does not use build isolation, which is what won't work with your source-relative build helper.
Using --no-use-pep517 reverts to the direct setup.py approach.

Are there plans to eventually support local build dependencies?

No. Doing so would need changes to the standards, and I'm not aware anyone has proposed any such changes. I don't think it's a use case that is sufficiently widely used to have come to the attention of anyone willing to propose a standard (which, just to be clear, means "anyone" - there's no barrier on proposing standards, other than the requirement to be willing to put in the work).

Are there better practices / examples I can follow to achieve something like this?

Not that I know of, sorry. Maybe someone else can offer suggestions. I think most people would distribute the build helper - but that's just a guess and as you say may not be appropriate for you.

@pradyunsg
Copy link
Member

What pip is doing if the build-backend key is missing, is to use the setuptools.build_meta:__legacy__ backend, which is not 100% compatible with running setup.py directly (as you've found).

Wait, using the __legacy__ backend is 100% the same as invoking setup.py (minus going through PEP 517, rather than calling setup.py directly) -- the thing that's messing up this user's build process is the build-isolation related issues.

@piotr-dobrogost
Copy link

Is there a possibility of using the presence of any build specific config section in pyprojects.toml to trigger that logic instead?

See discussion in #8437 and especially this comment #8437 (comment)

@pfmoore
Copy link
Member

pfmoore commented Mar 26, 2021

minus going through PEP 517, rather than calling setup.py directly

Sorry, I was simplifying (or maybe trying too hard to be precise while simplifying). It's the "going through PEP 517" which is the incompatibility, as that triggers build isolation, but yes, the inability to import the local helper is because of build isolation. I wanted to emphasise that --no-use-pep517 is another solution, and avoid "switch off build isolation" seeming like the hammer that deals with everything.

Off-topic for this issue, but there are other incompatibilities - for example the legacy backend supports setup.cfg only projects.

@omry
Copy link
Contributor Author

omry commented Mar 26, 2021

I just went through all the comments in #8437 (took me some time).
I appreciate all the effort you guys are putting into this.

opting out of build isolation

I am going to ignore the requests to enable opting out of build isolation for a moment (through the content of pyproject.toml). This would eliminate the problem for me in the short term. I don't want to add additional pressure to do that though. I read the discussion and I will find a way to work with your decisions there.

Using in-tree build logic

What I am trying to do is to run in-tree logic as part of the build process. Currently this is possible by adding logic to setup.py.
Is this something that will be supported in the future or are we eliminating this support incrementally?

Specifically I have logic to determine the version of my package and I also generate some code with a parser generator (I have some free functions and some build Commands right now).
This alone seems to work without issues, I am guessing that things are just getting copied into the target build tree and my setup.py can find them by luck.

Even excluding the question of how to share some functionality between different setup.py files in my repo, there is a bigger underlying question:
My understanding is that we are trying to kill setup.py altogether, what is the plan for projects like mine that has a setup.py with some custom logic?

A bad solution

It looks like the escape hatch is basically "make your own build backend", something like:

[build-system]
requires = ["setuptools", "wheel"] 
backend-path = ["build_helper"]
build-backend = "build_helper"
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    # my own wheel build here
    
def build_sdist(sdist_directory, config_settings=None):
    # my own sdist build here

I don't think we want each project to make their own broken build system though.
A more practical hack is to maybe wrap the pip build backend to change it's behavior (an example of this can help).

A good (?) solution

I think support for local build dependencies will offer the cleanest solution for my problem. There is probably a pretty large set of projects with complex setup.py that is using in-tree logic, and if I understand things correctly - all those will have a hard time transitioning into the bright new future.
in principle it could be expressed by something like requires = ["./build_helper"] or requires = ["file://build_helper"] (the later is more flexible and can support additional installation types like http or git).

@pfmoore
Copy link
Member

pfmoore commented Mar 26, 2021

@omry Thanks for your thoughtful comments. This is an area where opinions can get very polarised because everyone has their own views as to what the "key use case" for pip should be. I appreciate you taking the time to describe your situation objectively and clearly.

There is probably a pretty large set of projects with complex setup.py that is using in-tree logic, and if I understand things correctly - all those will have a hard time transitioning into the bright new future.

That's a notoriously hard statement to quantify. All I can reasonably say here is that we have seen a number of people complaining about build isolation, but the reasons are varied - and I don't recall any other case of someone with build helper code like you describe. There may be a lot of people struggling with this, but they are suspiciously quiet if so (unfortunately, we have direct experience of what "a pip feature that inconveniences a lot of people" looks like 🙁)

The PEP 517 view

From the perspective of PEP 517, the key is to clearly separate the responsibilities of the installer front end and the build back end, and to ensure that the user (both end user and project developer) does not have to worry about implementation details. Build isolation is an important part of that, as it protects the installer/build system from worrying about what the user might have installed. But conversely, it does make it essential that projects have a way to accurately describe their expectations for the build environment.

Another important part of the equation is that projects are self-contained. To that end, referencing arbitrary locations relative to the source tree is not allowed, because it could result in builds failing because a sibling directory was missing, or something of that nature. However, there is a good use case for projects wanting to include build-specific code within the project. The in-tree hook mechanism was designed for that scenario (although the use case that inspired it was bootstrapping build backends, which is slightly different than your use case).

Using in-tree hooks

You quite reasonably expressed reservations about using the in-tree backend mechanism. However, I think the problem here is a lack of good documentation or good examples, rather than that being a bad solution. The issue is that we designed the in-tree hook mechanism based on a specific use case, that of a build back end wanting to build itself. But the mechanism is general, and can be used for any in-tree build code, it's just that no-one has really used it like that, so there aren't many good examples.

Like most things, someone has to go first, and write up their experiences for everyone else 🙂

To address your specific concerns:

I don't think we want each project to make their own broken build system though.

Absolutely. But there's no reason why projects can't extend an existing build system:

# Not changing this one, so just import it and re-export it unchanged
from setuptools.build_meta import build_sdist
# Wrapping this, so we rename it for our own use
from setuptools.build_meta import build_wheel as orig_build_wheel

def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    # Add helper code to build environment - might even just be sys.path.append(location)
    orig_build_wheel(wheel_directory, config_settings, metadata_directory)

That still requires projects to implement some bits themselves, but it's not too bad.

A more practical hack is to maybe wrap the pip build backend to change it's behavior (an example of this can help).

The above is the sort of thing you can do.

A solution using a "normal" backend

If the use case is sufficiently important, I can imagine backend wrappers being developed and published on PyPI, so you might have:

pyproject.toml:

[build-system]
# wrap-setuptools will add setuptools and wheel to the build requirements,
# so no need to list them explicitly
requires = ["wrap_setuptools"]
build-backend = "wrap_setuptools:add_local_helper"

[tool.wrap_setuptools]
helpers = ["./build_helper"]

The hypothetical wrap_setuptools project would be a normal backend that wraps setuptools in basically the same way as the example code above, and injects a list of build helpers into the build environment before calling setuptools to do the actual build. It could offer a variety of "wrapping" services, selected via pyproject.toml configuration, if that was useful.

I think setuptools_scm might do something like this. If it became a common pattern, conventions might need to be developed to allow users to "compose" multiple wrappers, but that can all be done by the wrapper authors, who presumably will be much more familiar with the use cases and trade-offs, because that's their core focus.

What does this mean for pip?

I'll freely admit that the above is mostly just theorising, based on what I know of the PEP 517 design, and how I personally imagine the build backend ecosystem developing. There's plenty of work to do to make this a reality. But for me, there are crucial advantages:

  • It takes pip (and the pip developers) out of the position of being the bottleneck for every request for this sort of functionality.
  • It makes the feature available for all PEP 517 front ends, not just pip.
  • It puts the design and development of the feature squarely in the hands of the people who would benefit from it, and who are most closely familiar with the actual problem.

I'm not saying that pip will never entertain doing anything that helps in situations like this, but I think such features would need to be weighed against the fact that backend wrapper solutions like this could be developed outside of pip. In particular, it's important to remember that pip features are not "better" just because you¹ pay the cost of writing a backend wrapper but the pip developers pay the cost of a pip feature. I could easily imagine a wrapper backend being cheaper in overall terms than a pip feature.

¹ "you" generally, not you @omry 🙂

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

@pfmoore , I definitely understand (and fear) the pain of having a large community with very strict expectations.
navigating a project with such weight is very challenging.

There is probably a pretty large set of projects with complex setup.py that is using in-tree logic, and if I understand things correctly - all those will have a hard time transitioning into the bright new future.

I am going to throw a few darts at big projects, and then I am going to look at them to see if I suspect they will have issues.
You will have to trust me that I did not edit the initial list:

Here are the setup.py files of each I could along with my untested judgement if they will have issues or not to transfer to a pure data driven build system:

  • PyTorch : Very complex setup.py, in tree logic.
  • TensorFlow : Likely problems, large setup.py customizing setuptools commands.
  • Numpy : Very complex, in-tree logic.
  • OpenCV Python: complex, but also uses pyprojecr.toml, looks ready for the future.
  • Pandas : In tree logic (versioneer, like a rocketeer, but for versions!).
  • Celery: Customizing commands in setup.py, not seeing in-tree logic.
  • Jupyter notebook : Complex setup.py, In tree logic (setupbase.py)
  • Django primarily setup.cfg driven, got some sanity checks. looks ready.
  • Flask: Super clean and ready to go.
  • Pip: Pretty clean, got some common logic in setup.py but I hope this will be resolved in a generic way (version detection).

Tally:
5/10 has in-tree logic.
Almost every project has some non-trivial logic in setup.py.

Granted, those are all big projects and it's certainly possible that they can be ported without a major effort.
Also, they have all been around for a while, so many are not modern.
Finally, I recognize that there are millions of tiny simple projects that will be trivial to port, but I think a major framework is worth a million tiny projects, so there is that :).

I am going to respond to the rest later. I just wanted to put this 1 hour survey in its own comment.

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

I got a slightly simpler solution than the "Using in-tree hooks" one you proposed here.

Turns out any functions exported by the 'build backend' are also available in setup.py.
For that reason, it's enough to re-export the delegated functions and there is no need for customization of sys.path for that simple form.

I tried to do something similar to your improved suggestion ("A solution using a "normal" backend") to see if I can make it work and I was not able to.
I am not sure it's possible, I ran into two problems:

  1. I couldn't figure out how to pass config for pyproject.toml to it.
  2. I suspect that the rest of the source tree is not yet available in the isolated dir when the tree is running.

I did not find a way to determine the original build dir from within the build backend file.

Your point about the benefit of an isolated build environment are clear.
Correct me if I am misunderstanding what pip consider the source tree, but:
From the perspective of pip, when I do pip install dir, I think for it dir is the source tree.

If I have:

project/
  build-helper/
  plugins/plugin1/

Installing plugins/plugin1, pip will not see the complete project source tree.

However, for some project it makes sense to consider the whole checkout tree, even if the installation is of a sub directory.
I know this is an edge case, but something like allowing me to specify the source tree to be several levels above the installed target dir could (potentially) solve that problem.
Can't think of any other workaround for this issue.

@pfmoore
Copy link
Member

pfmoore commented Mar 27, 2021

However, for some project it makes sense to consider the whole checkout tree, even if the installation is of a sub directory.

That is in violation of PEP 517, which views the directory with setup.py/pyproject.toml as the source tree. Therefore, in your example, build-helper is helper code outside the source tree, which violates the principle that sources should be self-contained.

Let me ask this. If you built a sdist for this project, what would it contain? Assume for now that you do setup.py sdist, so pip isn't involved in that question.

I couldn't figure out how to pass config for pyproject.toml to it.

You don't need to, that file is (by definition) at the root of the source tree, so it's in your CWD (build hooks are called with the source directory as CWD).

I suspect that the rest of the source tree is not yet available in the isolated dir when the tree is running.

I suspect that you're meaning "files outside the source tree" when you say "rest" (if we accept that the source tree is rooted in the directory with pyproject.toml as PEP 517 requires).

Here are the setup.py files of each I could along with my untested judgement if they will have issues or not to transfer to a pure data driven build system

No one is suggesting a "pure data driven build system". Setuptools is, and probably always will be, a procedural build system. What we're trying to do is to make more of the build metadata and configuration that is common to all build backends, available without needing to run the project build process. But the build step itself can do whatever it likes.

The root cause of this particular issue is that one piece of data that front ends want to know is "how do I set up a fresh environment which I can use to run the build steps for this project?" Your examples have fundamentally all been of the form "you need to install package X, but package X isn't publicly available".

Hmm, putting the question this way makes me think. In your very original example:

$ tree
.
├── build_helper
│   ├── build_helper
│   │   └── __init__.py
│   └── setup.py
├── example
│   └── __init__.py
├── README.md
└── setup.py

why can't setup.py simply do

import sys
from pathlib import Path
sys.path.append(Path(__file__).parent / "build_helper")

There's no need to actually install build-helper in this case - it's present in the source tree (and presumably shipped in the sdist) so just use it directly.

I assume there's probably some other constraint that you didn't mention, but I'm not clear what that is.

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

That is in violation of PEP 517, which views the directory with setup.py/pyproject.toml as the source tree. Therefore, in your example, build-helper is helper code outside the source tree, which violates the principle that sources should be self-contained.
Let me ask this. If you built a sdist for this project, what would it contain? Assume for now that you do setup.py sdist, so pip isn't involved in that question.

Yeah, I agree. I am expecting the sdist to contain the plugin only.

repo_root/  # repo source tree, excluding plugins and tests etc.
  setup.py
  plugins/p1 # plugin p1 source tree 
    setup.py 

When I pip install . from plugin/p1 or pip install plugin/p1 from the repo root, I am expecting the sdist to be source under plugins/p1 (which is the case).

You don't need to, that file is (by definition) at the root of the source tree, so it's in your CWD (build hooks are called with the source directory as CWD).

I meant passing config to the build hook from pyproject.toml, something like what you described:

[tool.wrap_setuptools]
helpers = ["./build_helper"]

why can't setup.py simply do

import sys
from pathlib import Path
sys.path.append(Path(__file__).parent / "build_helper")

The example in my repro was something that just reproduced my the change in behavior when pyproject.toml was in the repo, it was not emulating the actual problem I had.
The real scenario looks more like what I am describing here (with a repo setup.py and a plugin setup.py).
I did not yet try to apply any of your suggestions on that scenario but as an intermediate step I tried your suggestion ("A solution using a "normal" backend").

Playing it out, I would need:

repo_root/  # repo source tree, excluding plugins and tests etc.
  setup.py
  pyproject.toml
  plugins/p1 # plugin p1 source tree 
    setup.py 
    pyproject.toml # plugin pyproject

The plugin pyproject file would need access to the build hook and it's not in the source tree so I think this is not going to work.

Zooming out a bit:
This is a form of a monorepo with multiple projects (primary (Hydra), plugins and some others).
There is a runtime dependency from the plugins to the primary project (Hydra plugins needs Hydra).
When testing and using plugins, Hydra has to be installed. CI is installing Hydra from the repo (and not the published release) to ensure everything is running against the correct version of the code).

I am trying to apply the same thing (in spirit) to the build itself.
Support for filesystem based build time dependencies will enable it.
I agree with the general sentiment that reproducible builds are good and that build isolation is an important part of it.
In this case, the pieces of code used for the build are in fact a part of the checkout. if they are not there, or they someone contains the wrong version it's my fault, not pip's.
The current support is only for only for published build time dependencies. I can go there but in fact it makes it harder to get a reproducible build because now my self contained checkout need to depended on published bit that may change (unless I pin them to a specific version) between two different builds.

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

Here is a more realistic emulation of the real problem.
See readme in that dir for details.
The intention is to reuse get_version from plugin/p1.
(This is what I really need. I don't need the other fancy custom build commands for the plugins).

Turns out this doesn't work even without pyproject.toml (and even with --no-build-isolation).

While creating it I realized I was probably misinterpreting what you meant by build isolation.
In my mind, this means:

  1. build from a temp location containing only the sdist.
  2. build in an environment containing only the declared build dependencies.

It appears that the temp location is used even with the flag --no-build-isolation.

@sbidoul
Copy link
Member

sbidoul commented Mar 27, 2021

@omry if I understand correctly your problem is not related to the presence of pyproject.toml. It looks like you suffer of #7555. You may want to try the feature flag added in #9091 (--use-feature=in-tree-build) with pip master branch.

@pfmoore
Copy link
Member

pfmoore commented Mar 27, 2021

OK. I think the problem here is fundamentally that pip (and indeed, Python packaging in general) works with projects, which are a single directory tree with a setup.py or pyproject.toml at the top level. The idea of a monorepo as a repository that holds multiple projects is fine, but what you're trying to do is have a project containing multiple sub-projects, which is not a structure that's supported (or even really considered) by Python's packaging tools. So to be perfectly honest you're trying to get the various tools to do something that they were never designed to support.

Here is a more realistic emulation of the real problem.

But that's exactly what I said wasn't supported - the setup.py in plugins/p1 depends on files outside of plugins/p1. Quoting myself from above

To that end, referencing arbitrary locations relative to the source tree is not allowed

So I think you're just going to have to accept that the structure you're using isn't supported (by PEP 517, pip or in general by the packaging ecosystem). Setuptools lets you do this, because setuptools basically lets you do anything you want, but the direction of the packaging ecosystem is generally away from that sort of unstructured referencing of data outside of the project.

There's no reason you can't keep the project structure and workflow that you're using at the moment, but you just need to realise that newer versions of tools are likely to stop supporting this (much like no-one now supports building Python extensions using plain distutils, or using raw makefiles like we used to in pre-distutils days). Whether the cost of using older tools, or applying increasingly hacky workarounds, is worse than the cost of changing your project structure, is something only you can decide, TBH.

@pfmoore
Copy link
Member

pfmoore commented Mar 27, 2021

It appears that the temp location is used even with the flag --no-build-isolation.

Yes. As @sbidoul spotted, this is not related to build isolation, but rather to building the project in a copy of the source tree. I'm confused as to why you originally only hit this when you have a pyproject.toml, but it does sound like in-tree builds will help you.

My musings above still apply, though - what you're doing isn't really the model that the existing standards are based on, so you should expect to hit some rough spots like this.

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

@sbidoul, --use-feature=in-tree-build in master works around the issue I am facing.
Is there a way to get pip to do it by default for specific projects? (maybe something pyproject.toml?)

@pfmoore, I hear you.
I will consider publishing the build helpers, honestly that code is very stable and is almost never changing so I think that approach should not cause issues.

@omry
Copy link
Contributor Author

omry commented Mar 27, 2021

I'm confused as to why you originally only hit this when you have a pyproject.toml

I already had pyproject.toml files (for towncrier and black) in place.
I attempted to reuse build logic between the sub projects and was not successful.
In a clean environment a more basic version of what I was trying to do worked without pyproject.toml, which is why I created that issue.

@sbidoul
Copy link
Member

sbidoul commented Mar 28, 2021

--use-feature=in-tree-build in master works around the issue I am facing.
Is there a way to get pip to do it by default for specific projects? (maybe something pyproject.toml?)

There is currently no plan to enable that on a per-project basis. The goal is to make in-tree builds the default and only way, at some point in the future, following a deprecation period for the current behaviour.

@omry
Copy link
Contributor Author

omry commented Mar 28, 2021

There is currently no plan to enabled that on a per-project basis. The goal is to make in-tree builds the default and only way, at some point in the future, following a deprecation period for the current behaviour.

Could be a good addition to support configuring feature flags in general, but that's a whole new discussion.

@omry
Copy link
Contributor Author

omry commented Mar 28, 2021

I am closing this, thanks for the discussion and help.

Here is a short summary (happy to update if I missed anything important):

  • pip switching on isolated builds based on the presence of a pyproject.toml is controversial but intentional. There is an in-depth discussion at Isolated builds when both pyproject.toml and setup.py co-exist #8437.
  • My underlying issue can be solved by publishing the build helpers or by enabling in-tree build (--use-feature=in-tree-build). In-tree build are planned to become the default.

@omry omry closed this as completed Mar 28, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
C: build logic Stuff related to metadata generation / wheel generation type: support User Support
Projects
None yet
Development

No branches or pull requests

5 participants