From 141eb6a315542317ddab2f7a413a24559c84492f Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 17 Jan 2024 12:08:18 +0000 Subject: [PATCH] Start mindeps --- ci/scripts/min-deps.py | 98 ++++++++++++++++++++++++++++++++++++++ ci/scripts/run-min-deps.sh | 17 +++++++ pyproject.toml | 22 ++++----- 3 files changed, 126 insertions(+), 11 deletions(-) create mode 100755 ci/scripts/min-deps.py create mode 100755 ci/scripts/run-min-deps.sh diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py new file mode 100755 index 0000000000..9d0fcbb1b5 --- /dev/null +++ b/ci/scripts/min-deps.py @@ -0,0 +1,98 @@ +#!python3 +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib +from packaging.requirements import Requirement +from packaging.version import Version + + +def min_dep(req: Requirement) -> str: + """ + Given a requirement, return the minimum version specifier. + + Example + ------- + + >>> min_dep(Requirement("numpy>=1.0")) + "numpy==1.0" + """ + req_name = req.name + if req.extras: + req_name = f"{req_name}[{','.join(req.extras)}]" + + # TODO: Should this be allowed? + if not req.specifier: + return req_name + + min_version = Version("0.0.0.a1") + for spec in req.specifier: + if spec.operator in [">", ">=", "~-"]: + min_version = max(min_version, Version(spec.version)) + elif spec.operator == "==": + min_version = Version(spec.version) + + # TODO: should this return `~=` or `==`? + return f"{req_name}=={min_version}.*" + + +def extract_min_deps( + dependencies: list[str], + *, + pyproject + ) -> list[str]: + dependencies = dependencies.copy() # We'll be mutating this + requirements: list[Requirement] = [] + project_name = pyproject["project"]["name"] + + while len(dependencies) > 0: + req = Requirement(dependencies.pop()) + + # If we are reffering to other optional dependency lists, resolve them + if req.name == project_name: + assert req.extras, f"Project included itself as dependency, without specifying extras: {req}" + for extra in req.extras: + dependencies.extend(pyproject["project"]["optional-dependencies"][extra]) + else: + requirements.append(min_dep(req)) + + return requirements + + +def main(): + # TODO: Allow optional dependencies + parser = argparse.ArgumentParser( + prog="min-deps", + description="""Parse a pyproject.toml file and output a list of minimum dependencies. + + Output is directly passable to `pip install`.""", + usage="pip install `python min-deps.py pyproject.toml`", + ) + parser.add_argument( + "path", type=Path, help="pyproject.toml to parse minimum dependencies from" + ) + parser.add_argument("--extras", type=str, nargs="*", help="extras to install") + + args = parser.parse_args() + + pyproject = tomllib.loads(args.path.read_text()) + + project_name = pyproject["project"]["name"] + deps = pyproject["project"]["dependencies"] + + for extra in args.extras: + deps.append(f"{project_name}[{extra}]") + + min_deps = extract_min_deps(deps, pyproject=pyproject) + + print(" ".join(min_deps)) + + +if __name__ == "__main__": + main() diff --git a/ci/scripts/run-min-deps.sh b/ci/scripts/run-min-deps.sh new file mode 100755 index 0000000000..6f33256b6f --- /dev/null +++ b/ci/scripts/run-min-deps.sh @@ -0,0 +1,17 @@ +mamba env remove -yn scanpy-min-deps-test +mamba create -yn scanpy-min-deps-test "python=3.9" + +PACKAGES=`python3 ci/scripts/min-deps.py pyproject.toml --extra dev test` + +# conda activate anndata-min-deps-test +# conda run -n anndata-min-deps-test pip install cupy-cuda12x + + +echo Installing $PACKAGES +conda run -n scanpy-min-deps-test pip install $PACKAGES +conda run -n scanpy-min-deps-test pip install pytest-xdist # cupy-cuda12x +conda run -n scanpy-min-deps-test pip install -e . --no-deps +echo "Starting tests" +conda run -n scanpy-min-deps-test pytest -n auto + +conda list -n scanpy-min-deps-tests \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 65d3dfb8bc..2057cb05bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,22 +48,22 @@ classifiers = [ dependencies = [ "anndata>=0.7.4", # numpy needs a version due to #1320 - "numpy>=1.17.0", + "numpy>=1.23", "matplotlib>=3.6", - "pandas >=2.1.3", - "scipy>=1.4", - "seaborn>=0.13.0", - "h5py>=3", + "pandas >=1.5", + "scipy>=1.8", + "seaborn>=0.13", + "h5py>=3.1", "tqdm", "scikit-learn>=0.24", - "statsmodels>=0.10.0rc2", + "statsmodels>=0.10", "patsy", "networkx>=2.3", "natsort", "joblib", - "numba>=0.41.0", + "numba>=0.53", "umap-learn>=0.3.10", - "packaging", + "packaging>=20.0", "session-info", "legacy-api-wrap>=1.4", # for positional API deprecations "get-annotations; python_version < '3.10'", @@ -133,8 +133,8 @@ dev = [ ] # Algorithms paga = ["igraph"] -louvain = ["igraph", "louvain>=0.6,!=0.6.2"] # Louvain community detection -leiden = ["igraph>=0.10", "leidenalg>=0.9"] # Leiden community detection +louvain = ["igraph", "louvain>=0.6.0,!=0.6.2"] # Louvain community detection +leiden = ["igraph>=0.10", "leidenalg>=0.9.0"] # Leiden community detection bbknn = ["bbknn"] # Batch balanced KNN (batch correction) magic = ["magic-impute>=2.0"] # MAGIC imputation method skmisc = ["scikit-misc>=0.1.3"] # highly_variable_genes method 'seurat_v3' @@ -143,7 +143,7 @@ scanorama = ["scanorama"] # Scanorama dataset integration scrublet = ["scrublet"] # Doublet detection # Acceleration rapids = ["cudf>=0.9", "cuml>=0.9", "cugraph>=0.9"] # GPU accelerated calculation of neighbors -dask = ["dask[array]!=2.17.0"] # Use the Dask parallelization engine +dask = ["dask[array]>=2022.09"] # Use the Dask parallelization engine dask-ml = ["dask-ml", "scanpy[dask]"] # Dask-ML for sklearn-like API [tool.hatch.build]