8000 Allow scripted modules to declare requirements and constraints, and enforce Slicer Core constraints. by allemangD · Pull Request #8181 · Slicer/Slicer · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow scripted modules to declare requirements and constraints, and enforce Slicer Core constraints. #8181

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

allemangD
Copy link
Contributor
@allemangD allemangD commented Jan 24, 2025

Work toward #7707. A summary:

  • Preinstall https://github.com/astral-sh/uv in the Slicer Python environment. This is needed for faster dependency resolution; other features like python installation management are disabled via environment variables.
  • The LazyImportGroup from the prior revision is renamed to slicer.deps.GuardedImports. I still don't love this name.
  • Any occurrence of a GuardedImports context manager registers its requirements.txt as a constraint that applies to all invocations of pip_install. GuardedImports also use pip_install to install missing dependencies.
  • Bundled Slicer packages (scipy, numpy, etc.) also have a constraint which is enforced by all invocations of pip_install and GuardedImports.

Together, the intent is for extension developers to declare their dependencies in a requirements.txt, have clear error messages and logs if any of those dependencies are incompatible with bundled packages or other extensions, and support static analysis via true import syntax. See #7707 for more details.

This PR is a draft for four reasons:

  • First, I have manually copied all the builtin requirements into slicer.deps:core-constraints.txt. This file must be generated automatically before the PR should be accepted. I see a couple options to do this, either generate the file via Utilities/Scripts/python_package_updater.py; or generate that file in CMake. For example see the ${requirements_file} for numpy in https://github.com/Slicer/Slicer/blob/main/SuperBuild/External_python-numpy.cmake#L30-L31. It is unclear how/when this file should be installed - probably not when the superbuild is configured, since we probably don't want disabled packages to be constrained.

  • Second, I need to improve reporting to the user. There are some # todo throughout the pip_install and pip_uninstall implementations. These are points where we would inspect the uv pip install return code and stdout to assemble and appropriate message for the user. Critically, it should be possible for the user to cancel the installation.

  • Third, I want to evaluate just how necessary the dependency on uv is. I suspect that it is worth it, but I want to benchmark dependency resolution on a large system, for example resolving different-but-compatible versions of torch and its dependencies. I haven't investigated the torch dependency graph enough yet to build this benchmark.

  • Fourth, developer documentation must be updated to explain where to place the requirements.txt and how to deal with the new error messages that will appear. To be clear, these are not new modes of failure, but they might not be ones that extension developers have yet considered. The error messages and documentation should be clear enough that a developer can quickly deal with the issue. (The brief advice should be: almost always use ~= version specifiers.)


  • I'd also like to clean up the commit history a bit before merging, but that's not as important as the functionality issues above.

I welcome comments on the general approach or advice on how to generate that core-constraints.txt file.

@allemangD allemangD requested a review from jcfr January 24, 2025 23:07
@allemangD
Copy link
Contributor Author
allemangD commented Jan 29, 2025

After writing the note about "halted import" for f10e7d7 I need to verify:

If module A uses a guarded import on eg. itk, and module B does not (say it still uses manual pip_install calls), it is possible for module A to halt module B's import itk. This seems undesirable, but I think it does indicate a true bug.


For more detail: Suppose module A guards import itk, and legacy module B uses explicit pip_install('itk') after an "install" button is pressed.

If the user causes module A to resolve the dependency first, things are fine.

But if the user causes module B to resolve the dependency first, the pip_install would succeed but the import itk would be halted.

pip_install needs to somehow detect which modules have been installed - even if it's used explicitly outside the context of a GuardedImport - and un-halt those modules.

If that logic can't be done or is too cumbersome, I think it is better to remove the import halting feature altogether. It is intended to help detect bugs that might go unnoticed. It is not worth it if the presence of a new module breaks older modules' existing code.

Unconditionally bundle UV in slicer if Python is enabled.
This is a combination of several commits:

WIP: Initial lazy import implementation
WIP: Test scaffolding for lazy import implementation
WIP: Start building tests
lazy loading wip
WIP: pip_install uses uv and slicer core constraints
WIP: pip_install respects guarded requirements
WIP: Set relevant UV environment variables
WIP: style and comments
WIP: Fix tests after restructure
Revert "WIP: Fix tests after restructure"
  This reverts commit 5253f72.
- Generate sensible error messages and prompts by inspecting a `--dry-run`
- Allow the user to cancel installation.
- Remove halted imports as they may break other modules.
- Support separate requirements.txt and constraints.txt for tricky ML use cases.

