Skip to content

Commit

Permalink
Make environment files management consistent between pip and conda (
Browse files Browse the repository at this point in the history
#429)

* Restructure environment file parsing across pip and conda, and versioning

* Linting

* Small fix

* Try with this syntax

* Update setup.py to remain only for backw-compat

* Update install page

* Fix dev env

* Fix packages find given coreg submodule

* Add wheel to build

* Use setuptools_scm

* Delete version.py file

* Make opencv fully optional

* Use the more common _version.py filename

* Linting

* Install opencv with base env in CI

* Fix test deprecate with new versioning

* Linting

* Skip test failing due to SciPy

* Linting

* Add pull request template

* Add warning on environment update

* Linting

* Move warning

* Fix space

* Update how-to-release with conda-forge explanations

* Change license

* Make dev-env and setup.cfg dev dependencies structure mirrored for clarity
  • Loading branch information
rhugonnet committed Sep 5, 2023
1 parent 3956073 commit 7321e97
Show file tree
Hide file tree
Showing 22 changed files with 408 additions and 157 deletions.
6 changes: 6 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!-- Feel free to remove check-list items that aren't relevant to your change -->

- [ ] Resolves #xxx,
- [ ] Tests added, otherwise issue #xxx opened,
- [ ] New optional dependencies added to both `dev-environment.yml` and `setup.cfg`,
- [ ] Fully documented, including `api/*.md` for new API.
148 changes: 148 additions & 0 deletions .github/scripts/generate_pip_deps_from_conda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
(Copied from pandas: https://github.com/pandas-dev/pandas/blob/main/scripts/generate_pip_deps_from_conda.py)
Convert the conda environment.yml to the pip requirements-dev.txt,
or check that they have the same packages (for the CI)
Usage:
Generate `requirements-dev.txt`
$ python scripts/generate_pip_deps_from_conda.py
Compare and fail (exit status != 0) if `requirements-dev.txt` has not been
generated with this script:
$ python scripts/generate_pip_deps_from_conda.py --compare
"""
import argparse
import pathlib
import re
import sys

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
import yaml

EXCLUDE = {"python"}
REMAP_VERSION = {"tzdata": "2022.1"}
RENAME = {
"pytables": "tables",
"psycopg2": "psycopg2-binary",
"dask-core": "dask",
"seaborn-base": "seaborn",
"sqlalchemy": "SQLAlchemy",
}


def conda_package_to_pip(package: str):
"""
Convert a conda package to its pip equivalent.
In most cases they are the same, those are the exceptions:
- Packages that should be excluded (in `EXCLUDE`)
- Packages that should be renamed (in `RENAME`)
- A package requiring a specific version, in conda is defined with a single
equal (e.g. ``pandas=1.0``) and in pip with two (e.g. ``pandas==1.0``)
"""
package = re.sub("(?<=[^<>])=", "==", package).strip()
print(package)

for compare in ("<=", ">=", "=="):
if compare in package:
pkg, version = package.split(compare)
if pkg in EXCLUDE:
return
if pkg in REMAP_VERSION:
return "".join((pkg, compare, REMAP_VERSION[pkg]))
if pkg in RENAME:
return "".join((RENAME[pkg], compare, version))

if package in EXCLUDE:
return

if package in RENAME:
return RENAME[package]

return package


def generate_pip_from_conda(conda_path: pathlib.Path, pip_path: pathlib.Path, compare: bool = False) -> bool:
"""
Generate the pip dependencies file from the conda file, or compare that
they are synchronized (``compare=True``).
Parameters
----------
conda_path : pathlib.Path
Path to the conda file with dependencies (e.g. `environment.yml`).
pip_path : pathlib.Path
Path to the pip file with dependencies (e.g. `requirements-dev.txt`).
compare : bool, default False
Whether to generate the pip file (``False``) or to compare if the
pip file has been generated with this script and the last version
of the conda file (``True``).
Returns
-------
bool
True if the comparison fails, False otherwise
"""
with conda_path.open() as file:
deps = yaml.safe_load(file)["dependencies"]

pip_deps = []
for dep in deps:
if isinstance(dep, str):
conda_dep = conda_package_to_pip(dep)
if conda_dep:
pip_deps.append(conda_dep)
elif isinstance(dep, dict) and len(dep) == 1 and "pip" in dep:
pip_deps.extend(dep["pip"])
else:
raise ValueError(f"Unexpected dependency {dep}")

header = (
f"# This file is auto-generated from {conda_path.name}, do not modify.\n"
"# See that file for comments about the need/usage of each dependency.\n\n"
)
pip_content = header + "\n".join(pip_deps) + "\n"

# add setuptools to requirements-dev.txt
with open(pathlib.Path(conda_path.parent, "pyproject.toml"), "rb") as fd:
meta = tomllib.load(fd)
for requirement in meta["build-system"]["requires"]:
if "setuptools" in requirement:
pip_content += requirement
pip_content += "\n"

if compare:
with pip_path.open() as file:
return pip_content != file.read()

with pip_path.open("w") as file:
file.write(pip_content)
return False


if __name__ == "__main__":
argparser = argparse.ArgumentParser(description="convert (or compare) conda file to pip")
argparser.add_argument(
"--compare",
action="store_true",
help="compare whether the two files are equivalent",
)
args = argparser.parse_args()

conda_fname = "environment.yml"
pip_fname = "requirements.txt"
repo_path = pathlib.Path(__file__).parent.parent.parent.absolute()
res = generate_pip_from_conda(
pathlib.Path(repo_path, conda_fname),
pathlib.Path(repo_path, pip_fname),
compare=args.compare,
)
if res:
msg = f"`{pip_fname}` has to be generated with `{__file__}` after " f"`{conda_fname}` is modified.\n"
sys.stderr.write(msg)
sys.exit(res)
File renamed without changes.
6 changes: 3 additions & 3 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ jobs:
if: steps.cache.outputs.cache-hit != 'true'
run: |
mamba install pyyaml python=${{ matrix.python-version }}
pkgs_conda_base=`python .github/get_yml_env_nopy.py "environment.yml" --p "conda"`
pkgs_pip_base=`python .github/get_yml_env_nopy.py "environment.yml" --p "pip"`
mamba install python=${{ matrix.python-version }} $pkgs_conda_base graphviz
pkgs_conda_base=`python .github/scripts/get_yml_env_nopy.py "environment.yml" --p "conda"`
pkgs_pip_base=`python .github/scripts/get_yml_env_nopy.py "environment.yml" --p "pip"`
mamba install python=${{ matrix.python-version }} $pkgs_conda_base graphviz opencv
if [[ "$pkgs_pip_base" != "None" ]]; then
pip install $pkgs_pip_base
fi
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ dmypy.json
# Pyre type checker
.pyre/

# version file
xdem/version.py
# Version file
xdem/_version.py

# Example data downloaded/produced during tests
examples/data/Longyearbyen/data/
Expand Down
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,13 @@ repos:
rev: 2.0.0
hooks:
- id: relint
- repo: local
hooks:
# Generate pip's requirements.txt from conda's environment.yml to ensure consistency
- id: pip-to-conda
name: Generate pip dependency from conda
language: python
entry: .github/scripts/generate_pip_deps_from_conda.py
files: ^(environment.yml|requirements.txt)$
pass_filenames: false
additional_dependencies: [tomli, pyyaml]
56 changes: 52 additions & 4 deletions HOW_TO_RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# How to issue an xDEM release

## The easy way
## GitHub and PyPI

1. Change the version number in setup.py. It can be easily done from GitHub directly without a PR. The version number is important for PyPI as it will determine the file name of the wheel. A name can [never be reused](https://pypi.org/help/#file-name-reuse), even if a file or project have been deleted.
### The easy way

1. Change the version number in `setup.cfg`. It can be easily done from GitHub directly without a PR. The version number is important for PyPI as it will determine the file name of the wheel. A name can [never be reused](https://pypi.org/help/#file-name-reuse), even if a file or project have been deleted.

2. Follow the steps to [create a new release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) on GitHub.
Use the same release number and tag as in setup.py.
Use the same release number and tag as in `setup.cfg`.

An automatic GitHub action will start to push and publish the new release to PyPI.

Expand All @@ -17,7 +19,7 @@ An automatic GitHub action will start to push and publish the new release to PyP
- You can now edit the version number on the main branch.
- Before releasing, you need to delete **both** the tag and the release of the previous release. If you release with the same tag without deletion, it will ignore your commit changing the version number, and PyPI will block the upload again. You're stuck in a circle.

## The hard way
### The hard way

1. Go to your local main repository (not the fork) and ensure your master branch is synced:
git checkout master
Expand Down Expand Up @@ -63,3 +65,49 @@ An automatic GitHub action will start to push and publish the new release to PyP
and switch your new release tag (at the bottom) from "Inactive" to "Active".
It should now build automatically.
15. Issue the release announcement!

## Conda-forge

Conda-forge distributions work by having a "feedstock" version of the package, containing instructions on how to bundle it for conda.
The xDEM feedstock is available at [https://github.com/conda-forge/xdem-feedstock](https://github.com/conda-forge/xdem-feedstock), and only accessible by maintainers.

### If the conda-forge bot works

To update the conda-forge distribution of xDEM, very few steps should have to be performed manually. If the conda bot works, a PR will be opened at [https://github.com/conda-forge/xdem-feedstock](https://github.com/conda-forge/xdem-feedstock) within a day of publishing a new GitHub release.
Assuming the dependencies have not changed, only two lines will be changed in the `meta.yaml` file of the feedstock: (i) the new version number and (ii) the new sha256 checksum for the GitHub-released package. Those will be updated automatically by the bot.

However, if the dependencies or license need to be updated, this has to be done manually. Then, add the bot branch as a remote branch and push the dependency changes to `meta.yaml` (see additional info from conda bot for license).

### If the conda-forge bot does not work

In this case, the PR has to be opened manually, and the new version number and new sha256 checksum have to be updated manually as well.

The most straightforward way to obtain the new sha256 checksum is to run `conda-build` (see below) with the old checksum which will fail, and then copying the new hash of the "SHA256 mismatch: ..." error that arises!

First, the xdem-feedstock repo has to be forked on GitHub.
Then, follow these steps for `NEW_VERSION` (substitute with the actual version name):
```bash

>>> conda install conda-build

>>> git clone https://github.com/your_username/xdem-feedstock # or git pull (and make sure the fork is up to date with the upstream repo) if the repo is already cloned

>>> cd xdem-feedstock/recipe

# Update meta.yaml:
# {% set version = "NEW_VERSION" %}
# sha256: NEW_SHA256

>>> conda-build . # This is to validate that the build process works, but is technically optional.

>>> git add -u && git commit -m "Updated version to NEW_VERSION" # Or whatever you want to tell us :)

>>> git push -u origin master
```

An alternative solution to get the sha256sum is to run `sha256sum` on the release file downloaded from GitHub

Now, a PR can be made from your personal fork to the upstream xdem-feedstock.
An automatic linter will say whether the updates conform to the syntax and a CI action will build the package to validate it.
Note that you have to be a maintainer or have the PR be okayed by a maintainer for the CI action to run.
If this works, the PR can be merged, and the conda-forge version will be updated within a few hours!
38 changes: 22 additions & 16 deletions dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,53 @@ channels:
- conda-forge
dependencies:
- python>=3.8
- proj>=7.2
- geopandas>=0.10.0
- fiona
- shapely
- numba
- numpy
- matplotlib
- opencv
- openh264
- pyproj>=3.4
- rasterio>=1.3
- scipy
- tqdm
- scikit-image
- scikit-gstat>=1.0
- pytransform3d
- geoutils==0.0.13
- geoutils=0.0.13

# Development-specific
# Development-specific, to mirror manually in setup.cfg [options.extras_require].
- pip
- sphinx-book-theme
- autovizwidget
- graphviz

# Optional dependencies
- opencv
- openh264
- pytransform3d
# - richdem

# Test dependencies
- pytest
- pytest-xdist
- pyyaml
- flake8
- pylint

# Doc dependencies
- sphinx
- myst-nb
- numpydoc
- sphinx-book-theme
- sphinxcontrib-programoutput
- flake8
- sphinx-design
- pylint
- sphinx-autodoc-typehints
- sphinx-gallery
- pyyaml
# - richdem
- autovizwidget
- graphviz
- myst-nb
- numpydoc

- pip:
- -e ./

# Development-specific
# Optional dependencies
- noisyopt

# To run CI against latest GeoUtils
# - git+https://github.com/GlacioHack/GeoUtils.git
20 changes: 0 additions & 20 deletions dev-requirements.txt

This file was deleted.

5 changes: 2 additions & 3 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from sphinx_gallery.sorting import ExplicitOrder

import xdem.version
import xdem

# -- Project information -----------------------------------------------------

Expand All @@ -29,8 +29,7 @@
author = "Erik Mannerfelt, Romain Hugonnet, Amaury Dehecq and others"

# The full version, including alpha/beta/rc tags
release = xdem.version.version

release = xdem.__version__

os.environ["PYTHON"] = sys.executable

Expand Down
Loading

0 comments on commit 7321e97

Please sign in to comment.