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

Testing Python packages which rely on entry points defined in the same package #11386

Closed
michaelboulton opened this issue Dec 24, 2020 · 14 comments · Fixed by #21062
Closed

Testing Python packages which rely on entry points defined in the same package #11386

michaelboulton opened this issue Dec 24, 2020 · 14 comments · Fixed by #21062
Labels
backend: Python Python backend-related issues enhancement

Comments

@michaelboulton
Copy link

I'm trying to use Pants v2 for my project but it relies on some entry points that are defined in the setup.cfg of the package. If I create a wheel using python_distribution, specify the entry_points, and install that wheel then everything works fine, but when I use python_tests() to test the code it won't work because when running the tests Pants doesn't seem to expose entry points from the setup.cfg like tox does, or like doing pip install -e .

Is there a way to get this working or is this something that isn't supported at the moment?

@stuhood
Copy link
Sponsor Member

stuhood commented Dec 29, 2020

@michaelboulton : Hey! Sorry for the delayed response.

When you say that "it relies on some entry points", do you mean that you are not able to call those entry points from within a test? Or are you expecting the entry point code to have run by the time the test does?

If your python_tests target has a dependency that has entry points defined, then you should be able to import and call the entry point.

If there is code that you want to run every time pytest runs (ie, you want one of the entry points to run for "every test"), then you might be able to use a conftest.py file for that: https://www.pantsbuild.org/docs/python-test-goal#conftestpy

If I'm way off the mark here, please feel free to drop into Slack to discuss it!

@jsirois
Copy link
Contributor

jsirois commented Dec 30, 2020

I'm guessing @michaelboulton means this: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html#console-scripts
IOW Pants does not setup console scripts and make them available on the PATH like tox does; so tests that attempt to shell out to those console scripts will fail.

@michaelboulton this isn't supported at the moment. Pants can produce a distribution as outlined here: https://www.pantsbuild.org/docs/python-distributions. That distribution can include console scripts via with_binaries as explained there, but Pants currently only produces distributions via the package goal. It does not install them*.

This feature probably makes sense to support but will require some design and code work if I've understood your use case correctly.

* Pants does install third party packages when they are dependencies (requirements) of your first party Python code; but, even then, the current method of installation does not properly expose third party console scripts**.

** Pants uses Pex to resolve third party requirements and recent changes in Pex will allow Pants to properly expose third party console scripts on the PATH of Python processes it executes.

@Eric-Arellano
Copy link
Contributor

Pants can produce a distribution as outlined here: https://www.pantsbuild.org/docs/python-distributions. That distribution can include console scripts via with_binaries as explained there, but Pants currently only produces distributions via the package goal. It does not install them*.

It does not, but you can include the built package in your chroot as a PEX file! See https://www.pantsbuild.org/docs/python-test-goal#depending-on-packages

@jsirois
Copy link
Contributor

jsirois commented Dec 30, 2020

It does not, but you can include the built package in your chroot as a PEX file!

That's a nice feature and may serve as a workaround here. It is bloated though. For a python package with many console scripts - say 3, and a test that tests them all, this would require building 3 pexes to include in the test chroot with identical contents save for the default entrypoint.

@michaelboulton
Copy link
Author

From reading the discussion I'm guessing what I want isn't supported yet - for reference I'm looking for support for the metadata.entry_points() function like with pytest plugins https://docs.pytest.org/en/stable/writing_plugins.html#making-your-plugin-installable-by-others where you can define a generic entry point, not just console_scripts

@jsirois
Copy link
Contributor

jsirois commented Jan 1, 2021

Aha - thanks for the clarification @michaelboulton. Indeed - we have no way of supporting that today. For that, the <project-version>.dist-info/entry_points.txt needs to be present on the sys.path which is what you get from a normally (or editable) installed package ala the way tox treats a project. We could and probably should definitely support this.

@thejcannon
Copy link
Member

@Eric-Arellano this should be doable now with runtime_dependencies right?

@Eric-Arellano
Copy link
Contributor

As a workaround, but I think the feature request is different, per John's reply:

That's a nice feature and may serve as a workaround here. It is bloated though. For a python package with many console scripts - say 3, and a test that tests them all, this would require building 3 pexes to include in the test chroot with identical contents save for the default entrypoint.

@jsirois
Copy link
Contributor

jsirois commented May 24, 2022