This is a combination of multiple commits:

ENH: Dry run, user cancellation, sensible error messages and logs.
WIP: Rename tests.
WIP: Support separate requirements.txt and constraints.txt
ENH: Naming improvements.
FIX: Remove halted imports. Improve proxy module.
DOC: Add new python_packages doc to developer guide.
DOC: Update pip_install to point to new python_packages doc.
DOC: Notes about pip_install, pip_uninstall, uv, and errors.
DOC: Notes about requirements, constraints, import groups, and halted import.
DOC: Fix table of contents, typos
DOC: Update docs with recent changes. Document manual usages.
DOC: Improve introduction.
The names of various classes and functions are more clear now.

- FileIdentifier
- Requirements
@allemangD
Copy link
Contributor Author
allemangD commented Jan 31, 2025

cc @jcfr, @lassoan, @pieper. Readthedocs for this PR are now comprehensive. https://slicer--8181.org.readthedocs.build/en/8181/developer_guide/python_packages.html

I'd appreciate any feedback or unsupported edge-cases you foresee on this. The goal is to primarily support things in discrete layers of abstraction:

  1. Plain pip_install. Most flexible, but a bit of friction.
pip_install('itk')

pip_install('itk', interactive=False)

pip_install(requirements=FileIdentifier('Sample Module', '.:requirements.txt'))

register_constraints(FileIdentifier('Sample Module', '.:constraints.txt'))

import itk
  1. Requirements groups with manual resolution. Less friction, but not quite as flexible.
requirements = Requirements('.:requirements.txt', name="Sample Module")

try:
  requirements.resolve()
except InstallAbortedError:
  ...
  
import itk
  1. Requirements groups with manual resolution, and proxy modules for static analysis. I think the static analysis makes the proxy module tradeoffs worth it.
from __future__ import annotations

with Requirements('.:requirements.txt') as requirements:
  import itk
  
try:
  requirements.resolve()
except InstallAbortedError:
  ...
  
im: itk.Image = itk.imread(...)
  1. Fully automatic installation and proxy modules. Mostly seamless, as long as the user accepts installation. Lowest barrier to entry by far.
from __future__ import annotations

with Requirements('.:requirements.txt'):
  import itk
  
im: itk.Image = itk.imread(...)

Note that all four of these layers will respect the constraints registered by core slicer and other modules; so it is already not possible to violate core-constraints.txt. If developers begin to use register_constraints (or, better, Requirements groups) it will be much harder to break the Python environment.


@jcfr RE my messages yesterday: I did remove import halting mentioned in #8181 (comment). It causes more problems than it solves.

Now that I think the interface is hardened, I plan to translate my ad-hoc testing modules into comprehensive unit tests. I also should take a look at the API documentation to be sure it's not out of date.

I need some help on automatically generating core-constraints.txt. Ideally, only the packages which are installed to the current build will be added to this file. I do not think it is appropriate to do this from Utilities/Scripts/python_package_updater.py since it is decoupled from the build and can't know which components are actually in use.

I think it is possible to do this from CMake, but my attempts to do this did not work. Each external python package needs to append ${requirements_file} to core-constraints.txt as part of its install step, not as part of the superbuild configure step. I haven't figured out how to get that timing.

Also, because of the requirement PyJWT[crypto]==2.8.0 it is not a valid constraints file (constraints can't list [extras]). We'd either need to modify the script to output a constraints file separately, or run the requirements file through something like uv pip compile to get suitable output.

`uv pip` supports extras in constraints, but `pip` does not. Make `core-constraints.txt` pip-compatible via `uv pip compile`.

`PyJWT[crypto]==2.8.0` is currently the only problematic constraint.
@allemangD allemangD marked this pull request as ready for review January 31, 2025 20:44
@allemangD allemangD changed the title Allow scripted modules to declare dependencies and constraints. Allow scripted modules to declare requirements and constraints, and enforce Slicer Core constraints. Jan 31, 2025
dependency.function()
# ^ Resolution occurs here.

See `Automatic Installation <https://slicer.readthedocs.io/en/latest/developer_guide/python_packaging.html#manual-installation>`_.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This obviously doesn't work right in the CI readthedocs. I assume there is some way to get reST to do this properly?

Comment on lines +84 to +86
# An omitted package_name is a relative import for convenience.
if not package_name:
package_name = "."
Copy link
Contributor Author
@allemangD allemangD Feb 18, 2025