diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index a9637c27167..aeb2b8a2996 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -23,6 +23,8 @@ jobs: env: # minimum supported version of Python PYTHON_VER1: '3.10' + # force installation of CPU-only PyTorch + PIP_EXTRA_INDEX_URL: 'https://download.pytorch.org/whl/cpu' steps: - uses: actions/checkout@v7 - name: Set up Python ${{ env.PYTHON_VER1 }} diff --git a/.github/workflows/cicd_tests.yml b/.github/workflows/cicd_tests.yml index d48e3a02f22..807bc4ad2cc 100644 --- a/.github/workflows/cicd_tests.yml +++ b/.github/workflows/cicd_tests.yml @@ -75,7 +75,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - python -m pip install --no-build-isolation -r requirements-dev.txt + python -m pip install .[all,testing] - name: Lint and type check run: | # clean up temporary files @@ -138,15 +138,14 @@ jobs: - name: Prepare pip wheel run: | which python - python -m pip install --upgrade pip wheel - python -m pip install --user more-itertools>=8.0 + python -m pip install --upgrade pip wheel tomli - name: Install the minimum dependencies run: | # min. requirements python -m pip install torch==${{ matrix.pytorch-version }} - python -m pip install -r requirements-min.txt + python monai/config/print_dependencies.py build-system | xargs pip install --no-build-isolation + python -m pip install --no-build-isolation .[testing] python -m pip list - BUILD_MONAI=0 python setup.py develop # no compile of extensions shell: bash - if: matrix.os == 'linux-gpu-runner' name: Print GPU Info @@ -175,6 +174,7 @@ jobs: timeout-minutes: 120 env: QUICKTEST: True + BUILD_MONAI: 1 steps: - if: runner.os == 'Linux' name: Clean unused tools @@ -219,16 +219,17 @@ jobs: shell: bash - name: Install the complete dependencies run: | - python -m pip install --user --upgrade pip wheel pybind11 # TODO: pybind11 added for macOS, may not be needed - cat "requirements-dev.txt" - python -m pip install --no-build-isolation -r requirements-dev.txt + python -m pip install --user --upgrade pip wheel tomli + python monai/config/print_dependencies.py build-system | xargs pip install --no-build-isolation + python -m pip install --no-build-isolation .[all,testing] python -m pip list - python -m pip install --no-build-isolation -e . # test no compile installation shell: bash - name: Run compiled (${{ runner.os }}) run: | python -m pip uninstall -y monai - BUILD_MONAI=1 python -m pip install --no-build-isolation -e . # compile the cpp extensions + python -m pip install --no-build-isolation -e . # compile the cpp extensions in-place with -e + # ensure extensions were compiled + python -c 'import monai._C' &> /dev/null shell: bash - if: runner.os != 'macOS' name: Run full tests @@ -275,14 +276,14 @@ jobs: cache: 'pip' - name: Install dependencies + nvsubquadratic (no-deps) run: | - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip wheel tomli pytest + python monai/config/print_dependencies.py build-system | xargs pip install --no-build-isolation python -m pip install torch==${PYTORCH_VER1} torchvision==${TORCHVISION_VER1} - python -m pip install --no-build-isolation -r requirements-dev.txt - python -m pip install -e . - # nvsubquadratic runtime imports need only torch + einops + omegaconf; install - # the package itself without its core dependency tree (see job comment above). - python -m pip install omegaconf - python -m pip install --no-deps 'nvsubquadratic>=0.1.1' + # # nvsubquadratic runtime imports need only torch + einops + omegaconf; install + # # the package itself without its core dependency tree (see job comment above). + # python -m pip install omegaconf + # python -m pip install --no-deps 'nvsubquadratic>=0.1.1' + python -m pip install --no-build-isolation .[hyena,testing] python -m pip list shell: bash - name: Run Hyena tests (CUDA-required cases skip cleanly) @@ -317,11 +318,13 @@ jobs: cache: 'pip' - name: Install dependencies run: | - python -m pip install --user --upgrade pip setuptools wheel twine packaging + python -m pip install --user --upgrade pip setuptools wheel twine packaging tomli # install the latest pytorch for testing # however, "pip install monai*.tar.gz" will build cpp/cuda with an isolated # fresh torch installation according to pyproject.toml python -m pip install torch==${PYTORCH_VER1} torchvision --extra-index-url https://download.pytorch.org/whl/cpu + python monai/config/print_dependencies.py build-system all testing > requirements.txt + pip install --no-build-isolation -r requirements.txt - name: Check packages run: | python -m pip uninstall -y monai diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3b4b427c990..ce1a0b98934 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -67,8 +67,8 @@ jobs: rm -rf /opt/hostedtoolcache/{node,go,Ruby,Java*} ls -al /opt/hostedtoolcache rm -rf /usr/share/dotnet/ - python -m pip install -U --no-build-isolation pip wheel wheel-stub - python -m pip install --no-build-isolation -r requirements-dev.txt + python -m pip install -U pip wheel wheel-stub + python -m pip install .[all,testing] BUILD_MONAI=1 ./runtests.sh --build - name: Perform CodeQL Analysis diff --git a/.github/workflows/cron-ngc-bundle.yml b/.github/workflows/cron-ngc-bundle.yml index a8539b284c5..650434eb37e 100644 --- a/.github/workflows/cron-ngc-bundle.yml +++ b/.github/workflows/cron-ngc-bundle.yml @@ -29,8 +29,8 @@ jobs: - name: Install dependencies run: | rm -rf /github/home/.cache/torch/hub/bundle/ - python -m pip install --no-build-isolation --upgrade pip wheel wheel-stub - python -m pip install --no-build-isolation -r requirements-dev.txt + python -m pip install -U pip wheel wheel-stub + python -m pip install .[all,testing] - name: Loading Bundles run: | # clean up temporary files diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 8ff912f5281..8fcce6af3ab 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -43,7 +43,7 @@ jobs: python -m pip install --upgrade pip wheel python -m pip uninstall -y torch torchvision python -m pip install ${{ matrix.pytorch }} - python -m pip install -r requirements-dev.txt + python -m pip install .[all,testing] python -m pip list - name: Run tests report coverage env: @@ -91,7 +91,7 @@ jobs: run: | which python python -m pip install --upgrade pip wheel - python -m pip install -r requirements-dev.txt + python -m pip install .[all,testing] python -m pip list - name: Run tests report coverage env: @@ -137,7 +137,8 @@ jobs: - name: Install the dependencies run: | which python - python -m pip install --upgrade pip wheel twine + python -m pip install --upgrade pip wheel twine tomli + python monai/config/print_dependencies.py build-system all testing | xargs -d '\n' pip install --no-build-isolation python -m pip list - name: Run tests report coverage shell: bash @@ -171,7 +172,6 @@ jobs: python -c 'import monai; print(monai.__file__)' # run tests - cp $root_dir/requirements*.txt "$tmp_dir" cp -r $root_dir/tests "$tmp_dir" pwd ls -al @@ -186,7 +186,6 @@ jobs: python -c $'import torch\na,b=torch.zeros(1,device="cuda:0"),torch.zeros(1,device="cuda:1");\nwhile True:print(a,b)' > /dev/null & python -c "import torch; print(torch.__version__); print('{} of GPUs available'.format(torch.cuda.device_count()))" - python -m pip install -r requirements-dev.txt PYTHONPATH="$tmp_dir":$PYTHONPATH BUILD_MONAI=1 python ./tests/runner.py -p 'test_((?!integration).)' # unit tests if pgrep python; then pkill python; fi @@ -238,8 +237,8 @@ jobs: id: monai-install run: | which python - python -m pip install --upgrade pip wheel - python -m pip install -r requirements-dev.txt + python -m pip install --upgrade pip wheel tomli + python monai/config/print_dependencies.py build-system | xargs -d '\n' pip install --no-cache-dir --no-build-isolation BUILD_MONAI=1 python setup.py develop # install monai nvidia-smi export CUDA_VISIBLE_DEVICES=$(python -m tests.utils | tail -n 1) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bef17d09362..921d8f5c89a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -69,8 +69,6 @@ jobs: cat _version.py mv _version.py monai/ - # build "latest": remove flake package as it is not needed on hub.docker.com - sed -i '/flake/d' requirements-dev.txt docker build -t projectmonai/monai:latest -f Dockerfile . # distribute as always w/ tag "latest" to hub.docker.com diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 785945de036..d3c1e473c72 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -37,8 +37,8 @@ jobs: pip uninstall -y monai pip uninstall -y monai-weekly pip uninstall -y monai-weekly - python -m pip install --upgrade torch torchvision torchaudio torchtext - python -m pip install -r requirements-dev.txt + python monai/config/print_dependencies.py build-system | xargs -d '\n' pip install --no-cache-dir --no-build-isolation + python -m pip install --no-build-isolation .[all,testing] rm -rf /github/home/.cache/torch/hub/mmars/ - name: Clean directory run: | @@ -113,8 +113,7 @@ jobs: pip uninstall -y monai pip uninstall -y monai-weekly pip uninstall -y monai-weekly - python -m pip install --upgrade torch torchvision torchaudio torchtext - python -m pip install -r requirements-dev.txt + python -m pip install .[all,testing] rm -rf /github/home/.cache/torch/hub/mmars/ - name: Clean directory run: | @@ -124,7 +123,7 @@ jobs: nvidia-smi export CUDA_VISIBLE_DEVICES=$(python -m tests.utils -c 1 | tail -n 1) echo $CUDA_VISIBLE_DEVICES - python -c "import torch; print(torch.__version__); print('{} of GPUs available'.format(torch.cuda.device_count()))" + python -c "import torch; print(torch.__version__); print(f'{torch.cuda.device_count()} of GPUs available')" python -c 'import torch; print(torch.rand(5,3, device=torch.device("cuda:0")))' - name: Auto3dseg latest algo diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml index bfc2f62e28c..8378d2742ab 100644 --- a/.github/workflows/pythonapp-gpu.yml +++ b/.github/workflows/pythonapp-gpu.yml @@ -96,7 +96,7 @@ jobs: rm -rf $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")/ruamel* rm -rf $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")/llvmlite* #6377 python -m pip install ${{ matrix.pytorch }} - python -m pip install -r requirements-dev.txt + python -m pip install .[all,testing] python -m pip list - name: Run quick tests (GPU) if: github.event.pull_request.merged != true diff --git a/.github/workflows/pythonapp-hyena-gpu.yml b/.github/workflows/pythonapp-hyena-gpu.yml index af49cb292bc..b080942bf3c 100644 --- a/.github/workflows/pythonapp-hyena-gpu.yml +++ b/.github/workflows/pythonapp-hyena-gpu.yml @@ -47,8 +47,7 @@ jobs: run: | python -m pip install --upgrade pip wheel python -c "import sys; assert sys.version_info >= (3, 10), f'Python >= 3.10 required for nvsubquadratic, got {sys.version}'" - python -m pip install -r requirements-dev.txt - python -m pip install -e . + python -m pip install -e .[all,testing] # Install nvsubquadratic with --no-deps: the default torch_fft path needs only # torch + einops + omegaconf, and nvsubquadratic pins torch>=2.10,<2.11 which can # clash with the container's torch. To exercise the accelerated fused CUDA diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 056b99edffe..d4c55e1f08b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,10 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Install setuptools + - name: Install Dependencies run: | - python -m pip install --user --upgrade setuptools wheel packaging + python -m pip install --user --upgrade setuptools wheel packaging tomli + python monai/config/print_dependencies.py build-system all testing | xargs -d '\n' pip install --no-build-isolation - name: Build and test source archive and wheel file run: | find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \; @@ -55,11 +56,10 @@ jobs: # clean up cd "$root_dir" rm -r "$tmp_dir" - rm -rf monai/ ls -al . - name: Quick test installed run: | - python -m pip install -r requirements-min.txt + python -m pip install -e .[testing] python -m tests.min_tests env: QUICKTEST: True @@ -105,7 +105,8 @@ jobs: run: | find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \; git describe - python -m pip install --user --upgrade setuptools wheel packaging + python -m pip install --user --upgrade setuptools wheel packaging tomli + python monai/config/print_dependencies.py build-system all testing | xargs -d '\n' pip install --no-build-isolation python setup.py build cat build/lib/monai/_version.py - name: Upload version @@ -159,8 +160,6 @@ jobs: echo "unmatched version string, please check the tagging branch." exit 1 fi - # remove flake package as it is not needed on hub.docker.com - sed -i '/flake/d' requirements-dev.txt docker build -t projectmonai/monai:"$RELEASE_VERSION" -f Dockerfile . # distribute with a tag to hub.docker.com echo "${{ secrets.DOCKER_PW }}" | docker login -u projectmonai --password-stdin diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml index 5d92ad2081d..d7bf1dc6dd0 100644 --- a/.github/workflows/setupapp.yml +++ b/.github/workflows/setupapp.yml @@ -163,9 +163,8 @@ jobs: if: github.ref == 'refs/heads/dev' run: | cd $GITHUB_WORKSPACE - rm -rf monai/ ls -al . - python -m pip install -r requirements-min.txt + python -m pip install -e .[testing] python -m tests.min_tests env: QUICKTEST: True diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml index 7e7349ec931..70e190b4bf9 100644 --- a/.github/workflows/weekly-preview.yml +++ b/.github/workflows/weekly-preview.yml @@ -32,8 +32,8 @@ jobs: cache: 'pip' - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - python -m pip install --no-build-isolation -r requirements-dev.txt + python -m pip install -U pip wheel + python -m pip install .[all,testing] - name: Lint and type check run: | # clean up temporary files @@ -59,12 +59,12 @@ jobs: - name: Build distribution run: | export HEAD_COMMIT_ID=$(git rev-parse HEAD) - sed -i 's/name\ =\ monai$/name\ =\ monai-weekly/g' setup.cfg + sed -i 's/name\ =\ "monai"$/name\ =\ "monai-weekly"/g' pyproject.toml echo "__commit_id__ = \"$HEAD_COMMIT_ID\"" >> monai/__init__.py - git diff setup.cfg monai/__init__.py + git diff pyproject.toml monai/__init__.py git config user.name "CI Builder" git config user.email "monai.contact@gmail.com" - git add setup.cfg monai/__init__.py + git add pyproject.toml monai/__init__.py git commit -m "Weekly build at $HEAD_COMMIT_ID" export YEAR_WEEK=$(date +'%y%U') echo "Year week for tag is ${YEAR_WEEK}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50f496af3ff..d19a01670a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,10 +57,6 @@ Before submitting a pull request, we recommend that all linting should pass, by ```bash # optionally update the dependencies and dev tools python -m pip install -U pip -python -m pip install -U -r requirements-dev.txt - -# run the linting and type checking tools -./runtests.sh --codeformat # try to fix the coding style errors automatically ./runtests.sh --autofix @@ -132,7 +128,7 @@ It is recommended that the new test `test_[module_name].py` is constructed by us python 3.9+ build-in functions, `torch`, `numpy`, `coverage` (for reporting code coverages) and `parameterized` (for organising test cases) packages. If it requires any other external packages, please make sure: -- the packages are listed in [`requirements-dev.txt`](requirements-dev.txt) +- the packages are listed in [`pyproject.toml`](pyproject.toml) - the new test `test_[module_name].py` is added to the `exclude_cases` in [`./tests/min_tests.py`](./tests/min_tests.py) so that the minimal CI runner will not execute it. @@ -219,10 +215,8 @@ Integration tests with minimal requirements are deployed to ensure this strategy To add new optional dependencies, please communicate with the core team during pull request reviews, and add the necessary information (at least) to the following files: -- [setup.cfg](https://github.com/Project-MONAI/MONAI/blob/dev/setup.cfg) (for package's `[options.extras_require]` config) -- [requirements-dev.txt](https://github.com/Project-MONAI/MONAI/blob/dev/requirements-dev.txt) (pip requirements file) +- [pyproject.toml](https://github.com/Project-MONAI/MONAI/blob/dev/pyproject.toml) (for package's `[project.optional-dependencies]` config) - [docs/requirements.txt](https://github.com/Project-MONAI/MONAI/blob/dev/docs/requirements.txt) (docs pip requirements file) -- [environment-dev.yml](https://github.com/Project-MONAI/MONAI/blob/dev/environment-dev.yml) (conda environment file) - [installation.md](https://github.com/Project-MONAI/MONAI/blob/dev/docs/source/installation.md) (documentation) When writing unit tests that use 3rd-party packages, it is a good practice to always consider @@ -437,7 +431,7 @@ Do **not** use `[skip ci]` for commits that change: - Source code in `monai/` - Test files in `tests/` -- Dependencies (`requirements*.txt`, `setup.cfg`, `setup.py`) +- Dependencies (`pyproject.toml`, `setup.py`, `docs/requirements.txt`) - Anything that could affect correctness or compatibility ### Quick example diff --git a/Dockerfile b/Dockerfile index 3240da7deda..307fc988e07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,8 @@ FROM ${PYTORCH_IMAGE} LABEL maintainer="monai.contact@gmail.com" +ENV BUILD_MONAI=1 + # TODO: remark for issue [revise the dockerfile](https://github.com/zarr-developers/numcodecs/issues/431) RUN if [[ $(uname -m) =~ "aarch64" ]]; then \ export CFLAGS="-O3" && \ @@ -24,36 +26,6 @@ RUN if [[ $(uname -m) =~ "aarch64" ]]; then \ pip install numcodecs; \ fi -WORKDIR /opt/monai - -# Patch NVIDIA's pip constraint file: -# - keep the base image's numpy pin if present (older images pin numpy==1.26.4 as -# their torch was compiled against NumPy 1.x; newer images may ship an empty file) -# - add setuptools<71 (setuptools>=71 removed pkg_resources, breaking MetricsReloaded) -# - pin urllib3>=2 to prevent inadvertent downgrades by pip-installing legacy packages -RUN (grep '^numpy' /etc/pip/constraint.txt || true) > /tmp/new_constraints.txt \ - && printf 'setuptools<71\nurllib3>=2\n' >> /tmp/new_constraints.txt \ - && cp /tmp/new_constraints.txt /etc/pip/constraint.txt - -# install full deps -COPY requirements.txt requirements-min.txt requirements-dev.txt /tmp/ -RUN cp /tmp/requirements.txt /tmp/req.bak \ - && awk '!/torch/' /tmp/requirements.txt > /tmp/tmp && mv /tmp/tmp /tmp/requirements.txt \ - && python -m pip install --upgrade --no-cache-dir --no-build-isolation pip wheel wheel-stub \ - && python -m pip install --no-cache-dir --no-build-isolation -r /tmp/requirements-dev.txt - -# compile ext and remove temp files -# TODO: remark for issue [revise the dockerfile #1276](https://github.com/Project-MONAI/MONAI/issues/1276) -# please specify exact files and folders to be copied -- else, basically always, the Docker build process cannot cache -# this or anything below it and always will build from at most here; one file change leads to no caching from here on... - -COPY LICENSE CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md versioneer.py setup.py setup.cfg runtests.sh MANIFEST.in ./ -COPY tests ./tests -COPY monai ./monai - -RUN BUILD_MONAI=1 FORCE_CUDA=1 python setup.py develop \ - && rm -rf build __pycache__ - # NGC Client WORKDIR /opt/tools ARG NGC_CLI_URI="https://ngc.nvidia.com/downloads/ngccli_linux.zip" @@ -68,5 +40,16 @@ RUN apt-get update \ ENV PATH=${PATH}:/opt/tools ENV POLYGRAPHY_AUTOINSTALL_DEPS=1 - WORKDIR /opt/monai + +# TODO: remark for issue [revise the dockerfile #1276](https://github.com/Project-MONAI/MONAI/issues/1276) +# please specify exact files and folders to be copied -- else, basically always, the Docker build process cannot cache +# this or anything below it and always will build from at most here; one file change leads to no caching from here on... +COPY LICENSE CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md versioneer.py setup.py pyproject.toml runtests.sh MANIFEST.in ./ +COPY tests ./tests +COPY monai ./monai + +# Need to install build requirements explicitly so that no-build-isolation can be used. This needed to make pip build +# against the included version of PyTorch, rather than install a new version in the isolated environment. +RUN python monai/config/print_dependencies.py build-system | xargs -d '\n' pip install --no-cache-dir --no-build-isolation \ + && FORCE_CUDA=1 pip install --no-cache-dir --no-build-isolation -e .[all,testing] diff --git a/Dockerfile.slim b/Dockerfile.slim index 1b3cf3bcd4c..bfaf897967f 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -28,9 +28,9 @@ RUN apt update && apt upgrade -y && \ wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb && \ dpkg -i cuda-keyring_1.1-1_all.deb && \ apt update && \ - ${APT_INSTALL} cuda-toolkit-12-9 && \ + ${APT_INSTALL} cuda-toolkit-13-3 && \ rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* && \ - python -m pip install --upgrade --no-cache-dir --no-build-isolation pip + python -m pip install --upgrade --no-cache-dir pip # TODO: remark for issue [revise the dockerfile](https://github.com/zarr-developers/numcodecs/issues/431) RUN if [[ $(uname -m) =~ "aarch64" ]]; then \ @@ -46,18 +46,14 @@ RUN wget -q ${NGC_CLI_URI} && unzip ngccli_linux.zip && chmod u+x ngc-cli/ngc && WORKDIR /opt/monai -# copy relevant parts of repo -COPY requirements.txt requirements-min.txt requirements-dev.txt versioneer.py setup.py setup.cfg pyproject.toml ./ -COPY LICENSE CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md MANIFEST.in runtests.sh ./ +# TODO: remark for issue [revise the dockerfile #1276](https://github.com/Project-MONAI/MONAI/issues/1276) +# please specify exact files and folders to be copied -- else, basically always, the Docker build process cannot cache +# this or anything below it and always will build from at most here; one file change leads to no caching from here on... +COPY LICENSE CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md versioneer.py setup.py pyproject.toml runtests.sh MANIFEST.in ./ COPY tests ./tests COPY monai ./monai -# install full deps -RUN python -m pip install --no-cache-dir --no-build-isolation -U wheel wheel-stub -RUN python -m pip install --no-cache-dir --no-build-isolation "torch>=2.8.0,<2.11" -r requirements-dev.txt - -# compile ext -RUN CUDA_HOME=/usr/local/cuda FORCE_CUDA=1 USE_COMPILED=1 BUILD_MONAI=1 python setup.py develop +RUN BUILD_MONAI=1 FORCE_CUDA=1 pip install --no-cache-dir -e .[all,testing] # recreate the image without the installed CUDA packages then copy the installed MONAI and Python directories FROM ${IMAGE} AS build2 @@ -66,10 +62,9 @@ ENV DEBIAN_FRONTEND=noninteractive ENV APT_INSTALL="apt install -y --no-install-recommends" RUN apt update && apt upgrade -y && \ - ${APT_INSTALL} ca-certificates python3-pip python-is-python3 git libopenslide0 && \ + ${APT_INSTALL} ca-certificates python-is-python3 git libopenslide0 && \ apt clean && \ - rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* && \ - python -m pip install --upgrade --no-cache-dir --no-build-isolation pip + rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* COPY --from=build /opt/monai /opt/monai COPY --from=build /opt/tools /opt/tools diff --git a/README.md b/README.md index d0927ad8c3a..92e4ca7eba8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ MONAI works with the [currently supported versions of Python](https://devguide.p * Major releases of MONAI will have dependency versions stated for them. The current state of the `dev` branch in this repository is the unreleased development version of MONAI which typically will support current versions of dependencies and include updates and bug fixes to do so. * PyTorch support covers [the current version](https://github.com/pytorch/pytorch/releases) plus three previous minor versions. If compatibility issues with a PyTorch version and other dependencies arise, support for a version may be delayed until a major release. * Our support policy for other dependencies adheres for the most part to [SPEC0](https://scientific-python.org/specs/spec-0000), where dependency versions are supported where possible for up to two years. Discovered vulnerabilities or defects may require certain versions to be explicitly not supported. -* See the `requirements*.txt` files for dependency version information. +* See the `pyproject.toml` file for dependency version information. ## Installation diff --git a/docs/requirements.txt b/docs/requirements.txt index 3027d401646..9e023cec5eb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -33,6 +33,7 @@ pynrrd pydicom h5py nni==2.10.1; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine +filelock<3.12.0 optuna opencv-python-headless onnx>=1.13.0 diff --git a/docs/source/installation.md b/docs/source/installation.md index 2d9e2a7f0ed..6b68799d3e0 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -19,13 +19,13 @@ --- -MONAI's core functionality is written in Python 3 (>= 3.10) and only requires [Numpy](https://numpy.org/) and [Pytorch](https://pytorch.org/). +MONAI's core functionality is written in Python 3 (>= 3.10) and only requires [Numpy](https://numpy.org/) and [PyTorch](https://pytorch.org/). The package is currently distributed via Github as the primary source code repository, and the Python package index (PyPI). The pre-built Docker images are made available on DockerHub. To install optional features such as handling the NIfTI files using -[Nibabel](https://nipy.org/nibabel/), or building workflows using [Pytorch +[Nibabel](https://nipy.org/nibabel/), or building workflows using [PyTorch Ignite](https://pytorch.org/ignite/), please follow the instructions: - [Installing the recommended dependencies](#installing-the-recommended-dependencies) @@ -49,6 +49,26 @@ To install the [current milestone release](https://pypi.org/project/monai/): pip install monai ``` +MONAI supports the extras syntax such as `pip install 'monai[nibabel]'`. The options are + +```text +clearml, cucim, cupy, einops, fire, gdown, h5py, huggingface_hub, hyena, ignite, imagecodecs, itk, jsonschema, lmdb, lpips, matplotlib, metrics_reloaded, mlflow, nibabel, nni, onnx, openslide, optuna, pandas, pillow, polygraphy, psutil, pyamg, pybind11, pydicom, pynrrd, pynvml, pyyaml, requests, segment_anything, scipy, skimage, tensorboard, tensorboardX, tifffile, torchio, torchvision, tqdm, transformers, zarr +``` + +which correspond to the packages: `clearml`, `cucim` (`cucim-cu12` or `cucim-cu13`), `cupy-cuda13x`, `einops`, `fire`, `gdown`, `h5py`, `huggingface_hub`, `nvsubquadratic`, `omegaconf`, `pytorch-ignite`, `imagecodecs`, `itk`, `jsonschema`, `lmdb`, `lpips`, `matplotlib`, `MetricsReloaded`, `mlflow`, `nibabel`, `nni`, `filelock`, `onnx`, `onnxruntime`, `onnx_graphsurgeon`, `onnxscript`, `openslide-python`, `openslide-bin`, `optuna`, `pandas`, `pillow`, `polygraphy`, `psutil`, `pyamg`, `pybind11`, `pydicom`, `pynrrd`, `nvidia-ml-py`, `pyyaml`, `requests`, `segment_anything`, `scipy`, `scikit-image`, `tensorboard`, `tensorboardX`, `tifffile`, `torchio`, `torchvision`, `tqdm`, `transformers`, `zarr`. + +Almost all of these can be installed together with the `all` option. For development on MONAI, this should be accompanied by `testing` which will install the testing static checking packages. Cupy is omitted from `all` since the choice between +Cuda 12 and 13 versions of the library can't be resolved when installing and must be manually installed. + +The `hyena` extra pulls in [`nvsubquadratic`](https://github.com/NVIDIA-BioNeMo/nvSubquadratic), +required by `HyenaNDUNETR` / `HyenaMixer` / `HyenaTransformerBlock` (subquadratic +O(N log N) alternatives to windowed self-attention). Install with +`pip install 'monai[hyena]'`. + +The command `pip install 'monai[all,hyena,testing]'` installs almost all the optional dependencies. + +When installing MONAI, the compiled extensions are not compiled by default. Set the environment variable `BUILD_MONAI` to `1` when invoking `pip` to compile these, see below for details. + ### Weekly preview release To install the [weekly preview release](https://pypi.org/project/monai-weekly/): @@ -110,13 +130,19 @@ or, to build with MONAI C++/CUDA extensions: BUILD_MONAI=1 pip install git+https://github.com/Project-MONAI/MONAI ``` -To build the extensions, if the system environment already has a version of Pytorch installed, +To build the extensions, if the system environment already has a version of PyTorch installed, `--no-build-isolation` might be preferred: ```bash BUILD_MONAI=1 pip install --no-build-isolation git+https://github.com/Project-MONAI/MONAI ``` +When using build isolation (pip's default behaviour), a version of PyTorch must be installed which may not be the same as an existing install. This can cause the compiled libraries to be built against an ABI-incompatible PyTorch and thus not function at runtime. Building without isolation requires the current environment to have the necessary building libraries already installed. See the `build-system` section of `pyproject.toml` for these libraries, or use the following to install them: + +```bash +python monai/config/print_dependencies.py build-system | xargs -d '\n' pip install --no-build-isolation +``` + On Windows the inline `BUILD_MONAI=1 pip install ...` form is not supported by `cmd.exe` or PowerShell. Set the environment variable first, then run either install command shown above: @@ -263,12 +289,14 @@ cd MONAI/ pip install -e ".[all]" ``` -To install all optional dependencies with `pip` based on MONAI development environment settings: +To install all optional dependencies with `pip` based on MONAI development environment settings without installing +MONAI itself: ```bash git clone https://github.com/Project-MONAI/MONAI.git cd MONAI/ -pip install -r requirements-dev.txt +python monai/config/print_dependencies.py \* > requirements.txt +pip install -r requirements.txt ``` To install all optional dependencies with `conda` based on MONAI development environment settings (`environment-dev.yml`; @@ -280,21 +308,3 @@ cd MONAI/ conda create -n python= # eg 3.10 conda env update -n -f environment-dev.yml ``` - -Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is available via PyPI. - -- The options are - -``` -[nibabel, skimage, scipy, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, clearml, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema, ninja, pynrrd, pydicom, h5py, nni, optuna, onnx, onnxruntime, zarr, lpips, pynvml, huggingface_hub, hyena] -``` - -which correspond to `nibabel`, `scikit-image`,`scipy`, `pillow`, `tensorboard`, -`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `clearml`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `pyyaml`, `fire`, `jsonschema`, `ninja`, `pynrrd`, `pydicom`, `h5py`, `nni`, `optuna`, `onnx`, `onnxruntime`, `zarr`, `lpips`, `nvidia-ml-py`, `huggingface_hub`, `pyamg`, and `nvsubquadratic` respectively. - -The `hyena` extra pulls in [`nvsubquadratic`](https://github.com/NVIDIA-BioNeMo/nvSubquadratic), -required by `HyenaNDUNETR` / `HyenaMixer` / `HyenaTransformerBlock` (subquadratic -O(N log N) alternatives to windowed self-attention). Install with -`pip install 'monai[hyena]'`. - -- `pip install 'monai[all]'` installs all the optional dependencies. diff --git a/environment-dev.yml b/environment-dev.yml index b2457006c89..7d6d95a306e 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -1,15 +1,8 @@ name: monai channels: - - pytorch - defaults - - nvidia - - conda-forge dependencies: - - numpy>=1.24,<3.0 - - pytorch>=2.8.0 - - torchio - - torchvision - - pytorch-cuda>=11.6 + - python>=3.10 - pip - pip: - - -r requirements-dev.txt + - -e .[all,testing] diff --git a/monai/__init__.py b/monai/__init__.py index 45e15ddcd9a..2d6f5dfc37b 100644 --- a/monai/__init__.py +++ b/monai/__init__.py @@ -62,8 +62,8 @@ def filter(self, record): PY_REQUIRED_MINOR = 9 version_dict = get_versions() -__version__: str = version_dict.get("version", "0+unknown") -__revision_id__: str = version_dict.get("full-revisionid") +__version__: str = str(version_dict.get("version", "0+unknown")) +__revision_id__: str = str(version_dict.get("full-revisionid") or "") del get_versions, version_dict __copyright__ = "(c) MONAI Consortium" diff --git a/monai/_version.py b/monai/_version.py index f2342271048..d14412be66c 100644 --- a/monai/_version.py +++ b/monai/_version.py @@ -5,8 +5,9 @@ # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.23 (https://github.com/python-versioneer/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -15,11 +16,11 @@ import re import subprocess import sys -from collections.abc import Callable +from typing import Any, Callable, Dict, List, Optional, Tuple import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -35,8 +36,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool -def get_config(): + +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -54,13 +62,13 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY: dict[str, str] = {} -HANDLERS: dict[str, dict[str, Callable]] = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -69,13 +77,19 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) process = None - popen_kwargs = {} + popen_kwargs: Dict[str, Any] = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() @@ -91,8 +105,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs) break - except OSError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -112,7 +125,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -137,15 +154,15 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - with open(versionfile_abs) as fobj: + with open(versionfile_abs, "r") as fobj: for line in fobj: if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) @@ -165,7 +182,11 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") @@ -229,7 +250,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -248,7 +274,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -259,7 +285,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): describe_out, rc = runner(GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -269,7 +295,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None @@ -361,14 +387,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -393,7 +419,7 @@ def render_pep440(pieces): return rendered -def render_pep440_branch(pieces): +def render_pep440_branch(pieces: Dict[str, Any]) -> str: """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards @@ -423,7 +449,7 @@ def render_pep440_branch(pieces): return rendered -def pep440_split_post(ver): +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the @@ -433,7 +459,7 @@ def pep440_split_post(ver): return vc[0], int(vc[1] or 0) if len(vc) == 2 else None -def render_pep440_pre(pieces): +def render_pep440_pre(pieces: Dict[str, Any]) -> str: """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: @@ -457,7 +483,7 @@ def render_pep440_pre(pieces): return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -484,7 +510,7 @@ def render_pep440_post(pieces): return rendered -def render_pep440_post_branch(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. @@ -513,7 +539,7 @@ def render_pep440_post_branch(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. @@ -535,7 +561,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -555,7 +581,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -575,7 +601,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -611,7 +637,7 @@ def render(pieces, style): "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some diff --git a/monai/config/print_dependencies.py b/monai/config/print_dependencies.py new file mode 100644 index 00000000000..ab656f61ded --- /dev/null +++ b/monai/config/print_dependencies.py @@ -0,0 +1,73 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This program prints the MONAI dependencies for the optional names given on the command line. The printed values can +be piped to a requirements file to work with pip. All required dependencies are always printed, those for builing are +included in "build-system" is given as an argument, and all optional requirements are included if "*" is given. This +assumes the pyproject.toml file is in the current working directory. +""" + +from __future__ import annotations + +import sys +from collections.abc import Collection + +BUILD_SYSTEM_KEY = "build-system" +PROJ_KEY = "project" +OPTS_KEY = "optional-dependencies" +DEP_KEY = "dependencies" +REQ_KEY = "requires" +TOML_FILE = "pyproject.toml" + + +def parse_dependencies(filename: str = TOML_FILE, sections: Collection[str] | None = None) -> list[str]: + """ + Parse the toml file given by `filename` and return the dependency sections selected by `sections`. + """ + # these imports should be here to avoid attempting to import when MONAI is imported and both packages are missing + # isort: off + if sys.version_info.minor >= 11: + import tomllib + + load_func = tomllib.loads + else: + import tomli + + load_func = tomli.loads + # isort: on + + with open(filename) as o: + data = load_func(o.read()) + + proj = data[PROJ_KEY] + opts = proj[OPTS_KEY] + dependencies = list(proj[DEP_KEY]) + sections = set(sections or []) + + if BUILD_SYSTEM_KEY in sections: + sections.remove(BUILD_SYSTEM_KEY) + dependencies += data[BUILD_SYSTEM_KEY][REQ_KEY] + + if "*" in sections: + dependencies += sum(opts.values(), []) + else: + for s in sections: + dependencies += opts[s] + + return sorted(set(dependencies)) + + +if __name__ == "__main__": + dependencies = parse_dependencies(sections=set(sys.argv[1:])) + + for d in dependencies: + print(d) diff --git a/pyproject.toml b/pyproject.toml index 325622b66a7..35e1ff0362d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,266 @@ + [build-system] requires = [ + "setuptools<71.0.0", # pkg_resources removed in setuptools>=71; needed by MetricsReloaded setup.py "wheel", - "setuptools", + "versioneer[toml]", "more-itertools>=8.0", + "ninja", + "packaging", "torch>=2.8.0", + "numpy>=1.24,<3.0", + "backports.tarfile" # see https://github.com/Project-MONAI/MONAI/issues/8791 +] +build-backend = "setuptools.build_meta" + +[project] +name = "monai" +description = "AI Toolkit for Healthcare Imaging" +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.10" +license = { text = "Apache License 2.0" } +authors = [{ name = "MONAI Consortium", email = "monai.contact@gmail.com" }] +classifiers = [ + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "Intended Audience :: Healthcare Industry", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Medical Science Apps.", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Typing :: Typed", +] +dependencies = [ + "torch>=2.8.0", + "numpy>=1.24,<3.0", + "setuptools<71.0.0" # used here to match requirement in build-system and prevent different PyTorch installations +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://project-monai.github.io/" +Documentation = "https://monai.readthedocs.io/" +"Bug Tracker" = "https://github.com/Project-MONAI/MONAI/issues" +"Source Code" = "https://github.com/Project-MONAI/MONAI" + +[project.optional-dependencies] +# All dependencies are included here except some omitted for compatibility. Testing dependencies are typically not +# needed and so present only in "testing". Ensure requirement changes in other lists are reflected here as well. +all = [ + "clearml>=1.10.0rc0", + "cucim-cu12; platform_system == 'Linux' and python_version <= '3.10'", + "cucim-cu13; platform_system == 'Linux' and python_version >= '3.11'", + "einops", + "filelock<3.12.0", + "fire", + "gdown>=4.7.3", + "h5py", + "huggingface_hub", + "imagecodecs; platform_system == 'Linux' or platform_system == 'Darwin'", + "itk>=5.2", + "jsonschema", + "lmdb", + "lpips==0.1.4", + "matplotlib>=3.6.3", + "MetricsReloaded @ git+https://github.com/Project-MONAI/MetricsReloaded@monai-support", + "mlflow>=2.12.2,<3.13", + "nibabel", "ninja", - "packaging" + "nni; platform_system == 'Linux' and 'arm' not in platform_machine and 'aarch' not in platform_machine", + "nvidia-ml-py", + "onnx_graphsurgeon", + "onnx>=1.13.0", + "onnxruntime; python_version <= '3.10'", + "onnxscript", + "openslide-bin", + "openslide-python", + "optuna", + "pandas", + "pillow!=8.3.0", + "polygraphy", + "psutil", + "pyamg>=5.0.0,<5.3.0", + "pybind11", + "pydicom", + "pynrrd", + "pytorch-ignite", + "pyyaml", + "requests", + "segment_anything @ git+https://github.com/facebookresearch/segment-anything.git@6fdee8f2727f4506cfbbe553e23b895e27956588", + "scikit-image>=0.19.0", + "scipy>=1.12.0", + "tensorboard>=2.12.0", + "tensorboardX", + "tifffile; platform_system == 'Linux' or platform_system == 'Darwin'", + "torchio", + "torchvision", + "tqdm>=4.47.0", + "transformers>=4.53.0, <5.0", + "zarr" +] +clearml = ["clearml>=1.10.0rc0"] +cucim = [ + "cucim-cu12; platform_system == 'Linux' and python_version <= '3.10'", + "cucim-cu13; platform_system == 'Linux' and python_version >= '3.11'" ] +cupy = ["cupy-cuda13x!=14.1.0"] # not in all, the choice between cuda12x and cuda13x that can't be resolved here +einops = ["einops"] +fire = ["fire"] +gdown = ["gdown>=4.7.3"] +h5py = ["h5py"] +huggingface_hub = ["huggingface_hub"] +hyena = ["nvsubquadratic>=0.1.1", "omegaconf", "einops"] # omitted from all for compatibility +ignite = ["pytorch-ignite"] +imagecodecs = ["imagecodecs; platform_system == 'Linux' or platform_system == 'Darwin'"] +itk = ["itk>=5.2"] +jsonschema = ["jsonschema"] +lmdb = ["lmdb"] +lpips = ["lpips==0.1.4"] +matplotlib = ["matplotlib>=3.6.3"] +metrics_reloaded = ["MetricsReloaded @ git+https://github.com/Project-MONAI/MetricsReloaded@monai-support"] +mlflow = ["mlflow>=2.12.2,<3.13"] +nibabel = ["nibabel"] +nni = [ + "nni; platform_system == 'Linux' and 'arm' not in platform_machine and 'aarch' not in platform_machine", + "filelock<3.12.0" # https://github.com/microsoft/nni/issues/5523 +] +onnx = ["onnx>=1.13.0", "onnxruntime; python_version <= '3.10'", "onnx_graphsurgeon", "onnxscript"] +openslide = ["openslide-python", "openslide-bin"] +optuna = ["optuna"] +pandas = ["pandas"] +pillow = ["pillow!=8.3.0"] # https://github.com/python-pillow/Pillow/issues/5571 +polygraphy = ["polygraphy"] +psutil = ["psutil"] +pyamg = ["pyamg>=5.0.0,<5.3.0"] +pybind11 = ["pybind11"] +pydicom = ["pydicom"] +pynrrd = ["pynrrd"] +pynvml = ["nvidia-ml-py"] +pyyaml = ["pyyaml"] +requests = ["requests"] +segment_anything = ["segment_anything @ git+https://github.com/facebookresearch/segment-anything.git@6fdee8f2727f4506cfbbe553e23b895e27956588"] +scipy = ["scipy>=1.12.0"] +skimage = ["scikit-image>=0.19.0"] +tensorboard = ["tensorboard>=2.12.0"] # https://github.com/Project-MONAI/MONAI/issues/7434 +tensorboardX = ["tensorboardX"] +tifffile = ["tifffile; platform_system == 'Linux' or platform_system == 'Darwin'"] +torchio = ["torchio"] +torchvision = ["torchvision"] +tqdm = ["tqdm>=4.47.0"] +transformers = ["transformers>=4.53.0, <5.0"] # 5.x references torch.float8_e8m0fnu absent in older PyTorch builds +zarr = ["zarr"] +# these dependencies are for testing/building only, they aren't needed for regular use so don't appear in "all" +testing = [ + "black>=26.3.1", + "coverage>=5.5", + "isort>=5.1,<6,!=6.0.0", + "mccabe", + "mypy>=1.5.0,<1.12.0", + "packaging", + "parameterized", + "pep8-naming", + "pre-commit", + "pycodestyle", + "pyflakes", + "ruff>=0.14.11,<0.15", + "tomli", # used in print_dependencies.py for Python<3.11 + "typeguard<3", # https://github.com/microsoft/nni/issues/5457 + "types-PyYAML", + "types-setuptools" +] + +[tool.setuptools] +license-files = ["LICENSE"] + +[tool.setuptools.dynamic] +version = {attr = "monai.__version__"} + +[tool.versioneer] +VCS = "git" +style = "pep440" +versionfile_source = "monai/_version.py" +versionfile_build = "monai/_version.py" +tag_prefix = "" +parentdir_prefix = "" + +[tool.isort] +known_first_party = ["monai"] +profile = "black" +line_length = 120 +skip = [".git", ".eggs", "venv", ".venv", "versioneer.py", "_version.py", "conf.py", "monai/__init__.py"] +skip_glob = ["*.pyi"] +add_imports = ["from __future__ import annotations"] +append_only = true + +[tool.mypy] +ignore_missing_imports = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = false +warn_return_any = true +strict_equality = true +show_column_numbers = true +show_error_codes = true +pretty = false +warn_unused_configs = true +extra_checks = true +exclude = ["venv/"] + +[[tool.mypy.overrides]] +module = ["versioneer", "monai._version", "monai.eggs"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["monai.*"] +check_untyped_defs = true +disallow_untyped_decorators = true + +[[tool.mypy.overrides]] +module = [ + "monai._extensions.*", + "monai.apps.*", + "monai.auto3dseg.*", + "monai.bundle.*", + "monai.config.*", + "monai.engines.*", + "monai.fl.*", + "monai.handlers.*", + "monai.inferers.*", + "monai.losses.*", + "monai.metrics.*", + "monai.optimizers.*", + "monai.utils.*", + "monai.visualize.*" +] +disallow_incomplete_defs = true + +[tool.coverage.run] +concurrency = ["multiprocessing"] +source = ["."] +data_file = ".coverage/.coverage" +omit = ["setup.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] +show_missing = true +skip_covered = true + +[tool.coverage.xml] +output = "coverage.xml" [tool.black] line-length = 120 diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index b2c36f8de62..00000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,66 +0,0 @@ -# Full requirements for developments --r requirements-min.txt -pytorch-ignite -gdown>=4.7.3 -scipy>=1.12.0 -itk>=5.2 -nibabel -pillow!=8.3.0 # https://github.com/python-pillow/Pillow/issues/5571 -tensorboard>=2.12.0 # https://github.com/Project-MONAI/MONAI/issues/7434 -scikit-image>=0.19.0 -tqdm>=4.47.0 -lmdb -mccabe -pep8-naming -pycodestyle -pyflakes -black>=26.3.1 -isort>=5.1, <6, !=6.0.0 -ruff>=0.14.11,<0.15 -pybind11 -setuptools<71 # pkg_resources removed in setuptools>=71; needed by MetricsReloaded setup.py -types-setuptools -mypy>=1.5.0, <1.12.0 -ninja -torchio -torchvision -psutil -cucim-cu12; platform_system == "Linux" and python_version <= "3.10" -cucim-cu13; platform_system == "Linux" and python_version >= '3.11' -openslide-python -openslide-bin -imagecodecs; platform_system == "Linux" or platform_system == "Darwin" -tifffile; platform_system == "Linux" or platform_system == "Darwin" -pandas -requests -einops -transformers>=4.53.0, <5.0 # 5.x references torch.float8_e8m0fnu absent in older PyTorch builds -mlflow>=2.12.2, <3.0 # 3.x broken on Python 3.12 (relative import in mlflow.utils.uv_utils) -clearml>=1.10.0rc0 -matplotlib>=3.6.3 -tensorboardX -types-PyYAML -pyyaml -fire -jsonschema -pynrrd -pre-commit -pydicom -h5py -nni==2.10.1; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine -optuna -git+https://github.com/Project-MONAI/MetricsReloaded@monai-support#egg=MetricsReloaded -onnx>=1.13.0 -onnxscript -onnxruntime -typeguard<3 # https://github.com/microsoft/nni/issues/5457 -filelock<3.12.0 # https://github.com/microsoft/nni/issues/5523 -zarr -lpips==0.1.4 -nvidia-ml-py -huggingface_hub -pyamg>=5.0.0, <5.3.0 -git+https://github.com/facebookresearch/segment-anything.git@6fdee8f2727f4506cfbbe553e23b895e27956588 -onnx_graphsurgeon -polygraphy -pytest # FIXME: added to get around cupy 14.1.0 creating the requirement through polygraphy and trt_compiler somehow diff --git a/requirements-min.txt b/requirements-min.txt deleted file mode 100644 index ddda9064a6b..00000000000 --- a/requirements-min.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Requirements for minimal tests --r requirements.txt -setuptools>=50.3.0,<66.0.0,!=60.6.0 ; python_version < "3.12" -setuptools>=70.2.0,<=79.0.1; python_version >= "3.12" -coverage>=5.5 -parameterized -packaging -backports.tarfile diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7d283182a40..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -torch>=2.8.0 -numpy>=1.24,<3.0 diff --git a/runtests.sh b/runtests.sh index 431e9298c35..0cb8907b6ad 100755 --- a/runtests.sh +++ b/runtests.sh @@ -137,8 +137,14 @@ function print_version { } function install_deps { - echo "Pip installing MONAI development dependencies and compile MONAI cpp extensions..." - ${cmdPrefix}"${PY_EXE}" -m pip install --no-build-isolation -r requirements-dev.txt + echo "Pip installing MONAI development dependencies..." + # needed for Python<3.11 + ${cmdPrefix}"${PY_EXE}" -m pip install -U tomli + # create a temporary requirements file and install using it + REQ=$(mktemp --tmpdir XXX.txt) + trap 'rm -f -- "$REQ"' EXIT + ${cmdPrefix}"${PY_EXE}" monai/config/print_dependencies.py all testing > "$REQ" + ${cmdPrefix}"${PY_EXE}" -m pip install -r "$REQ" } function compile_cpp { diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c025c685f4d..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,271 +0,0 @@ -[metadata] -name = monai -author = MONAI Consortium -author_email = monai.contact@gmail.com -url = https://project-monai.github.io/ -description = AI Toolkit for Healthcare Imaging -long_description = file:README.md -long_description_content_type = text/markdown; charset=UTF-8 -platforms = OS Independent -license = Apache License 2.0 -license_files = - LICENSE -project_urls = - Documentation=https://monai.readthedocs.io/ - Bug Tracker=https://github.com/Project-MONAI/MONAI/issues - Source Code=https://github.com/Project-MONAI/MONAI -classifiers = - Intended Audience :: Developers - Intended Audience :: Education - Intended Audience :: Science/Research - Intended Audience :: Healthcare Industry - Programming Language :: C++ - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - Topic :: Scientific/Engineering - Topic :: Scientific/Engineering :: Artificial Intelligence - Topic :: Scientific/Engineering :: Medical Science Apps. - Topic :: Scientific/Engineering :: Information Analysis - Topic :: Software Development - Topic :: Software Development :: Libraries - Typing :: Typed - -[options] -python_requires = >= 3.10 -# for compiling and develop setup only -# no need to specify the versions so that we could -# compile for multiple targeted versions. -setup_requires = - torch - ninja - packaging -install_requires = - torch>=2.8.0 - numpy>=1.24,<3.0 - -[options.extras_require] -all = - nibabel - ninja - scikit-image>=0.14.2 - scipy>=1.12.0 - pillow - tensorboard - gdown>=4.7.3 - pytorch-ignite==0.4.11 - torchio - torchvision - itk>=5.2 - tqdm>=4.47.0 - lmdb - psutil - cucim-cu12; platform_system == "Linux" and python_version <= '3.10' - cucim-cu13; platform_system == "Linux" and python_version >= '3.11' - openslide-python - openslide-bin - tifffile; platform_system == "Linux" or platform_system == "Darwin" - imagecodecs; platform_system == "Linux" or platform_system == "Darwin" - pandas - einops - transformers>=4.53.0 - mlflow>=2.12.2,<3.13 - clearml>=1.10.0rc0 - matplotlib>=3.6.3 - tensorboardX - pyyaml - fire - jsonschema - pynrrd - pydicom - h5py - nni; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine - optuna - onnx>=1.13.0 - onnxruntime - zarr - lpips==0.1.4 - nvidia-ml-py - huggingface_hub - pyamg>=5.0.0, <5.3.0 -nibabel = - nibabel -ninja = - ninja -skimage = - scikit-image>=0.14.2 -scipy = - scipy>=1.12.0 -pillow = - pillow!=8.3.0 -tensorboard = - tensorboard -gdown = - gdown>=4.7.3 -ignite = - pytorch-ignite==0.4.11 -torchio = - torchio -torchvision = - torchvision -itk = - itk>=5.2 -tqdm = - tqdm>=4.47.0 -lmdb = - lmdb -psutil = - psutil -cucim = - cucim-cu12; platform_system == "Linux" and python_version <= '3.10' - cucim-cu13; platform_system == "Linux" and python_version >= '3.11' -openslide = - openslide-python - openslide-bin -tifffile = - tifffile; platform_system == "Linux" or platform_system == "Darwin" -imagecodecs = - imagecodecs; platform_system == "Linux" or platform_system == "Darwin" -pandas = - pandas -einops = - einops -hyena = - nvsubquadratic>=0.1.1 -transformers = - transformers>=4.36.0, <4.41.0; python_version <= '3.10' -mlflow = - mlflow>=2.12.2,<3.13 -matplotlib = - matplotlib>=3.6.3 -clearml = - clearml -tensorboardX = - tensorboardX -pyyaml = - pyyaml -fire = - fire -packaging = - packaging -jsonschema = - jsonschema -pynrrd = - pynrrd -pydicom = - pydicom -h5py = - h5py -nni = - nni; platform_system == "Linux" and "arm" not in platform_machine and "aarch" not in platform_machine -optuna = - optuna -onnx = - onnx>=1.13.0 - onnxruntime; python_version <= '3.10' -zarr = - zarr -lpips = - lpips==0.1.4 -pynvml = - nvidia-ml-py -polygraphy = - polygraphy - -# # workaround https://github.com/Project-MONAI/MONAI/issues/5882 -# MetricsReloaded = - # MetricsReloaded @ git+https://github.com/Project-MONAI/MetricsReloaded@monai-support#egg=MetricsReloaded -huggingface_hub = - huggingface_hub -pyamg = - pyamg>=5.0.0, <5.3.0 -# segment-anything = -# segment-anything @ git+https://github.com/facebookresearch/segment-anything@6fdee8f2727f4506cfbbe553e23b895e27956588#egg=segment-anything - -[isort] -known_first_party = monai -profile = black -line_length = 120 -skip = .git, .eggs, venv, .venv, versioneer.py, _version.py, conf.py, monai/__init__.py -skip_glob = *.pyi -add_imports = from __future__ import annotations -append_only = true - -[versioneer] -VCS = git -style = pep440 -versionfile_source = monai/_version.py -versionfile_build = monai/_version.py -tag_prefix = -parentdir_prefix = - -[mypy] -# Suppresses error messages about imports that cannot be resolved. -ignore_missing_imports = True -# Changes the treatment of arguments with a default value of None by not implicitly making their type Optional. -no_implicit_optional = True -# Warns about casting an expression to its inferred type. -warn_redundant_casts = True -# No error on unneeded # type: ignore comments. -warn_unused_ignores = False -# Shows a warning when returning a value with type Any from a function declared with a non-Any return type. -warn_return_any = True -# Prohibit equality checks, identity checks, and container checks between non-overlapping types. -strict_equality = True -# Shows column numbers in error messages. -show_column_numbers = True -# Shows error codes in error messages. -show_error_codes = True -# Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location markers. -pretty = False -# Warns about per-module sections in the config file that do not match any files processed when invoking mypy. -warn_unused_configs = True -# Make arguments prepended via Concatenate be truly positional-only. -extra_checks = True -# Allows variables to be redefined with an arbitrary type, -# as long as the redefinition is in the same block and nesting level as the original definition. -# allow_redefinition = True - -exclude = venv/ - -[mypy-versioneer] -# Ignores all non-fatal errors. -ignore_errors = True - -[mypy-monai._version] -# Ignores all non-fatal errors. -ignore_errors = True - -[mypy-monai.eggs] -# Ignores all non-fatal errors. -ignore_errors = True - -[mypy-monai.*] -# Also check the body of functions with no types in their type signature. -check_untyped_defs = True -# Warns about usage of untyped decorators. -disallow_untyped_decorators = True - -[mypy-monai.visualize.*,monai.utils.*,monai.optimizers.*,monai.losses.*,monai.inferers.*,monai.config.*,monai._extensions.*,monai.fl.*,monai.engines.*,monai.handlers.*,monai.auto3dseg.*,monai.bundle.*,monai.metrics.*,monai.apps.*] -disallow_incomplete_defs = True - -[coverage:run] -concurrency = multiprocessing -source = . -data_file = .coverage/.coverage -omit = setup.py - -[coverage:report] -exclude_lines = - pragma: no cover - if TYPE_CHECKING: - # Don't complain if tests don't hit code: - raise NotImplementedError - if __name__ == .__main__.: -show_missing = True -skip_covered = True - -[coverage:xml] -output = coverage.xml diff --git a/setup.py b/setup.py index 4d9badca415..2ebc7a9ba63 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ BUILD_CPP = BUILD_CUDA = False TORCH_VERSION = 0 + try: import torch @@ -126,7 +127,7 @@ def get_extensions(): ext_modules = [ extension( name="monai._C", - sources=sources, + sources=list(map(os.path.relpath, sources)), include_dirs=include_dirs, define_macros=define_macros, extra_compile_args=extra_compile_args, diff --git a/tests/min_tests.py b/tests/min_tests.py index f98bf4b7397..c0ab8c2325b 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -20,10 +20,10 @@ def run_testsuit(): """ Load test cases by excluding those need external dependencies. - The loaded cases should work with "requirements-min.txt":: + The loaded cases should work with testing requirements:: # in the monai repo folder: - pip install -r requirements-min.txt + pip install -e .[testing] QUICKTEST=true python -m tests.min_tests :return: a test suite diff --git a/versioneer.py b/versioneer.py index 5d0a606c91d..1e3753e63fb 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,4 +1,5 @@ -# Version: 0.23 + +# Version: 0.29 """The Versioneer - like a rocketeer, but for versions. @@ -8,12 +9,12 @@ * like a rocketeer, but for versions! * https://github.com/python-versioneer/python-versioneer * Brian Warner -* License: Public Domain (CC0-1.0) -* Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 * [![Latest Version][pypi-image]][pypi-url] * [![Build Status][travis-image]][travis-url] -This is a tool for managing a recorded version number in distutils/setuptools-based +This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control @@ -22,10 +23,38 @@ ## Quick Install +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + * `pip install versioneer` to somewhere in your $PATH -* add a `[versioneer]` section to your setup.cfg (see [Install](INSTALL.md)) -* run `versioneer install` in your source tree, commit the results -* Verify version information with `python setup.py version` + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` ## Version Identifiers @@ -230,9 +259,10 @@ To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files @@ -262,9 +292,8 @@ To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. [pypi-image]: https://img.shields.io/pypi/v/versioneer.svg [pypi-url]: https://pypi.python.org/pypi/versioneer/ @@ -273,7 +302,6 @@ [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer """ - # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error @@ -287,15 +315,34 @@ import re import subprocess import sys -from typing import Callable, Dict +from pathlib import Path +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import NoReturn import functools +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False + class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + versionfile_source: str + versionfile_build: Optional[str] + parentdir_prefix: Optional[str] + verbose: Optional[bool] + -def get_root(): +def get_root() -> str: """Get the project root directory. We require that all commands are run from the project root, i.e. the @@ -303,20 +350,28 @@ def get_root(): """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ( - "Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND')." - ) + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools @@ -328,38 +383,59 @@ def get_root(): my_path = os.path.realpath(os.path.abspath(__file__)) me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(my_path), versioneer_py)) + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(my_path), versioneer_py)) except NameError: pass return root -def get_config_from_root(root): +def get_config_from_root(root: str) -> VersioneerConfig: """Read the project setup.cfg file to determine Versioneer config.""" # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.ConfigParser() - with open(setup_cfg, "r") as cfg_file: - parser.read_file(cfg_file) - VCS = parser.get("versioneer", "VCS") # mandatory - - # Dict-like interface for non-mandatory entries - section = parser["versioneer"] + root_pth = Path(root) + pyproject_toml = root_pth / "pyproject.toml" + setup_cfg = root_pth / "setup.cfg" + section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + if pyproject_toml.exists() and have_tomllib: + try: + with open(pyproject_toml, 'rb') as fobj: + pp = tomllib.load(fobj) + section = pp['tool']['versioneer'] + except (tomllib.TOMLDecodeError, KeyError) as e: + print(f"Failed to load config from {pyproject_toml}: {e}") + print("Try to load it from setup.cfg") + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing + + section = parser["versioneer"] + + # `cast`` really shouldn't be used, but its simplest for the + # common VersioneerConfig users at the moment. We verify against + # `None` values elsewhere where it matters cfg = VersioneerConfig() - cfg.VCS = VCS + cfg.VCS = section['VCS'] cfg.style = section.get("style", "") - cfg.versionfile_source = section.get("versionfile_source") + cfg.versionfile_source = cast(str, section.get("versionfile_source")) cfg.versionfile_build = section.get("versionfile_build") - cfg.tag_prefix = section.get("tag_prefix") + cfg.tag_prefix = cast(str, section.get("tag_prefix")) if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" cfg.parentdir_prefix = section.get("parentdir_prefix") - cfg.verbose = section.get("verbose") + if isinstance(section, configparser.SectionProxy): + # Make sure configparser translates to bool + cfg.verbose = section.getboolean("verbose") + else: + cfg.verbose = section.get("verbose") + return cfg @@ -372,23 +448,28 @@ class NotThisMethod(Exception): HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" - - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" HANDLERS.setdefault(vcs, {})[method] = f return f - return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) process = None - popen_kwargs = {} + popen_kwargs: Dict[str, Any] = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() @@ -399,17 +480,12 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen( - [command] + args, - cwd=cwd, - env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr else None), - **popen_kwargs, - ) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except OSError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -429,17 +505,16 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return stdout, process.returncode -LONG_VERSION_PY[ - "git" -] = r''' +LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.23 (https://github.com/python-versioneer/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -448,11 +523,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= import re import subprocess import sys -from typing import Callable, Dict +from typing import Any, Callable, Dict, List, Optional, Tuple import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -468,8 +543,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + -def get_config(): +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -491,9 +573,9 @@ class NotThisMethod(Exception): HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -502,13 +584,19 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) process = None - popen_kwargs = {} + popen_kwargs: Dict[str, Any] = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() @@ -524,8 +612,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs) break - except OSError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -545,7 +632,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -570,13 +661,13 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: @@ -598,7 +689,11 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") @@ -662,7 +757,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -681,7 +781,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) @@ -692,7 +792,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): describe_out, rc = runner(GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -702,7 +802,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None @@ -794,14 +894,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -826,7 +926,7 @@ def render_pep440(pieces): return rendered -def render_pep440_branch(pieces): +def render_pep440_branch(pieces: Dict[str, Any]) -> str: """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards @@ -856,7 +956,7 @@ def render_pep440_branch(pieces): return rendered -def pep440_split_post(ver): +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the @@ -866,7 +966,7 @@ def pep440_split_post(ver): return vc[0], int(vc[1] or 0) if len(vc) == 2 else None -def render_pep440_pre(pieces): +def render_pep440_pre(pieces: Dict[str, Any]) -> str: """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: @@ -890,7 +990,7 @@ def render_pep440_pre(pieces): return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -917,7 +1017,7 @@ def render_pep440_post(pieces): return rendered -def render_pep440_post_branch(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. @@ -946,7 +1046,7 @@ def render_pep440_post_branch(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. @@ -968,7 +1068,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -988,7 +1088,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1008,7 +1108,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -1044,7 +1144,7 @@ def render(pieces, style): "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -1092,13 +1192,13 @@ def get_versions(): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: @@ -1120,7 +1220,11 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") @@ -1146,7 +1250,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1155,7 +1259,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r"\d", r)} + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1163,35 +1267,33 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] + r = ref[len(tag_prefix):] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') - if not re.match(r"\d", r): + if not re.match(r'\d', r): continue if verbose: print("picking %s" % r) - return { - "version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": None, - "date": date, - } + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return { - "version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": "no suitable tags", - "date": None, - } + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1209,7 +1311,8 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1217,9 +1320,10 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner( - GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*"], cwd=root - ) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -1229,12 +1333,13 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") @@ -1274,16 +1379,17 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[: git_describe.rindex("-dirty")] + git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) return pieces # tag @@ -1292,9 +1398,10 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] + pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -1318,7 +1425,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): return pieces -def do_vcs_install(versionfile_source, ipy): +def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1330,14 +1437,15 @@ def do_vcs_install(versionfile_source, ipy): files = [versionfile_source] if ipy: files.append(ipy) - try: - my_path = __file__ - if my_path.endswith(".pyc") or my_path.endswith(".pyo"): - my_path = os.path.splitext(my_path)[0] + ".py" - versioneer_file = os.path.relpath(my_path) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) present = False try: with open(".gitattributes", "r") as fobj: @@ -1355,7 +1463,11 @@ def do_vcs_install(versionfile_source, ipy): run_command(GITS, ["add", "--"] + files) -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -1367,23 +1479,20 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return { - "version": dirname[len(parentdir_prefix) :], - "full-revisionid": None, - "dirty": False, - "error": None, - "date": None, - } + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.23) from +# This file was generated by 'versioneer.py' (0.29) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. @@ -1400,39 +1509,41 @@ def get_versions(): """ -def versions_from_file(filename): +def versions_from_file(filename: str) -> Dict[str, Any]: """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() except OSError: raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) -def write_to_version_file(filename, versions): +def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -1450,13 +1561,14 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_branch(pieces): +def render_pep440_branch(pieces: Dict[str, Any]) -> str: """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards @@ -1479,13 +1591,14 @@ def render_pep440_branch(pieces): rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def pep440_split_post(ver): +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the @@ -1495,7 +1608,7 @@ def pep440_split_post(ver): return vc[0], int(vc[1] or 0) if len(vc) == 2 else None -def render_pep440_pre(pieces): +def render_pep440_pre(pieces: Dict[str, Any]) -> str: """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: @@ -1519,7 +1632,7 @@ def render_pep440_pre(pieces): return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -1546,7 +1659,7 @@ def render_pep440_post(pieces): return rendered -def render_pep440_post_branch(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. @@ -1575,7 +1688,7 @@ def render_pep440_post_branch(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. @@ -1597,7 +1710,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -1617,7 +1730,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1637,16 +1750,14 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return { - "version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None, - } + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default @@ -1670,20 +1781,16 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return { - "version": rendered, - "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], - "error": None, - "date": pieces.get("date"), - } + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" -def get_versions(verbose=False): +def get_versions(verbose: bool = False) -> Dict[str, Any]: """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. @@ -1698,8 +1805,9 @@ def get_versions(verbose=False): assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, "please set versioneer.versionfile_source" + verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) @@ -1753,21 +1861,17 @@ def get_versions(verbose=False): if verbose: print("unable to compute version") - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", - "date": None, - } + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} -def get_version(): +def get_version() -> str: """Get the short version string for this project.""" return get_versions()["version"] -def get_cmdclass(cmdclass=None): +def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): """Get the custom setuptools subclasses used by Versioneer. If the package uses a different cmdclass (e.g. one from numpy), it @@ -1795,16 +1899,16 @@ def get_cmdclass(cmdclass=None): class cmd_version(Command): description = "report generated version string" - user_options = [] - boolean_options = [] + user_options: List[Tuple[str, str, str]] = [] + boolean_options: List[str] = [] - def initialize_options(self): + def initialize_options(self) -> None: pass - def finalize_options(self): + def finalize_options(self) -> None: pass - def run(self): + def run(self) -> None: vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) @@ -1812,7 +1916,6 @@ def run(self): print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version # we override "build_py" in setuptools @@ -1834,13 +1937,13 @@ def run(self): # but the build_py command is not expected to copy any files. # we override different "build_py" commands for both environments - if "build_py" in cmds: - _build_py = cmds["build_py"] + if 'build_py' in cmds: + _build_py: Any = cmds['build_py'] else: from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1852,19 +1955,19 @@ def run(self): # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - if "build_ext" in cmds: - _build_ext = cmds["build_ext"] + if 'build_ext' in cmds: + _build_ext: Any = cmds['build_ext'] else: from setuptools.command.build_ext import build_ext as _build_ext class cmd_build_ext(_build_ext): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1877,22 +1980,21 @@ def run(self): return # now locate _version.py in the new build/ directory and replace # it with an updated value - target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) if not os.path.exists(target_versionfile): - print( - f"Warning: {target_versionfile} does not exist, skipping " - "version update. This can happen if you are running build_ext " - "without first running build_py." - ) + print(f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py.") return print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) - cmds["build_ext"] = cmd_build_ext if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - + from cx_Freeze.dist import build_exe as _build_exe # type: ignore # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -1901,7 +2003,7 @@ def run(self): # ... class cmd_build_exe(_build_exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1913,25 +2015,24 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) cmds["build_exe"] = cmd_build_exe del cmds["build_py"] - if "py2exe" in sys.modules: # py2exe enabled? - from py2exe.distutils_buildexe import py2exe as _py2exe + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore + except ImportError: + from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore class cmd_py2exe(_py2exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1943,27 +2044,23 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) cmds["py2exe"] = cmd_py2exe # sdist farms its file list building out to egg_info - if "egg_info" in cmds: - _sdist = cmds["egg_info"] + if 'egg_info' in cmds: + _egg_info: Any = cmds['egg_info'] else: from setuptools.command.egg_info import egg_info as _egg_info class cmd_egg_info(_egg_info): - def find_sources(self): + def find_sources(self) -> None: # egg_info.find_sources builds the manifest list and writes it # in one shot super().find_sources() @@ -1971,7 +2068,7 @@ def find_sources(self): # Modify the filelist and normalize it root = get_root() cfg = get_config_from_root(root) - self.filelist.append("versioneer.py") + self.filelist.append('versioneer.py') if cfg.versionfile_source: # There are rare cases where versionfile_source might not be # included by default, so we must be explicit @@ -1984,23 +2081,23 @@ def find_sources(self): # We will instead replicate their final normalization (to unicode, # and POSIX-style paths) from setuptools import unicode_utils + normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') + for f in self.filelist.files] - normalized = [unicode_utils.filesys_decode(f).replace(os.sep, "/") for f in self.filelist.files] + manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') + with open(manifest_filename, 'w') as fobj: + fobj.write('\n'.join(normalized)) - manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") - with open(manifest_filename, "w") as fobj: - fobj.write("\n".join(normalized)) - - cmds["egg_info"] = cmd_egg_info + cmds['egg_info'] = cmd_egg_info # we override different "sdist" commands for both environments - if "sdist" in cmds: - _sdist = cmds["sdist"] + if 'sdist' in cmds: + _sdist: Any = cmds['sdist'] else: from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): - def run(self): + def run(self) -> None: versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old @@ -2008,7 +2105,7 @@ def run(self): self.distribution.metadata.version = versions["version"] return _sdist.run(self) - def make_release_tree(self, base_dir, files): + def make_release_tree(self, base_dir: str, files: List[str]) -> None: root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) @@ -2017,8 +2114,8 @@ def make_release_tree(self, base_dir, files): # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, self._versioneer_generated_versions) - + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist return cmds @@ -2073,14 +2170,16 @@ def make_release_tree(self, base_dir, files): """ -def do_setup(): +def do_setup() -> int: """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) - except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: + except (OSError, configparser.NoSectionError, + configparser.NoOptionError) as e: if isinstance(e, (OSError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", file=sys.stderr) + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) @@ -2089,18 +2188,16 @@ def do_setup(): print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + maybe_ipy: Optional[str] = ipy if os.path.exists(ipy): try: with open(ipy, "r") as f: @@ -2121,16 +2218,16 @@ def do_setup(): print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) - ipy = None + maybe_ipy = None # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. - do_vcs_install(cfg.versionfile_source, ipy) + do_vcs_install(cfg.versionfile_source, maybe_ipy) return 0 -def scan_setup_py(): +def scan_setup_py() -> int: """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False @@ -2167,10 +2264,14 @@ def scan_setup_py(): return errors +def setup_command() -> NoReturn: + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) + setup_command()