I think this is doable - suboptimally again, but less so - with a dependency on a python_distribution target. IIUC that nets you the depended upon python_distribution as an installed distribution visible on the sys.path.

Unfortunately, I think this is all still clunky to get setup correctly. A naive attempt at a demo in the Pants repo:

$ cat test.py 
from importlib.metadata import distribution


def test_metadata_discoverable() -> None:
    dist = distribution("pantsbuild.pants")
    print(dir(dist))

$ cat BUILD.test 
python_test(
    name="test",
    source="test.py",
    dependencies=[
        "src/python/pants:pants-packaged",
    ]
)

Nets:

$ ./pants test test.py
17:34:00.43 [INFO] Initializing scheduler...
17:34:00.77 [INFO] Scheduler initialized.
17:34:05.63 [INFO] Completed: Building pytest_runner.pex
17:34:05.64 [ERROR] 1 Exception encountered:

Engine traceback:
  in select
  in pants.core.goals.test.run_tests
  in pants.backend.python.goals.pytest_runner.run_python_test (//:test)
  in pants.backend.python.goals.pytest_runner.setup_pytest_for_target
  in pants.backend.python.util_rules.pex.create_venv_pex (pytest_runner.pex)
  in pants.backend.python.util_rules.pex.build_pex (pytest_runner.pex)
  in pants.engine.process.fallible_to_exec_result_or_raise
Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/pants/src/python/pants/engine/process.py", line 277, in fallible_to_exec_result_or_raise
    process_cleanup=process_cleanup.val,
pants.engine.process.ProcessExecutionFailure: Process 'Building pytest_runner.pex' failed with exit code 1.
stdout:

stderr:
Traceback (most recent call last):
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 517, in execute
    exit_value = self._wrap_coverage(self._wrap_profiling, self._execute)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 422, in _wrap_coverage
    return runner(*args)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 453, in _wrap_profiling
    return runner(*args)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 573, in _execute
    return self.execute_entry(self._pex_info.entry_point)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 751, in execute_entry
    return self.execute_pkg_resources(entry_point)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/3bd0d0039760eaecc336dc2a645a570f613ecb1f/.bootstrap/pex/pex.py", line 783, in execute_pkg_resources
    return runner()
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/bin/pex.py", line 768, in main
    env=env,
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/bin/pex.py", line 788, in do_main
    cache=ENV.PEX_ROOT,
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/bin/pex.py", line 708, in build_pex
    pex_builder.set_script(options.script)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/pex_builder.py", line 349, in set_script
    distributions.update(PEX(pex, interpreter=self._interpreter).resolve())
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/orderedset.py", line 45, in update
    for key in iterable:
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/pex.py", line 134, in resolve
    for dist in env.resolve():
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/environment.py", line 492, in resolve
    for fingerprinted_distribution in self.resolve_dists(all_reqs)
  File "/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/95b65ab5df10fc9f20ba5e336f6865cfcbfc9d1e78d6e4e22c53582e97e633a7/pex-2.1.88-py2.py3-none-any.whl/pex/environment.py", line 583, in resolve_dists
    pex=self._pex, platform=self._target.platform.tag, items="\n".join(items)
pex.environment.ResolveError: Failed to resolve requirements from PEX environment @ /home/jsirois/.cache/pants/named_caches/pex_root/unzipped_pexes/6e72520a74e10f7752d06a319c20c281cee1cb7c.
Needed cp37-cp37m-manylinux_2_35_x86_64 compatible dependencies for:
 1: PyYAML<7.0,>=6.0
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'PyYAML' distributions.
 2: ansicolors==1.1.8
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'ansicolors' distributions.
 3: fasteners==0.16.3
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'fasteners' distributions.
 4: humbug==0.2.7
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'humbug' distributions.
 5: ijson==3.1.4
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'ijson' distributions.
 6: packaging==21.3
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'packaging' distributions.
 7: pex==2.1.88
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'pex' distributions.
 8: psutil==5.9.0
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'psutil' distributions.
 9: python-lsp-jsonrpc==1.0.0
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'python-lsp-jsonrpc' distributions.
 10: setproctitle==1.2.2
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'setproctitle' distributions.
 11: setuptools<58.0,>=56.0.0
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'setuptools' distributions.
 12: toml==0.10.2
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'toml' distributions.
 13: types-PyYAML==6.0.3
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'types-PyYAML' distributions.
 14: types-setuptools==57.4.7
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'types-setuptools' distributions.
 15: types-toml==0.10.3
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'types-toml' distributions.
 16: typing-extensions==4.0.1
    Required by:
      FingerprintedDistribution(distribution=pantsbuild.pants 2.13.0.dev3 (/home/jsirois/.cache/pants/named_caches/pex_root/installed_wheels/5903f90ecc5e651453998b9647259234ee96a872ebc613395d404e4cf70cabe8/pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl), fingerprint='1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772')
    But this pex had no 'typing-extensions' distributions.



Use `--no-process-cleanup` to preserve process chroots for inspection.

This is because local dists are not 1st class and we don't follow their dependencies:

$ ./pants --no-process-cleanup test test.py
17:28:42.89 [INFO] Preserving local process execution dir /tmp/process-executionoVJ7e2 for "Building pytest_runner.pex"
...
$ jq . /tmp/process-executionoVJ7e2/local_dists.pex/PEX-INFO
{
  "bootstrap_hash": "71ad9fbe4bac6b8f6930a9d18386151795658de0",
  "build_properties": {
    "pex_version": "2.1.88"
  },
  "code_hash": "06d996e1db3306289ba25e839ffeb0b4a7264c53",
  "distributions": {
    "pantsbuild.pants-2.13.0.dev3-cp37-cp37m-linux_x86_64.whl": "1a43b442ab72d35f0e3d36f738dde18b45b1d9c315a4739b6cb28a7d42ee3772"
  },
  "emit_warnings": false,
  "ignore_errors": false,
  "includes_tools": false,
  "inherit_path": "false",
  "interpreter_constraints": [],
  "pex_hash": "6e72520a74e10f7752d06a319c20c281cee1cb7c",
  "pex_path": null,
  "requirements": [
    "pantsbuild.pants==2.13.0.dev3"
  ],
  "strip_pex_env": true,
  "venv": false,
  "venv_bin_path": "false",
  "venv_copies": false,
  "venv_site_packages_copies": false
}

@koffie
Copy link

koffie commented May 30, 2022

@jsirois I ran in the same problem of dependencies not being tracked of python distributions at: #15688

Do you know where to manually specify these as a workaround and not have the building of pytest_runner.pex fail because of this?

@achimnol
Copy link
Sponsor

I've workarounded this issue by writing a custom entrypoint scanner based on BUILD files.:
https://github.com/lablup/backend.ai/blob/main/src/ai/backend/plugin/entrypoint.py#L63

@benjyw
Copy link
Sponsor Contributor

benjyw commented Oct 21, 2022

I posted a possible solution on #17308

In that if the entry_points.txt is wrapped in a resources() target and the test depends on it, then it should be on the sys.path at test time.

@cognifloyd
Copy link
Member

Exposing entry points is actually fairly easy even without installing the package or using pip -e.

I wrote a rule in #18132 that generates an entry_points.txt using a subset of the entry points in each relevant python_distribution. Something similar could be done so that if a python_distribution is in dependencies of a python_test/python_tests target, the rule would add entry_points.txt so that the package appears to be installed when using the setuptools utils to lookup metadata from that package.

@cognifloyd cognifloyd added the backend: Python Python backend-related issues label Mar 13, 2023
cognifloyd added a commit that referenced this issue Apr 10, 2023
## About Editable Installs

Editable installs were traditionally done via pip with `pip install
--editable`. It is primarily useful during development when software
needs access to the entry points metadata.

When [PEP 517](https://peps.python.org/pep-0517/) was adopted, they
punted on how to allow for editable installs. [PEP
660](https://peps.python.org/pep-0660/) extended the PEP 517 backend API
to support building "editable" wheels. Therefore, there is now a
standard way to collect and install the metadata for "editable"
installs, using the "editable" wheel as an interchange between the
backend (which collects the metadata + builds the editable wheel) and
the frontend (which marshals the backend to perform a user-requested
"editable" install).

## Why would we need editable installs in pants?

I need editable installs in pants-exported virtualenvs so that dev tools
outside of pants have access to:
- The locked requirements
- The editable sources on the python path
- The entry points (and any other package metadata that can be loaded
from `dist-info`. Entry points is the biggest most impactful example)

I need to point all the external dev tooling at a virtualenv, and
technically I could export a pex that includes all of the
python-distributions pre-installed and use pex-tools to create a
virtualenv, but then I would have to recreate that venv for every dev
change wouldn't be a good dev experience.

One of those dev tools is `nosetest`. I considred using `run` to support
running that, but I am leary of adding all the complex BUILD machinery
to support running a tool that I'm trying to get rid of. Editable
installs is a more generic solution that serves my current needs, while
allowing for using it in other scenarios.

This PR comes in part from
#16621 (comment)

## Overview of this PR

### Scope & Future work

This PR focuses on adding editable installs to exported virtualenvs. Two
other issues ask for editable installs while running tests:
- #11386
- #15481

We can probably reuse a significant portion of this to generate editable
wheels for use in testing as well. Parts of this code will need to be
refactored to support that use case. But we also have to figure out the
UX for how users will define dependencies on a `python_distribution`
when they want an editable install instead of the built wheel to show up
in the sandbox. Anyway, addressing that is out of scope for this PR.

### New option `[export].py_editables_in_resolves` (a `StrListOption`)

This option allows user resolves to opt in to using the editable
installs. After
[consulting](https://pantsbuild.slack.com/archives/C0D7TNJHL/p1680810411706569?thread_ts=1680809713.310039&cid=C0D7TNJHL)
with @kaos, I decided to add an option for this instead of always trying
to generate/install the editable wheels.

> `python_distribution` does not have a `resolve` field. So figuring out
which resolve a `python_distribution` belongs to can be expensive:
calculating the owned deps of all distributions, and for each
distribution, look through those deps until one of them has a resolve
field, and use that for that dist’s resolve.
>
> Plus there’s the cost of building the PEP-660 wheels - if the
configured PEP-517 build backend doesn’t support the PEP-660 methods,
then it falls back to a method that is, sadly, optional in PEP-517. If
that method isn’t there, then it falls back to telling that backend to
build the whole wheel, and then it extracts just the dist-info directory
from it and discards the rest.
>
> So, installing these editable wheels isn’t free. It’ll slow down the
export, though I’m not sure by how much.

For StackStorm, I plan to set this in `pants.toml` for the default
resolve that has python_distributions.

Even without this option, I tried to bail out early if there were no
`python_distribution`s to install.

### Installing editable wheels for exports

I added this feature to the new export code path, which requires using
`export --resolve=`. The legacy codepath, which uses cli specs `export
<address specs>` did not change at all. I also ignored the `tool`
resolves which cannot have any relevant dists (and `tool` resolves are
deprecated anyway). Also, this is only for `mutable_virtualenv` exports,
as we need modify the virtualenv to install the editable wheels in the
venv after pex creates it from the lockfile.

When exporting a user resolve, we do a `Get(EditableLocalDists,
EditableLocalDistsRequest(resolve=resolve))`: _I'll skip over exactly
how this builds the wheels for now so this section can focus on how
installing works._


https://github.com/pantsbuild/pants/blob/f3a4620e81713f5022bf9a2dd1a4aa5ca100d1af/src/python/pants/backend/python/goals/export.py#L373-L379

As described in the commit message of
b5aa26a, I tried a variety of methods
for installing the editable wheels using pex. Ultimately, the best I
came up with is telling pex that the digest containing our editable
wheels are `sources` when building the `requirements.pex` used to
populate the venv, so that they would land in the virtualenv (even
though they land as plain wheel files.

Then we run `pex-tools` in a `PostProcessingCommand` to create and
populate the virtualenv, just as we did before this PR.

Once the virtualenv is created, we add 3 more `PostProcessingCommands`
to actually do the editable install. In this step, Pants is essentially
acting as the PEP-660 front end, though we use pip for some of the heavy
lifting. These commands:
1. move the editable wheels out of the virtualenv lib directory to the
temp dir that gets deleted at the end of the export
2. use pip to install all of the editable wheels (which contain a `.pth`
file that injects the source dir into `sys.path` and a `.dist-info`
directory with dist metadata such as entry points).
3. replace some of the pip-generated install metadata
(`*.dist-info/direct_url.json`) with our own so that we comply with
PEP-660 and mark the install as editable with a file url pointing the
the sources in the build_root (vs in a sandbox).

Now, anything that uses the exported venv should have access to the
standardized package metadata (in `.dist-info`) and the relevant source
roots should be automatically included in `sys.path`.

### Building PEP-660 editable wheels

The logic that actually builds the editable wheels is in
`pants.backend.python.util_rules.local_dists_pep660`. Building these
wheels requires the same chroot that pants uses to build regular wheels
and sdists. So, I refactored the rule in `util_rules.setup_py` so that I
could reuse the part that builds the `DistBuildRequest`.

These `local_dists_pep660` rules do approx this, starting with the rule
called in export:
- `Get(EditableLocalDists, EditableLocalDistsRequest(resolve=resolve))`
uses rule `build_editable_local_dists`
- injected arg: `ResolveSortedPythonDistributionTargets` comes from
rule: `sort_all_python_distributions_by_resolve`
- injected arg: `AllPythonDistributionTargets` comes from rule:
`find_all_python_distributions`
- `Get(LocalDistPEP660Wheels, PythonDistributionFieldSet.create(dist))`
for each dist in the resolve uses rule:
`isolate_local_dist_pep660_wheels`
- create `DistBuildRequest` using the `create_dist_build_request` method
I exposed in `util_rules.setup_py`
- `Get(PEP660BuildResult, DistBuildRequest)` uses rule:
`run_pep660_build`
            - generates the `.pth` file that goes in the editable wheel
            - runs a PEP 517 build backend wrapper script I wrote
- uses the PEP 517 build backend configured for the
`python_distribution` to generate the `.dist-info` directory
- generates the `WHEEL` and `RECORD` file to build a conformant wheel
file
- includes the `.pth` file previously generated (and placed in the
sandbox with the wrapper script)
- uses `zipfile` to build the wheel (using a vendored+modified function
from the `wheel` package).
                - prints a path to the generated wheel
- collects the generated editable wheel into a digest and collects
metadata about the digest similar to how the `local_dists` rules do.
- merges the editable wheel digests for all of the `python_distribution`
targets. This gets wrapped in `EditableLocalDists`

Much of the rule logic was based on (copied then modified):
`pants.backend.python.util_rules.dists` and
`pants.backend.python.util_rules.local_dists`.
@cognifloyd
Copy link
Member

I'm adding an entry_point_dependencies to python_test/s targets in: #21062
I believe that will close this request. Does that interface make sense + work for the use cases in this issue?

cognifloyd added a commit that referenced this issue Jun 20, 2024
…from a python_distribution (#21062)

This adds a new `entry_point_dependencies` field to `python_tests` and
`python_test` targets.

This allows tests to depend on a subset (or all) of the `entry_points`
defined on `python_distribution` targets (only on the `entry_points`
field, not in the `provides` field).

For example:
```python
python_tests(
    name="tests",
    entry_point_dependencies={
        "//address/to:python_distro_tgt_1": ["*"],  # all entry points
        # only one group of entry points
        "//address/to:python_distro_tgt_2": ["console_scripts"],
        "//address/to:python_distro_tgt_4": ["st2common.runners.runner"],
        # or multiple groups of entry points
        "//address/to:python_distro_tgt_5": ["console_scripts", "st2common.runners.runner"],
        # or 1+ individual entry points
        "//address/to:python_distro_tgt_6": ["console_scripts/foo-bar"],
        "//address/to:python_distro_tgt_7": ["console_scripts/foo-bar", "console_scripts/baz"],
        "//address/to:python_distro_tgt_8": ["st2common.runners.runner/action-chain", "console_scripts/foo-bar"],
    },
)
```

A dependency defined in `entry_point_dependencies` emulates an editable
install of those `python_distribution` targets. Instead of including all
of the `python_distribution`'s sources, only the specified entry points
are made available. The entry_points metadata is also installed in the
pytest sandbox so that tests (or the code under test) can load that
metadata via `pkg_resources` or `importlib.metadata.entry_points` or
similar.

Misc notes:
- I added this to the `pants.backend.experimental.python` backend and
also registered the rules I extracted from
`pants.backend.experimental.python.framework.stevedore` with the
stevedore backend since it needs those rules.
- Unlike all other dependencies fields, `entry_point_dependencies` is a
`dict[str, list[str]]`. The `.keys()` of the dict are logically similar
to standard dependencies fields. Using a dict provides more granular
control over the dependencies.
- Also unlike other dependencies fields, this field does NOT result in a
dependency on the `python_distribution` target. Instead, that target
provides the `entry_points` metadata to infer dependencies on the actual
code for those entry points.
- I extracted most of the logic for this from the experimental stevedore
plugin. Enabling the stevedore plugin is not required to use this
feature.

Closes #11386
Closes #15481
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend: Python Python backend-related issues enhancement
Projects
None yet
9 participants