|
| 1 | +#!/usr/bin/env python |
| 2 | +"""Convert the conda py3.6.yml to the pip requirements-dev.txt. |
| 3 | +
|
| 4 | +It also checks that they have the same packages (for the CI). |
| 5 | +The original script is taken from Pandas github repository. |
| 6 | +https://github.com/pandas-dev/pandas/blob/master/scripts/generate_pip_deps_from_conda.py. |
| 7 | +
|
| 8 | +Usage |
| 9 | +----- |
| 10 | +
|
| 11 | + Generate `requirements-dev.txt` |
| 12 | + $ ./generate_pip_deps_from_conda.py |
| 13 | +
|
| 14 | + Compare and fail (exit status != 0) if `requirements-dev.txt` has not been |
| 15 | + generated with this script: |
| 16 | + $ ./generate_pip_deps_from_conda.py --compare |
| 17 | +""" |
| 18 | +import argparse |
| 19 | +import os |
| 20 | +import re |
| 21 | +import sys |
| 22 | + |
| 23 | +import yaml |
| 24 | + |
| 25 | +EXCLUDE = {"python"} |
| 26 | +RENAME = { |
| 27 | + "pytables": "tables", |
| 28 | + "pyqt": "pyqt5", |
| 29 | + "dask-core": "dask", |
| 30 | + "matplotlib-base": "matplotlib", |
| 31 | + "seaborn-base": "seaborn", |
| 32 | +} |
| 33 | + |
| 34 | + |
| 35 | +def conda_package_to_pip(package): |
| 36 | + """Convert a conda package to its pip equivalent. |
| 37 | +
|
| 38 | + In most cases they are the same, those are the exceptions: |
| 39 | + - Packages that should be excluded (in `EXCLUDE`) |
| 40 | + - Packages that should be renamed (in `RENAME`) |
| 41 | + - A package requiring a specific version, in conda is defined with a single |
| 42 | + equal (e.g. ``pandas=1.0``) and in pip with two (e.g. ``pandas==1.0``) |
| 43 | + """ |
| 44 | + package = re.sub("(?<=[^<>])=", "==", package).strip() |
| 45 | + |
| 46 | + for compare in ("<=", ">=", "=="): |
| 47 | + if compare not in package: |
| 48 | + continue |
| 49 | + |
| 50 | + pkg, version = package.split(compare) |
| 51 | + if pkg in EXCLUDE: |
| 52 | + return |
| 53 | + |
| 54 | + if pkg in RENAME: |
| 55 | + return "".join((RENAME[pkg], compare, version)) |
| 56 | + |
| 57 | + break |
| 58 | + |
| 59 | + if package in RENAME: |
| 60 | + return RENAME[package] |
| 61 | + |
| 62 | + return package |
| 63 | + |
| 64 | + |
| 65 | +def main(conda_fname, pip_fname, compare=False): |
| 66 | + """Generate the pip dependencies file from the conda file. |
| 67 | +
|
| 68 | + It also compares that they are synchronized (``compare=True``). |
| 69 | +
|
| 70 | + Parameters |
| 71 | + ---------- |
| 72 | + conda_fname : str |
| 73 | + Path to the conda file with dependencies (e.g. `py3.6.yml`). |
| 74 | + pip_fname : str |
| 75 | + Path to the pip file with dependencies (e.g. `requirements-dev.txt`). |
| 76 | + compare : bool, default False |
| 77 | + Whether to generate the pip file (``False``) or to compare if the |
| 78 | + pip file has been generated with this script and the last version |
| 79 | + of the conda file (``True``). |
| 80 | +
|
| 81 | + Returns |
| 82 | + ------- |
| 83 | + bool |
| 84 | + True if the comparison fails, False otherwise |
| 85 | + """ |
| 86 | + with open(conda_fname) as conda_fd: |
| 87 | + deps = yaml.safe_load(conda_fd)["dependencies"] |
| 88 | + |
| 89 | + pip_deps = [] |
| 90 | + for dep in deps: |
| 91 | + if isinstance(dep, str): |
| 92 | + conda_dep = conda_package_to_pip(dep) |
| 93 | + if conda_dep: |
| 94 | + pip_deps.append(conda_dep) |
| 95 | + elif isinstance(dep, dict) and len(dep) == 1 and "pip" in dep: |
| 96 | + pip_deps += dep["pip"] |
| 97 | + else: |
| 98 | + raise ValueError(f"Unexpected dependency {dep}") |
| 99 | + |
| 100 | + fname = os.path.split(conda_fname)[1] |
| 101 | + header = ( |
| 102 | + f"# This file is auto-generated from {fname}, do not modify.\n" |
| 103 | + "# See that file for comments about the need/usage of each dependency.\n\n" |
| 104 | + ) |
| 105 | + pip_content = header + "\n".join(pip_deps) |
| 106 | + |
| 107 | + if compare: |
| 108 | + with open(pip_fname) as pip_fd: |
| 109 | + return pip_content != pip_fd.read() |
| 110 | + else: |
| 111 | + with open(pip_fname, "w") as pip_fd: |
| 112 | + pip_fd.write(pip_content) |
| 113 | + return False |
| 114 | + |
| 115 | + |
| 116 | +if __name__ == "__main__": |
| 117 | + argparser = argparse.ArgumentParser(description="convert (or compare) conda file to pip") |
| 118 | + argparser.add_argument( |
| 119 | + "--compare", action="store_true", help="compare whether the two files are equivalent", |
| 120 | + ) |
| 121 | + args = argparser.parse_args() |
| 122 | + |
| 123 | + repo_path = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) |
| 124 | + res = main( |
| 125 | + os.path.join(repo_path, "ci/requirements/py3.6.yml"), |
| 126 | + os.path.join(repo_path, "requirements.txt"), |
| 127 | + compare=args.compare, |
| 128 | + ) |
| 129 | + if res: |
| 130 | + msg = ( |
| 131 | + f"`requirements-dev.txt` has to be generated with `{sys.argv[0]}` after " |
| 132 | + "`py3.6.yml` is modified.\n" |
| 133 | + ) |
| 134 | + if args.azure: |
| 135 | + msg = f"##vso[task.logissue type=error;sourcepath=requirements-dev.txt]{msg}" |
| 136 | + sys.stderr.write(msg) |
| 137 | + sys.exit(res) |
0 commit comments