diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000..266ac94 --- /dev/null +++ b/.cruft.json @@ -0,0 +1,38 @@ +{ + "template": "https://github.com/wpk-nist-gov/cookiecutter-pypackage.git", + "commit": "a0209ae199aa6f953364fc929e50d41d58082173", + "checkout": "feature/markdown", + "context": { + "cookiecutter": { + "full_name": "William P. Krekelberg", + "email": "wpk@nist.gov", + "github_username": "wpk-nist-gov", + "pypi_username": "wpk-nist", + "conda_channel": "wpk-nist", + "project_name": "pyproject2conda", + "project_slug": "pyproject2conda", + "_copy_without_render": [ + "*.html", + "docs/_templates/*.rst", + "docs/_templates/autosummary/*.rst", + "docs/_templates/autodocsumm/*.rst", + "docs/_static/css/*", + "docs/_static/js/*", + "changelog.d/templates/*.j2", + "changelog.d/templates/auto-changelog/*.jinja2" + ], + "project_short_description": "A script to convert a Python project declared on a pyproject.toml to a conda environment.", + "version": "0.0.1", + "use_pytest": "y", + "use_pypi_deployment_with_travis": "n", + "command_line_interface": "Click", + "create_author_file": "y", + "open_source_license": "NIST license", + "sphinx_auto": "automodule", + "sphinx_use_autodocsumm": "y", + "sphinx_theme": "sphinx_book_theme", + "_template": "https://github.com/wpk-nist-gov/cookiecutter-pypackage.git" + } + }, + "directory": null +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..018c82b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,44 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab + +[*.mk] +indent_style = tab + +[*.{yaml,yml}] +indent_size = 2 + +[*.ini] +indent_size = 4 + +[*.json] +indent_size = 2 + + +[*.txt] +indent_size = 4 +trim_trailing_whitespace = false + + +[*.md] +indent_size = 2 +max_line_length = 80 +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..9f762eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,17 @@ + + +- pyproject2conda version: +- Python version: +- Operating System: + +### Description + +Describe what you were trying to get done. Tell us what happened, what went +wrong, and what you expected to happen. + +### What I Did + +```bash +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83f512f --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# IDE settings +.vscode/ +pyrightconfig.json +.autoenv.zsh +.autoenv_leave.zsh +/docs/**/generated/ +/monkeytype.sqlite3 +/dist-conda/* +!/dist-conda/Makefile +/pyproject2conda-feedstock*/ +/tmp/* diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..9ad3717 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,6 @@ +# Example markdownlint configuration with all properties set to their default value + +# Default state for all rules +default: true +# disable rules? +# MD026: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..19a5ba2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,134 @@ +--- +# pre-commit install +# pre-commit run --all-files +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_install_hook_types: + - pre-commit + - commit-msg +repos: + #* Top level + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + #* Formatting + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.0-alpha.6" + hooks: + - id: prettier + stages: [commit] + additional_dependencies: + - prettier-plugin-toml + #** markdown + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.6.0 + hooks: + - id: markdownlint-cli2 + args: ["--style prettier"] + #* Linting + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: "v0.0.261" + hooks: + - id: ruff + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + # - id: black-jupyter + # Move to just black. use nbqa for notebook formatting + - id: black + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.13.0" + hooks: + - id: blacken-docs + additional_dependencies: + - black==23.3.0 + # exclude: ^README.md + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.7.0 + hooks: + - id: nbqa-ruff + additional_dependencies: [ruff] + - id: nbqa-black + additional_dependencies: [black] + + #* Commit message + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.0.1 + hooks: + - id: commitizen + stages: [commit-msg] + + #* Manual Linting + - repo: local + hooks: + - id: mypy + name: mypy + entry: tox + args: ["-e", "lint-mypy"] + language: system + pass_filenames: false + # additional_dependencies: [tox] + types: [python] + require_serial: true + stages: [manual] + - id: pyright + name: pyright + entry: pyright + args: [] + language: system + pass_filenames: true + # additional_dependencies: [tox] + types: [python] + require_serial: true + stages: [manual] + # isort, pyupgrade, flake8 defer to ruff + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + stages: [manual] + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + stages: [manual] + args: [--py38-plus] + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + stages: [manual] + additional_dependencies: + - flake8-docstrings + - Flake8-pyproject + # - pep8-naming + # - flake8-rst-docstrings + exclude: ^tests/|^src/pyproject2conda/tests/|^docs/conf.py|^setup.py + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.7.0 + hooks: + - id: nbqa-pyupgrade + additional_dependencies: [pyupgrade] + stages: [manual] + args: [--py38-plus] + - id: nbqa-isort + additional_dependencies: [isort] + stages: [manual] + #** spelling + - repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell + types_or: [python, rst, markdown, cython, c] + additional_dependencies: [tomli] + args: [-I, docs/spelling_wordlist.txt] + stages: [manual] diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..4a19fb5 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1 @@ +proseWrap: "always" diff --git a/.recipe-append.yaml b/.recipe-append.yaml new file mode 100644 index 0000000..f340501 --- /dev/null +++ b/.recipe-append.yaml @@ -0,0 +1,19 @@ +test: + imports: + - pyproject2conda + commands: + - pip check + requires: + - pip + +about: + home: https://github.com/wpk-nist-gov/pyproject2conda + summary: A script to convert a Python project declared on a pyproject.toml to a conda environment. + description: | + A script to convert a Python project declared on a pyproject.toml to a conda environment. + license: NIST-PD + license_file: LICENSE + +extra: + recipe-maintainers: + - wpk-nist-gov diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..1375743 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,9 @@ +# Credits + +## Development Lead + +- William P. Krekelberg \ + +## Contributors + +None yet. Why not be the first? diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a1951da --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +Changelog for `pyproject2conda` + +## Unreleased + +See the fragment files in [changelog.d](https://github.com/wpk-nist-gov/pyproject2conda) + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bd7a033 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,400 @@ +# Contributing + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +## Types of Contributions + +### Report Bugs + +Report bugs at . + +If you are reporting a bug, please include: + +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. + +### Fix Bugs + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +### Implement Features + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +### Write Documentation + +`pyproject2conda` could always use more documentation, whether +as part of the official `pyproject2conda` docs, in docstrings, +or even on the web in blog posts, articles, and such. + +### Submit Feedback + +The best way to send feedback is to file an issue at . + +If you are proposing a feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions are + welcome :) + +## Get Started + +### Environment setup + +[pipx]: https://github.com/pypa/pipx +[condax]: https://github.com/mariusvniekerk/condax +[mamba]: https://github.com/mamba-org/mamba +[conda-fast-setup]: + https://www.anaconda.com/blog/a-faster-conda-for-a-growing-community +[pre-commit]: https://pre-commit.com/ +[tox]: https://tox.wiki/en/latest/ +[tox-conda]: https://github.com/tox-dev/tox-conda +[cruft]: https://github.com/cruft/cruft +[conda-merge]: https://github.com/amitbeka/conda-merge +[git-flow]: https://github.com/nvie/gitflow +[scriv]: https://github.com/nedbat/scriv +[conventional-style]: https://www.conventionalcommits.org/en/v1.0.0/ +[commitizen]: https://github.com/commitizen-tools/commitizen +[nb_conda_kernels]: https://github.com/Anaconda-Platform/nb_conda_kernels + +This project uses a host of tools to (hopefully) make development easier. We +recommend installing some of these tools system wide. For this, we recommend +using either [pipx] or [condax]. We mostly use conda/condax, but the choice is +yours. For conda, we recommend actually using [mamba]. Alternatively, you can +setup `conda` to use the faster `mamba` solver. See [here][conda-fast-setup] for +details. + +Additional tools are: + +- [pre-commit] +- [tox] and [tox-conda] +- [cruft] +- [conda-merge] +- [scriv] + +These are setup using the following: + +```console +condax install pre-commit +condax install tox +condax inject tox tox-conda +condax install cruft +condax install conda-merge +condax install commitizen +pipx install scriv +``` + +Alternatively, you can install these dependencies using: + +```console +conda env update -n {env-name} environment/tools.yaml +``` + +### Getting the repo + +Ready to contribute? Here's how to set up `pyproject2conda` for +local development. + +- Fork the `pyproject2conda` repo on GitHub. + +- Clone your fork locally: + + ```bash + git clone git@github.com:your_name_here/pyproject2conda.git + ``` + + If the repo includes submodules, you can add them either with the initial + close using: + + ```bash + git clone --recursive-submodules git@github.com:your_name_here/pyproject2conda.git + ``` + + or after the clone using + + ```bash + cd pyproject2conda + git submodule update --init --recursive + ``` + +- Create development environment. There are two options to create the + development environment. + + - The recommended method is to use tox by using either: + + ```bash + tox -e dev + ``` + + or + + ```bash + make dev-env + ``` + + These create a development environment located at `.tox/dev`. + + ```bash + make tox-ipykernel-display-name + ``` + + This will add a meaningful display name for the kernel (assuming you're + using [nb_conda_kernels]) + + - Alternativley, you can create centrally located conda environmentment using + the command: + + ```bash + make mamba-dev + ``` + + This will create a conda environment 'pyproject2conda-env' in the default location. + + To install (an editable version) of the current package: + + ```bash + pip install -e . --no-deps + ``` + + or + + ```bash + make install-dev + ``` + +- Initiate [pre-commit] with: + + ```bash + pre-commit install + ``` + + To update the recipe, periodically run: + + ```bash + pre-commit autoupdate + ``` + + If recipes change over time, you can clean up old installs with: + + ```bash + pre-commit gc + ``` + +- Create a branch for local development: + + ```bash + git checkout -b name-of-your-bugfix-or-feature + ``` + + Now you can make your changes locally. Alternatively, we recommend using + [git-flow]. + +- When you're done making changes, check that your changes pass the pre-commit + checks: tests. + + ```bash + pre-commit run [--all-files] + ``` + + To run tests, use: + + ```bash + pytest + ``` + + To test against multiple python versions, use tox: + + ```bash + tox + ``` + + or using the `make`: + + ```bash + make test-all + ``` + + Additionally, you should run the following: + + ```bash + make pre-commit-lint-markdown + make pre-commit-codespell + ``` + +- Create changelog fragment. See [scriv] for more info. + + ```bash + scriv create --edit + ``` + +- Commit your changes and push your branch to GitHub: + + ```bash + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + ``` + + Note that the pre-commit hooks will force the commit message to be in the + [conventional sytle][conventional-style]. To assist this, you may want to + commit using [commitizen]. + + ```bash + cz commit + ``` + +- Submit a pull request through the GitHub website. + +### Dependency management + +Dependencies need to be placed in a few locations, which depend on the nature of +the dependency. + +- Package dependency: `environment.yaml` and `dependencies` section of + `pyproject.toml` +- Documentation dependency: `environment/docs-extras.yaml` and `test` section of + `pyproject.toml` +- Development dependency: `environment/dev-extras.yaml` and `dev` section of + `pyproject.toml` + +Note that total yaml files are build using [conda-merge]. For example, +`environment.yaml` is combined with `environment/docs-extras.yaml` to produce +`environment/docs.yaml`. This is automated in the `Makefile`. You can also run, +after doing any updates, + +```bash +make environment-files-build +``` + +which will rebuild all the needed yaml files. + +## Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +- The pull request should include tests. +- If the pull request adds functionality, the docs should be updated. Put your + new functionality into a function with a docstring, and add the feature to the + list in CHANGELOG.md. You should use [scriv] for this. +- The pull request should work for Python 3.8, 3.9, 3.10. + +## Building the docs + +We use [tox] to isolate the documentation build. Useful commands are as follows. + +- Build the docs: + + ```bash + tox -e docs -- build + ``` + +- Spellcheck the docs: + + ```bash + tox -e docs -- spelling + ``` + +- Create a release of the docs: + + ```bash + tox -e docs -- release + ``` + + If you make any changes to `docs/examples`, you should run: + + ```bash + make docs-examples-symlink + ``` + + to update symlinks from `docs/examples` to `examples`. + + After this, the docs can be pushed to the correct branch for distribution. + +- Live documentation updates using + + ```bash + make docs-livehtml + ``` + +## Using tox + +The package is setup to use tox to test, build and release pip and conda +distributions, and release the docs. Most of these tasks have a command in the +`Makefile`. To test against multiple versions, use: + +```bash +make test-all +``` + +To build the documentation in an isolated environment, use: + +```bash +make docs-build +``` + +To release the documentation use: + +```bash +make docs-release release_args='-m "commit message" -r origin -p' +``` + +Where posargs is are passed to ghp-import. Note that the branch created is +called `nist-pages`. This can be changed in `tox.ini`. + +To build the distribution, use: + +```bash +make dist-pypi-[build-testrelease-release] +``` + +where `build` build to distro, `testrelease` tests putting on `testpypi` and +release puts the distro on pypi. + +To build the conda distribution, use: + +```bash +make dist-conda-[recipe, build] +``` + +where `recipe` makes the conda recipe (using grayskull), and `build` makes the +distro. This can be manually added to a channel. + +To test the created distributions, you can use one of: + +```bash +tox -e test-dist-[pypi, conda]-[local,remote]-py[38,39,...] +``` + +or + +```bash +make test-dist-[pypi, conda]-[local,remote] py=[38, 39, 310] +``` + +where one options in the brackets should be choosen. + +## Package version + +[setuptools_scm]: https://github.com/pypa/setuptools_scm + +Versioning is handled with [setuptools_scm].The pacakge version is set by the +git tag. For convenience, you can override the version in the makefile (calling +tox) by setting `version=v{major}.{minor}.{micro}`. This is useful for updating +the docs, etc. + + + +## Creating conda recipe + +[grayskull]: https://github.com/conda/grayskull + +For the most part, we use [grayskull] to create the conda recipe. However, I've had +issues getting it to play nice with `pyproject.toml` for some of the 'extra' variables. +So, we use grayskull to build the majority of the recipe, and append the file `.recipe-append.yaml`. For some edge cases (install name different from package name, etc), you'll need to manually edit this file to create the final recipe. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b96ea66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This software was developed by employees of the National Institute of Standards +and Technology (NIST), an agency of the Federal Government. Pursuant to title 17 +United States Code Section 105, works of NIST employees are not subject to +copyright protection in the United States and are considered to be in the public +domain. Permission to freely use, copy, modify, and distribute this software and +its documentation without fee is hereby granted, provided that this notice and +disclaimer of warranty appears in all copies. + +THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER +EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY +THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM +INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE +SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT +SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, +INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, OR +IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, +CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR +PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT +OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. + +Distributions of NIST software should also include copyright and licensing +statements of any third-party software that are legally bundled with the code in +compliance with the conditions of those licenses. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..86080a9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +# Defer to setuptools_scm +# manually add not tracked files +# or prune tracked files + +# include README.md +# recursive-include tests *.py +# global-exclude *.py[co] +# prune */*.egg-info +# recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9f9e8eb --- /dev/null +++ b/Makefile @@ -0,0 +1,322 @@ +.PHONY: clean clean-test clean-pyc clean-build help +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_/.-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr docs/_build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + + + + +################################################################################ +# utilities +################################################################################ +.PHONY: pre-commit-init pre-commit pre-commit-all +pre-commit-init: ## install pre-commit + pre-commit install + +pre-commit: ## run pre-commit + pre-commit run + +pre-commit-all: ## run pre-commit on all files + pre-commit run --all-files + +.PHONY: pre-commit-lint pre-commit-lint-notebooks pre-commit-prettier pre-commit-lint-markdown +pre-commit-lint: ## run ruff and black on on all files + pre-commit run --all-files ruff + pre-commit run --all-files black + pre-commit run --all-files blacken-docs + +pre-commit-lint-notebooks: ## Run nbqa linting + pre-commit run --all-files nbqa-ruff + pre-commit run --all-files nbqa-black + +pre-commit-prettier: ## run prettier on all files. + pre-commit run --all-files prettier + +pre-commit-lint-markdown: ## run markdown linter. + pre-commit run --all-files --hook-stage manual markdownlint-cli2 + +.PHONY: pre-commit-lint-extra pre-commit-mypy pre-commit-codespell +pre-commit-lint-extra: ## run all extra linting (isort, flake8, pyupgrade, nbqa isort and pyupgrade) + pre-commit run --all-files --hook-stage manual isort + pre-commit run --all-files --hook-stage manual flake8 + pre-commit run --all-files --hook-stage manual pyupgrade + pre-commit run --all-files --hook-stage manual nbqa-pyupgrade + pre-commit run --all-files --hook-stage manual nbqa-isort + +pre-commit-mypy: ## run mypy + pre-commit run --all-files --hook-stage manual mypy + +pre-commit-pyright: ## run pyright + pre-commit run --all-files --hook-stage manual pyright + +pre-commit-codespell: ## run codespell. Note that this imports allowed words from docs/spelling_wordlist.txt + pre-commit run --all-files --hook-stage manual codespell + + +################################################################################ +# my convenience functions +################################################################################ +.PHONY: user-venv user-autoenv-zsh user-all +user-venv: ## create .venv file with name of conda env + echo $${PWD}/.tox/dev > .venv + +user-autoenv-zsh: ## create .autoenv.zsh files + echo conda activate $$(cat .venv) > .autoenv.zsh + echo conda deactivate > .autoenv_leave.zsh + +user-all: user-venv user-autoenv-zsh ## runs user scripts + + +################################################################################ +# Testing +################################################################################ +.PHONY: test coverage +test: ## run tests quickly with the default Python + pytest -x -v + +test-accept: ## run tests and accept doctest results. (using pytest-accept) + DOCFILLER_SUB=False pytest -v --accept + +coverage: ## check code coverage quickly with the default Python + coverage run --source pyproject2conda -m pytest + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + + +################################################################################ +# versioning +################################################################################ +.PHONY: version-scm version-import version +version-scm: ## check version of package + python -m setuptools_scm + +version-import: ## check version from python import + python -c 'import pyproject2conda; print(pyproject2conda.__version__)' + +version: version-scm version-import + +################################################################################ +# Environment files +################################################################################ +ENVIRONMENTS = $(addsuffix .yaml,$(addprefix environment/, dev docs test)) +PRETTIER = bash scripts/run-prettier.sh + +environment/%.yaml: environment.yaml environment/%-extras.yaml ## create combined environment/{dev,docs,test}.yaml + conda-merge $^ > $@ + $(PRETTIER) $@ + +environment/dev.yaml: ## development environment yaml file +environment/test.yaml: ## testing environment yaml file +enviornment/docs.yaml: ## docs environment yaml file + + +# special for linters +environment/lint.yaml: environment.yaml $(addsuffix .yaml, $(addprefix environment/, test-extras lint-extras)) ## mypy environment + echo $^ + conda-merge $^ > $@ + $(PRETTIER) $@ + +ENVIRONMENTS += environment/lint.yaml + +.PHONY: environment-files-clean +environment-files-clean: ## clean all created environment/{dev,docs,test}.yaml + -rm $(ENVIRONMENTS) 2> /dev/null || true + +.PHONY: environment-files-build +environment-files-build: $(ENVIRONMENTS) ## rebuild all environment files + +################################################################################ +# virtual env +################################################################################ +.PHONY: mamba-env mamba-dev mamba-env-update mamba-dev-update + +mamba-env: environment.yaml ## create base environment + mamba env create -f $< + +mamba-env-update: environment.yaml ## update base environment + mamba env update -f $< + +mamba-dev: environment/dev.yaml ## create development environment + mamba env create -f $< + +mamba-dev-update: environment/dev.yaml ## update development environment + mamba env update -f $< + +################################################################################ +# TOX +############################################################################### +tox_args?=-v +version?= +TOX=CONDA_EXE=mamba SETUPTOOLS_SCM_PRETEND_VERSION=$(version) tox $(tox_args) + +.PHONY: tox-ipykernel-display-name +tox-ipykernel-display-name: ## Update display-name for any tox env with ipykernel + bash ./scripts/tox-ipykernel-display-name.sh pyproject2conda + +## dev env +.PHONY: dev-env +dev-env: environment/dev.yaml ## create development environment using tox + tox -e dev + +## testing +.PHONY: test-all +test-all: environment/test.yaml ## run tests on every Python version with tox. can pass posargs=... + $(TOX) -- $(posargs) + +## docs +.PHONY: docs-examples-symlink +docs-examples-symlink: ## create symlinks to notebooks from /examples/ to /docs/examples. + bash ./scripts/docs-examples-symlinks.sh + + +.PHONY: docs-build docs-release docs-clean docs-command +docs-build: ## build docs in isolation + $(TOX) -e docs -- build +docs-clean: ## clean docs + rm -rf docs/_build/* + rm -rf docs/generated/* + rm -rf docs/reference/generated/* +docs-clean-build: docs-clean docs-build ## clean and build +docs-release: ## release docs. use release_args=... to override stuff + $(TOX) -e docs -- release +docs-command: ## run command with command=... + $(TOX) -e docs -- command + +.PHONY: .docs-spelling docs-nist-pages docs-open docs-livehtml docs-clean-build docs-linkcheck +docs-spelling: ## run spell check with sphinx + $(TOX) -e docs -- spelling +docs-livehtml: ## use autobuild for docs + $(TOX) -e docs -- livehtml +docs-open: ## open the build + $(BROWSER) docs/_build/html/index.html +docs-linkcheck: ## check links + $(TOX) -e docs -- linkcheck + +docs-build docs-release docs-command docs-clean docs-livehtml docs-linkcheck: environment/docs.yaml + +## linting +.PHONY: lint-mypy lint-pyright lint-pytype lint-all lint-command +lint-mypy: ## run mypy mypy_args=... + $(TOX) -e lint -- mypy +lint-pyright: ## run pyright pyright_args=... + $(TOX) -e lint -- pyright +lint-pytype: ## run pytype pytype_args=... + $(TOX) -e lint -- pytype +lint-all: + $(TOX) -e lint -- all +lint-command: + $(TOX) -e lint -- command + +lint-mypy lint-pyright lint-pytype lint-all lint-command: environment/lint.yaml + +## distribution +.PHONY: dist-pypi-build dist-pypi-testrelease dist-pypi-release dist-pypi-command + +dist-pypi-build: ## build dist + $(TOX) -e dist-pypi -- build +dist-pypi-testrelease: ## test release on testpypi + $(TOX) -e dist-pypi -- testrelease +dist-pypi-release: ## release to pypi, can pass posargs=... + $(TOX) -e dist-pypi -- release +dist-pypi-command: ## run command with command=... + $(TOX) -e dist-pypi -- command +dist-pypi-build dist-pypi-testrelease dist-pypi-release dist-pypi-command: environment/dist-pypi.yaml + +.PHONY: dist-conda-recipe dist-conda-build dist-conda-command +dist-conda-recipe: ## build conda recipe can pass posargs=... + $(TOX) -e dist-conda -- recipe +dist-conda-build: ## build conda recipe can pass posargs=... + $(TOX) -e dist-conda -- build +dist-conda-command: ## run command with command=... + $(TOX) -e dist-conda -- command +dist-conda-build dist-conda-recipe dist-conda-command: environment/dist-conda.yaml + + +## test distribution +.PHONY: testdist-pypi-remote testdist-conda-remote testdist-pypi-local testdist-conda-local + +py?=310 +testdist-pypi-remote: ## testdist-pypi-remote: ## test pypi install, can run as `make test-dist-pypi-remote py=39` to run test-dist-pypi-local-py39. Can specify version setting, eg, TEST_VERSION='==0.1.0'. Note that the the format should be '=={version}'. + $(TOX) -e $@-py$(py) -- $(posargs) +testdist-conda-remote: ## test conda install, can run as `make test-dist-conda-remote py=39` to run test-dist-conda-local-py39 + $(TOX) -e $@-py$(py) -- $(poasargs) +testdist-pypi-local: ## test pypi install, can run as `make test-dist-pypi-local py=39` to run test-dist-pypi-local-py39 + $(TOX) -e $@-py$(py) -- $(posargs) +testdist-conda-local: ## test conda install, can run as `make test-dist-conda-local py=39` to run test-dist-conda-local-py39 + $(TOX) -e $@-py$(py) -- $(poasargs) + +testdist-pypi-remote testdist-conda-remote testdist-pypi-local testdist-conda-local: environment/test.yaml + +## list all options +.PHONY: tox-list +tox-list: + $(TOX) -a + + +################################################################################ +# installation +################################################################################ +.PHONY: install install-dev +install: ## install the package to the active Python's site-packages (run clean?) + pip install . --no-deps + +install-dev: ## install development version (run clean?) + pip install -e . --no-deps + + +################################################################################ +# other tools +################################################################################ + +# Note that this requires `auto-changelog`, which can be installed with pip(x) +auto-changelog: ## autogenerate changelog and print to stdout + auto-changelog -u -r usnistgov -v unreleased --tag-prefix v --stdout --template changelog.d/templates/auto-changelog/template.jinja2 + +commitizen-changelog: + cz changelog --unreleased-version unreleased --dry-run --incremental diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bf4ceb --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ + + +[![Repo][repo-badge]][repo-link] [![Docs][docs-badge]][docs-link] +[![PyPI license][license-badge]][license-link] +[![PyPI version][pypi-badge]][pypi-link] +[![Conda (channel only)][conda-badge]][conda-link] +[![Code style: black][black-badge]][black-link] + + + +[black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg +[black-link]: https://github.com/psf/black +[pypi-badge]: https://img.shields.io/pypi/v/pyproject2conda +[pypi-link]: https://pypi.org/project/pyproject2conda +[docs-badge]: https://img.shields.io/badge/docs-sphinx-informational +[docs-link]: https://pages.nist.gov/pyproject2conda/ +[repo-badge]: https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff +[repo-link]: https://github.com/wpk-nist-gov/pyproject2conda +[conda-badge]:https://img.shields.io/conda/v/wpk-nist/pyproject2conda +[conda-link]: https://anaconda.org/wpk-nist/pyproject2conda +[license-badge]: https://img.shields.io/pypi/l/cmomy?color=informational +[license-link]: https://github.com/wpk-nist-gov/pyproject2conda/blob/main/LICENSE + + + +[poetry2conda]: https://github.com/dojeda/poetry2conda + +# `pyproject2conda` + +A script to convert `pyproject.toml` dependecies to `environemnt.yaml` files. + +## Overview + +The main goal of `pyproject2conda` is to provide a means to keep all basic dependency information, for both `pip` based and `conda` based environments, in `pyproject.toml`. +I often use a mix of pip and conda when developing packages, and in my everyday workflow. Some packages just aren't available on both. +If you use poetry, I'd highly recommend [poetry2conda]. + +## Features + +- Simple comment based syntax to add information to dependencies when creating `environment.yaml` + +## Status + +This package is actively used by the author, but is still very much a work in progress. +Please feel free to create a pull +request for wanted features and suggestions! + +## Quick start + +Use one of the following + +```bash +pip install pyproject2conda +``` + +or + +```bash +conda install -c wpk-nist pyproject2conda +``` + +## Example usage + +Consider the `toml` file [test-pyproject.toml](./tests/test-pyproject.toml). + +```toml +[project] +name = "hello" +requires-python = ">=3.8,<3.11" +dependencies = [ + "athing", # p2c: -p # a comment + "bthing", # p2c: -s bthing-conda + "cthing", # p2c: -c conda-forge + +] +# ... +``` + +Note the comment lines `# p2c:...`. These are special tokens that `pyproject2conda` will analyze. The basic options are: + +``` +Arguments: Additional (conda) packages + +-p --pip Pip install pyproject package on this line. +-s --skip Skip pyproject package on this line. +-c --channel Add channel to pyproject package on this line +``` + +So, if we run the following, we get: + +```bash +$ pyproject2conda create -f test/test-pyproject.toml +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing +``` +Note that other comments can be mixed in. This also works with extras. For example, with the following: + +```toml +# ... +[project.optional-dependencies] +test = [ + "pandas", + "pytest", # p2c: -c conda-forge + +] +# ... +``` + +and running the the following gives: + +```bash +$ pyproject2conda create -f tests/test-pyproject.toml test +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - pip + - pip: + - athing +``` + +`pyproject2conda` can also be used within python: + +```pycon +>>> from pyproject2conda import PyProject2Conda +>>> p = PyProject2Conda.from_path("./tests/test-pyproject.toml") + +# Basic environment +>>> print(p.to_conda_yaml().strip()) +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing + +# Environment with extras +>>> print(p.to_conda_yaml(extras='test').strip()) +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - pip + - pip: + - athing + +``` + + + + + + + + +## License + +This is free software. See [LICENSE][license-link]. + +## Related work + +TBD + +## Contact + +The author can be reached at wpk@nist.gov. + +## Credits + +This package was created with +[Cookiecutter](https://github.com/audreyr/cookiecutter) and the +[wpk-nist-gov/cookiecutter-pypackage](https://github.com/wpk-nist-gov/cookiecutter-pypackage) +Project template forked from +[audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage). diff --git a/changelog.d/README.txt b/changelog.d/README.txt new file mode 100644 index 0000000..4d12638 --- /dev/null +++ b/changelog.d/README.txt @@ -0,0 +1 @@ +This directory will hold the changelog entries managed by scriv diff --git a/changelog.d/templates/auto-changelog/macros.jinja2 b/changelog.d/templates/auto-changelog/macros.jinja2 new file mode 100644 index 0000000..fea2edd --- /dev/null +++ b/changelog.d/templates/auto-changelog/macros.jinja2 @@ -0,0 +1,9 @@ +{% macro section(title, notes) %} +{%- if notes -%} +### {{ title }} + +{% for note in notes -%} +-{% if note.scope %} **{{ note.scope }}**:{% endif %} {{ note.description }} +{% endfor -%} +{% endif -%} +{% endmacro -%} diff --git a/changelog.d/templates/auto-changelog/template.jinja2 b/changelog.d/templates/auto-changelog/template.jinja2 new file mode 100644 index 0000000..a2486dc --- /dev/null +++ b/changelog.d/templates/auto-changelog/template.jinja2 @@ -0,0 +1,18 @@ +{% import 'macros.jinja2' as macros -%} +# {{ changelog.title }} +{% if changelog.description %} +{{ changelog.description }} +{% endif -%} +{%- for release in changelog.releases %} +## {{ release.title }} - {{release.date}} + +{{ macros.section('Added', release.features) -}} +{{- macros.section('Fixed', release.fixes) -}} +{{- macros.section('Performance improvements', release.performance_improvements) -}} +{{- macros.section('Refactored', release.refactorings) -}} +{{- macros.section('Docs', release.docs) -}} +{{- macros.section('Others', release.builds+release.ci+release.chore+release.reverts+release.style_changes+release.tests) -}} +{% if release.diff_url %} +Full set of changes: [`{{release.previous_tag}}...{{release.tag}}`]({{release.diff_url}}) +{% endif -%} +{% endfor -%} diff --git a/changelog.d/templates/new_fragment.md.j2 b/changelog.d/templates/new_fragment.md.j2 new file mode 100644 index 0000000..c65165d --- /dev/null +++ b/changelog.d/templates/new_fragment.md.j2 @@ -0,0 +1,15 @@ + + + +{% for cat in config.categories -%} + +{% endfor -%} diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..8723836 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,52 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SPHINXAUTOBUILD = sphinx-autobuild +SOURCEDIR = . +BUILDDIR = _build + +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) + +ALLSPHINXAUTOOPTS = $(ALLSPHINXOPTS) \ + --ignore "*/$(BUILDDIR)/*" \ + --ignore "*/generated/*" \ + --ignore "*/jupyter_execute/*" \ + --ignore "*/.ipynb_checkpoints/*" + + + +.PHONY: help Makefile clean-all build livehtml showlinks release command +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(ALLSPHINXOPTS) $(O) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(ALLSPHINXOPTS) $(O) + +clean-all: clean + rm -rf $(BUILDDIR)/* + rm -rf generated/* + rm -rf reference/generated/* + +# Alias to html +build: html + +livehtml: + $(SPHINXAUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(ALLSPHINXAUTOOPTS) $(O) + +showlinks: + python -m sphinx.ext.intersphinx $(BUILDDIR)/html/objects.inv + +release_args ?= -m "update docs" -b nist-pages +release: + ghp-import -o -n $(release_args) $(BUILDDIR)/html + +command ?= @echo pass "command=..." +command: + $(command) diff --git a/docs/_static/css/nist-combined.css b/docs/_static/css/nist-combined.css new file mode 100644 index 0000000..98ed0cb --- /dev/null +++ b/docs/_static/css/nist-combined.css @@ -0,0 +1,169 @@ +/** + * @file + * Header and Footer styles + * + */ + +/* Header Styles */ +.nist-header { + background: #000; + font-family: Helvetica, Arial, sans-serif; + padding: 10px 16px 0; + font-size: 16px; +} + +.nist-header__logo-link { + display: inline-block; + height: 35px; +} + +/* For Bootstrap Users - Updated the vertical align property since it interfered with NIST SVG icon. */ +.nist-header__logo-link svg { + vertical-align: inherit; +} + +.nist-header__logo-icon { + fill: #fff; + display: inline-block; + height: 16px; + position: relative; + top: -2px; + margin-right: 2px; +} + +.nist-header__logo-image { + fill: #fff; + display: inline-block; + height: 24px; + width: 90px; +} + +/* Limit main content area width */ +.nist-main { + margin-left: auto; + margin-right: auto; + max-width: 1200px; + padding: 0 16px; + box-sizing: border-box; +} + +/* Make sure body has no margin or padding when using only this header component */ +body { + padding: 0; + margin: 0; +} + +/* Footer styles */ + +.nist-footer { + background: #333333; + position: relative; + z-index: 200; + font-family: Helvetica, Arial, sans-serif; + font-size: 16px; + box-sizing: border-box; +} + +.nist-footer__inner { + margin-left: auto; + margin-right: auto; + max-width: 1200px; + padding: 0 16px; +} + +.nist-footer__inner:after { + content: ""; + display: table; + clear: both; +} + +.nist-footer { + background: #333333; + color: white; + padding: 40px 0px; + position: relative; +} + +.nist-footer a { + color: white; + text-decoration: none; +} + +.nist-footer .nist-footer__logo img { + width: 340px; + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 30px; +} + +.nist-footer__logo { + display: flex; + justify-content: center; + align-items: center; +} + +.nist-footer__menu { + clear: both; + margin-bottom: 10px; +} + +.nist-footer__menu.first { + padding-top: 20px; +} + +.nist-footer__menu ul { + margin: 0; + padding: 0; + list-style: none; + text-align: center; +} + +.nist-footer__menu-item { + display: inline-block; + font-size: 14px; + padding: 0; + margin-left: 0; +} + +.nist-footer__menu-item:after { + content: "|"; + margin-right: 1.6px; + display: inherit; + position: static; + font: inherit; + line-height: inherit; + color: inherit; +} + +.nist-footer__menu-item:last-child:after { + content: none; +} + +.nist-footer__menu-item a { + font-weight: normal; + margin-right: 6px; + white-space: nowrap; + padding: 0.5em 0; + display: inline-block; +} + +/** + * Stick footer to bottom of page + * Source: https://css-tricks.com/couple-takes-sticky-footer/ + */ + +html.nist-footer-bottom, +html.nist-footer-bottom body { + height: 100%; +} +html.nist-footer-bottom body { + display: flex; + flex-direction: column; +} +html.nist-footer-bottom #main { + flex: 1 0 auto; +} +html.nist-footer-bottom .nist-footer { + flex-shrink: 0; +} diff --git a/docs/_static/js/leave_notice.js b/docs/_static/js/leave_notice.js new file mode 100644 index 0000000..bf45e3e --- /dev/null +++ b/docs/_static/js/leave_notice.js @@ -0,0 +1,21 @@ +$(document).ready(function () { + // Mark external (non-nist.gov) A tags with class "external" + //If the adress start with https and ends with nist.gov + var re_nist = new RegExp("^https?://((^/)*.)*nist\\.gov(/|$)"); + //Regex to find address that start with https + var re_absolute_address = new RegExp("^((https?:)?//)"); + $("a").each(function () { + var url = $(this).attr("href"); + if (re_nist.test(url) || !re_absolute_address.test(url)) { + $(this).addClass("local"); + } else { + $(this).addClass("external"); + } + }); + // Add leaveNotice to external A elements + $("a.external").leaveNotice({ + siteName: "pages.nist.gov", + timeOut: 2000, + overlayAlpha: 0.1, + }); +}); diff --git a/docs/_static/js/nist-header-footer.js b/docs/_static/js/nist-header-footer.js new file mode 100644 index 0000000..f51d983 --- /dev/null +++ b/docs/_static/js/nist-header-footer.js @@ -0,0 +1,27 @@ +/** + * @file + * Header and footer scripts + * + */ + +$(document).ready(function () { + $("body").prepend('
'); + $.ajax({ + url: "https://pages.nist.gov/nist-header-footer/boilerplate-header.html", + cache: false, + dataType: "html", + success: function (data) { + $("#nistheadergoeshere").append(data); + }, + }); + + $("body").append('
'); + $.ajax({ + url: "https://pages.nist.gov/nist-header-footer/boilerplate-footer.html", + cache: false, + dataType: "html", + success: function (data) { + $("#nistfootergoeshere").append(data); + }, + }); +}); diff --git a/docs/_templates/autodocsumm/class-noindex.rst b/docs/_templates/autodocsumm/class-noindex.rst new file mode 100644 index 0000000..ccd1b4c --- /dev/null +++ b/docs/_templates/autodocsumm/class-noindex.rst @@ -0,0 +1,13 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + + + +.. autoclass:: {{ objname }} + :autosummary: + :autosummary-nomembers: + :noindex: + :show-inheritance: + :inherited-members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ diff --git a/docs/_templates/autodocsumm/class.rst b/docs/_templates/autodocsumm/class.rst new file mode 100644 index 0000000..dac7da4 --- /dev/null +++ b/docs/_templates/autodocsumm/class.rst @@ -0,0 +1,11 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + + + +.. autoclass:: {{ objname }} + :autosummary: + :show-inheritance: + :inherited-members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ diff --git a/docs/_templates/autodocsumm/module-inherit.rst b/docs/_templates/autodocsumm/module-inherit.rst new file mode 100644 index 0000000..cca5906 --- /dev/null +++ b/docs/_templates/autodocsumm/module-inherit.rst @@ -0,0 +1,21 @@ + + +.. automodule:: {{ fullname }} + :autosummary: + :show-inheritance: + :inherited-members: + :members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ + + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: autodocsumm/module-inherit.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/autodocsumm/module-noindex.rst b/docs/_templates/autodocsumm/module-noindex.rst new file mode 100644 index 0000000..f72d24e --- /dev/null +++ b/docs/_templates/autodocsumm/module-noindex.rst @@ -0,0 +1,21 @@ + +.. automodule:: {{ fullname }} + :autosummary: + :autosummary-nomembers: + :noindex: + :no-members: + :show-inheritance: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ + + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: autodocsumm/module-noindex.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/autodocsumm/module.rst b/docs/_templates/autodocsumm/module.rst new file mode 100644 index 0000000..acce769 --- /dev/null +++ b/docs/_templates/autodocsumm/module.rst @@ -0,0 +1,20 @@ + + +.. automodule:: {{ fullname }} + :autosummary: + :show-inheritance: + :members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ + + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: autodocsumm/module.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst new file mode 100644 index 0000000..b7556eb --- /dev/null +++ b/docs/_templates/autosummary/base.rst @@ -0,0 +1,5 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000..6bcbf9f --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,61 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + + + +.. autoclass:: {{ objname }} + :show-inheritance: + :inherited-members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ + + + + {% block methods %} + {% if methods %} + + + .. rubric:: {{ _('Methods') }} + .. autosummary:: + {% for item in methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + + + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + + {% block all_methods %} + {% if all_methods %} + .. rubric:: {{_('Dunder Methods') }} + + .. autosummary:: + {% for item in all_methods %} + {%- if item in [ + '__call__', + '__add__', + '__iadd__', + '__sub__', + '__isub__', + '__mul__', + '__imul__', + ] %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 0000000..a4bf0f7 --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,63 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ fullname }} + +.. automodule:: {{ fullname }} + :members: + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/class-individual-pages.rst b/docs/_templates/class-individual-pages.rst new file mode 100644 index 0000000..e2b00e7 --- /dev/null +++ b/docs/_templates/class-individual-pages.rst @@ -0,0 +1,61 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :show-inheritance: + + {% block methods %} + .. + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + :toctree: generated/ + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + :toctree: generated/ + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block all_methods %} + {% if all_methods %} + .. rubric:: {{_('Dunder Methods') }} + + .. autosummary:: + :toctree: generated/ + {% for item in all_methods %} + {%- if item in ['__repr__', + '__len__', + '__call__', + '__next__', + '__iter__', + '__getitem__', + '__setitem__', + '__delitem__', + '__add__', + '__iadd__', + '__sub__', + '__isub__', + '__mul__', + '__imul__', + ] %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/_templates/class-single-page.rst b/docs/_templates/class-single-page.rst new file mode 100644 index 0000000..6bcbf9f --- /dev/null +++ b/docs/_templates/class-single-page.rst @@ -0,0 +1,61 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + + + +.. autoclass:: {{ objname }} + :show-inheritance: + :inherited-members: + :special-members: __call__, __add__, __iadd__, __sub__, __isub__, __mul__, __imul__ + + + + {% block methods %} + {% if methods %} + + + .. rubric:: {{ _('Methods') }} + .. autosummary:: + {% for item in methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + + + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + + {% block all_methods %} + {% if all_methods %} + .. rubric:: {{_('Dunder Methods') }} + + .. autosummary:: + {% for item in all_methods %} + {%- if item in [ + '__call__', + '__add__', + '__iadd__', + '__sub__', + '__isub__', + '__mul__', + '__imul__', + ] %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst new file mode 100644 index 0000000..cfca0ea --- /dev/null +++ b/docs/_templates/custom-module-template.rst @@ -0,0 +1,65 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/module-custom-imported.rst b/docs/_templates/module-custom-imported.rst new file mode 100644 index 0000000..2864bf4 --- /dev/null +++ b/docs/_templates/module-custom-imported.rst @@ -0,0 +1,67 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + :members: + :imported-members: + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: module-custom.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/module-custom.rst b/docs/_templates/module-custom.rst new file mode 100644 index 0000000..b11fa04 --- /dev/null +++ b/docs/_templates/module-custom.rst @@ -0,0 +1,65 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: module-custom.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/module-single.rst b/docs/_templates/module-single.rst new file mode 100644 index 0000000..ec361cb --- /dev/null +++ b/docs/_templates/module-single.rst @@ -0,0 +1,66 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + +{% block attributes %} +{% if attributes %} +.. rubric:: Module attributes + +.. autosummary:: + :toctree: +{% for item in attributes %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block functions %} +{% if functions %} +.. rubric:: {{ _('Functions') }} + +.. autosummary:: + :toctree: + :nosignatures: +{% for item in functions %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block classes %} +{% if classes %} +.. rubric:: {{ _('Classes') }} + +.. autosummary:: + :toctree: + :template: custom-class.rst + :nosignatures: +{% for item in classes %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block exceptions %} +{% if exceptions %} +.. rubric:: {{ _('Exceptions') }} + +.. autosummary:: + :toctree: +{% for item in exceptions %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: module-single.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/module-template.rst b/docs/_templates/module-template.rst new file mode 100644 index 0000000..7afca5c --- /dev/null +++ b/docs/_templates/module-template.rst @@ -0,0 +1,65 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/authors.md b/docs/authors.md new file mode 100644 index 0000000..2f31832 --- /dev/null +++ b/docs/authors.md @@ -0,0 +1,5 @@ + + +```{include} ../AUTHORS.md + +``` diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..cf85c46 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ + + +```{include} ../CHANGELOG.md + +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..70e9951 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python +# +# python_boilerplate documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 13:47:02 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another +# directory, add these directories to sys.path here. If the directory is +# relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +# +"""Build docs.""" +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +import pyproject2conda + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "autodocsumm", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.autosectionlabel", + "IPython.sphinxext.ipython_directive", + "IPython.sphinxext.ipython_console_highlighting", + # "nbsphinx", + ## easier external links + # "sphinx.ext.extlinks", + ## view source code on created page + # "sphinx.ext.viewcode", + ## view source code on github + "sphinx.ext.linkcode", + ## add copy button + "sphinx_copybutton", + ## redirect stuff? + # "sphinxext.rediraffe", + ## pretty things up? + # "sphinx_design" + ## myst stuff + "myst_nb", +] + +nitpicky = True +autosectionlabel_prefix_document = True + +# -- myst stuff --------------------------------------------------------- +myst_enable_extensions = [ + "dollarmath", + "amsmath", + "deflist", + "fieldlist", + "html_admonition", + "html_image", + "colon_fence", + "smartquotes", + "replacements", + # "linkify", + "strikethrough", + "substitution", + "tasklist", + # "attrs_inline", + # "attrs_block", +] + + +myst_heading_anchors = 2 +myst_footnote_transition = True +myst_dmath_double_inline = True +myst_enable_checkboxes = True +myst_substitutions = { + "role": "[role](#syntax/roles)", + "directive": "[directive](#syntax/directives)", +} +# myst_enable_extensions = [ +# "dollarmath", +# "amsmath", +# "deflist", +# # "html_admonition", +# "html_image", +# "colon_fence", +# # "smartquotes", +# # "replacements", +# # "linkify", +# # "substitution", +# "attrs_inline", +# "attrs_block", +# ] + +myst_url_schemes = ("http", "https", "mailto") + +nb_execution_mode = "cache" +# nb_execution_mode = "auto" + +# set the kernel name +nb_kernel_rgx_aliases = {"pyproject2conda.*": "python3", "conda.*": "python3"} + +nb_execution_allow_errors = True + +# - top level variables -------------------------------------------------------- +# set github_username variable to be subbed later. +# this makes it easy to switch from wpk -> usnistgov later +github_username = "wpk-nist-gov" + +html_context = { + "github_user": "wpk-nist-gov", + "github_repo": "pyproject2conda", + "github_version": "main", + "doc_path": "docs", +} + +# -- python3 --------------------------------------------------------------- +autosummary_generate = True +# autosummary_generate = False +autodoc_member_order = "bysource" + +# autoclass_content = "both" # include both class docstring and __init__ +autodoc_default_flags = [ + # Make sure that any autodoc declarations show the right members + "members", + "inherited-members", + "private-members", + "show-inheritance", +] +autodoc_typehints = "none" + +# -- napoleon ------------------------------------------------------------------ +napoleon_google_docstring = False +napoleon_numpy_docstring = True + +napoleon_use_param = False +napoleon_use_rtype = False +napoleon_preprocess_types = True +napoleon_type_aliases = { + # general terms + "sequence": ":term:`sequence`", + "iterable": ":term:`iterable`", + "callable": ":py:func:`callable`", + "dict_like": ":term:`dict-like `", + "dict-like": ":term:`dict-like `", + "path-like": ":term:`path-like `", + "mapping": ":term:`mapping`", + "hashable": ":term:`hashable`", + "file-like": ":term:`file-like `", + # special terms + # "same type as caller": "*same type as caller*", # does not work, yet + # "same type as values": "*same type as values*", # does not work, yet + # stdlib type aliases + "MutableMapping": "~collections.abc.MutableMapping", + "sys.stdout": ":obj:`sys.stdout`", + "timedelta": "~datetime.timedelta", + "string": ":class:`string `", + # numpy terms + "array_like": ":term:`array_like`", + "array-like": ":term:`array-like `", + "scalar": ":term:`scalar`", + "array": ":term:`array`", + # matplotlib terms + "color-like": ":py:func:`color-like `", + "matplotlib colormap name": ":doc:`matplotlib colormap name `", + "matplotlib axes object": ":py:class:`matplotlib axes object `", + "colormap": ":py:class:`colormap `", + # objects without namespace: xarray + "DataArray": "~xarray.DataArray", + "Dataset": "~xarray.Dataset", + "Variable": "~xarray.Variable", + "DatasetGroupBy": "~xarray.core.groupby.DatasetGroupBy", + "DataArrayGroupBy": "~xarray.core.groupby.DataArrayGroupBy", + "CentralMoments": "~cmomy.CentralMoments", + "xCentralMoments": "~cmomy.xCentralMoments", + # objects without namespace: numpy + "ndarray": "~numpy.ndarray", + "MaskedArray": "~numpy.ma.MaskedArray", + "dtype": "~numpy.dtype", + "ComplexWarning": "~numpy.ComplexWarning", + # objects without namespace: pandas + "Index": "~pandas.Index", + "MultiIndex": "~pandas.MultiIndex", + "CategoricalIndex": "~pandas.CategoricalIndex", + "TimedeltaIndex": "~pandas.TimedeltaIndex", + "DatetimeIndex": "~pandas.DatetimeIndex", + "Series": "~pandas.Series", + "DataFrame": "~pandas.DataFrame", + "Categorical": "~pandas.Categorical", + "Path": "~~pathlib.Path", + # objects with abbreviated namespace (from pandas) + "pd.Index": "~pandas.Index", + "pd.NaT": "~pandas.NaT", +} + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = { + ".rst": "restructuredtext", + ".ipynb": "myst-nb", + ".myst": "myst-nb", +} + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "pyproject2conda" +copyright = "2023, William P. Krekelberg" +author = "William P. Krekelberg" + +# The version info for the project you're documenting, acts as replacement +# for |version| and |release|, also used in various other places throughout +# the built documents. +# +# The short X.Y version. +# versioning with scm with editable install has issues. +# instead, try to use scm if available. +try: + from setuptools_scm import get_version + + version = get_version(root="..", relative_to=__file__) + release = version +except ImportError: + version = pyproject2conda.__version__ + # The full version, including alpha/beta/rc tags. + release = pyproject2conda.__version__ + +# if always want to print "latest" +# release = "latest" +# version = "latest" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +html_theme = "sphinx_book_theme" + +html_theme_options = dict( + # analytics_id='' this is configured in rtfd.io + # canonical_url="", + repository_url=f"https://github.com/{github_username}/pyproject2conda", + repository_branch=html_context["github_version"], + path_to_docs=html_context["doc_path"], + # use_edit_page_button=True, + use_repository_button=True, + use_issues_button=True, + home_page_in_toc=True, + show_toc_level=3, + show_navbar_depth=0, +) +# handle nist css/js from here. +html_css_files = [ + # "css/nist-combined.css", + "https://pages.nist.gov/nist-header-footer/css/nist-combined.css", + "https://pages.nist.gov/leaveNotice/css/jquery.leaveNotice.css", +] + +html_js_files = [ + "https://code.jquery.com/jquery-3.6.2.min.js", + "https://pages.nist.gov/nist-header-footer/js/nist-header-footer.js", + # "js/nist-header-footer.js", + "https://pages.nist.gov/leaveNotice/js/jquery.leaveNotice-nist.min.js", + "js/leave_notice.js", + # google stuff: + ( + "https://dap.digitalgov.gov/Universal-Federated-Analytics-Min.js?agency=NIST&subagency=github&pua=UA-66610693-1&yt=true&exts=ppsx,pps,f90,sch,rtf,wrl,txz,m1v,xlsm,msi,xsd,f,tif,eps,mpg,xml,pl,xlt,c", + {"async": "async", "id": "_fed_au_ua_tag", "type": "text/javascript"}, + ), +] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + + +# Sometimes the savefig directory doesn't exist and needs to be created +# https://github.com/ipython/ipython/issues/8733 +# becomes obsolete when we can pin ipython>=5.2; see ci/requirements/doc.yml +ipython_savefig_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "_build", "html", "_static" +) +if not os.path.exists(ipython_savefig_dir): + os.makedirs(ipython_savefig_dir) + + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +today_fmt = "%Y-%m-%d" +html_last_updated_fmt = today_fmt + + +# -- Options for HTMLHelp output --------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "pyproject2condadoc" + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "pyproject2conda.tex", + "pyproject2conda Documentation", + "William P. Krekelberg", + "manual", + ), +] + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + master_doc, + "pyproject2conda", + "pyproject2conda Documentation", + [author], + 1, + ), +] + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "pyproject2conda", + "pyproject2conda Documentation", + author, + "pyproject2conda", + "One line description of project.", + "Miscellaneous", + ), +] + +# -- user defined stuff ------------------------------------------------ + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "numba": ("https://numba.pydata.org/numba-doc/latest", None), + # "matplotlib": ("https://matplotlib.org", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "dask": ("https://docs.dask.org/en/latest", None), + "cftime": ("https://unidata.github.io/cftime", None), + "sparse": ("https://sparse.pydata.org/en/latest/", None), + "xarray": ("https://docs.xarray.dev/en/stable/", None), +} + +linkcheck_ignore = ["https://doi.org/"] + + +# based on numpy doc/source/conf.py +def linkcode_resolve(domain, info): + """Determine the URL corresponding to Python object""" + import inspect + from operator import attrgetter + + if domain != "py": + return None + + parent_name, *sub_parts = info["module"].split(".") + parent_mod = sys.modules.get(parent_name) + + try: + if len(sub_parts) > 0: + sub_name = ".".join(sub_parts) + obj = attrgetter(sub_name)(parent_mod) + else: + obj = parent_mod + + # get fullname + obj = attrgetter(info["fullname"])(obj) + + except AttributeError: + return None + + try: + fn = inspect.getsourcefile(inspect.unwrap(obj)) + except TypeError: + fn = None + if not fn: + return None + + try: + source, lineno = inspect.getsourcelines(obj) + except OSError: + lineno = None + + if lineno: + linespec = f"#L{lineno}-L{lineno + len(source) - 1}" + else: + linespec = "" + + fn = os.path.relpath(fn, start=os.path.dirname(pyproject2conda.__file__)) + + return f"https://github.com/{github_username}/pyproject2conda/blob/{html_context['github_version']}/src/pyproject2conda/{fn}{linespec}" + + +# only set spelling stuff if installed: +try: + import sphinxcontrib.spelling # noqa: F401 + + extensions += ["sphinxcontrib.spelling"] + spelling_word_list_filename = "spelling_wordlist.txt" + +except ImportError: + pass diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..d35b222 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,5 @@ + + +```{include} ../CONTRIBUTING.md + +``` diff --git a/docs/examples/example-usage.md b/docs/examples/example-usage.md new file mode 100644 index 0000000..a162854 --- /dev/null +++ b/docs/examples/example-usage.md @@ -0,0 +1,44 @@ +--- +jupytext: + text_representation: + format_name: myst +kernelspec: + display_name: Python 3 + name: python3 +--- + +# Usage + +An example for using ipython directives or jupytext + +## jupytext + +```{code-cell} ipython3 + +import pyproject2conda + +a = 1 +``` + +```{code-cell} ipython3 + +print(a) +``` + +## ipython directive + +To use Python Boilerplate in a project: + +```python +import pyproject2conda +``` + +see, e.g., {py:meth}`~pyproject2conda.core.another_func` + +ipython example... + +```{eval-rst} +.. ipython:: python + + import pyproject2conda +``` diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..c0486b3 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,9 @@ +# User guide + +```{toctree} +:maxdepth: 2 + +example-usage +usage/demo + +``` diff --git a/docs/examples/usage/demo.ipynb b/docs/examples/usage/demo.ipynb new file mode 100644 index 0000000..8f2aab3 --- /dev/null +++ b/docs/examples/usage/demo.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A demo notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "sys.version" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + } + ], + "source": [ + "print(1 + 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "This is a note!\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "see, e.g., [another_func](generated/pyproject2conda.another_func.rst)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python [conda env:dev38]", + "language": "python", + "name": "conda-env-dev38-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a31aec1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,19 @@ + + +```{include} ../README.md +:end-before: +``` + +```{toctree} +:maxdepth: 1 + + +installation +examples/index +reference/index +license +contributing +authors +changelog +navigation +``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..fefd6b9 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,47 @@ +# Installation + +## Stable release + +To install pyproject2conda, run this command in your terminal: + +```bash +pip install pyproject2conda +``` + +or + +```bash +conda install -c wpk-nist pyproject2conda +``` + +This is the preferred method to install pyproject2conda, as it +will always install the most recent stable release. + +## From sources + +The sources for pyproject2conda can be downloaded from the +[Github repo]. + +You can either clone the public repository: + +```bash +git clone git://github.com/wpk-nist-gov/pyproject2conda.git +``` + +Once you have a copy of the source, you can install it with: + +```bash +pip install . +``` + +To install dependencies with conda/mamba, use: + +```bash +conda env create [-n {name}] -f environment.yaml +conda activate {name} +pip install [-e] --no-deps . +``` + +where options in brackets are options (for environment name, and editable install, repectively). + +[github repo]: https://github.com/wpk-nist-gov/pyproject2conda diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..996a5f8 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,5 @@ +# License + +```{include} ../LICENSE + +``` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..9d83ed9 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=cmomy + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/navigation.md b/docs/navigation.md new file mode 100644 index 0000000..277790e --- /dev/null +++ b/docs/navigation.md @@ -0,0 +1,13 @@ +# Indices and tables + +```{toctree} + +genindex +modindex +``` + + + + + + diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..79590d6 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,11 @@ +# API Reference + +```{eval-rst} +.. currentmodule:: pyproject2conda + +.. autosummary:: + :toctree: generated/ + + a_function + another_func +``` diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..e69de29 diff --git a/environment.yaml b/environment.yaml new file mode 100644 index 0000000..21d9a5f --- /dev/null +++ b/environment.yaml @@ -0,0 +1,10 @@ +name: pyproject2conda-env +channels: + - conda-forge + - wpk-nist +dependencies: + - python>=3.8 + - tomli + - ruamel.yaml + - tomlkit + # cli- Click diff --git a/environment/dev-extras.yaml b/environment/dev-extras.yaml new file mode 100644 index 0000000..2cff9ae --- /dev/null +++ b/environment/dev-extras.yaml @@ -0,0 +1,55 @@ +dependencies: + # numerics/tests + # - matplotlib + # - pandas + # development + # - isort + # - black + # - blackdoc + # - flake8 + # testing + - pytest + - pytest-cov + - pytest-xdist + # - tox-conda + # specials + # - pre-commit + # repl + - ipython + - ipykernel + # build + - setuptools-scm + # - setuptools + # - twine + # - setuptools_scm_git_archive + # - build + # conda specific + # - conda-build + # - anaconda-client + # - greyskull + # doc stuff + # - sphinx + # - sphinx_rtd_theme + # - recommonmark # if want markdown + # - nbsphinx # if want notebooks + # - sphinxcontrib-spelling + # - sphinx-toolbox + # lsp stuff + # - autoflake + # - pyls-mypy + # - pyls-black + # - pyls-isort + # mypy + # - mypy + # - pytest-mypy + # # Monkeytype: autocreate type hints + # - MonkeyType + - pip + - pip: + - pytest-accept + # - mypy-extensions + # - pytest-monkeytype + # - flake8-mypy + # - attr-utils + # spelling? + # - pyenchant diff --git a/environment/dev.yaml b/environment/dev.yaml new file mode 100644 index 0000000..349b5a7 --- /dev/null +++ b/environment/dev.yaml @@ -0,0 +1,18 @@ +channels: + - conda-forge + - wpk-nist +dependencies: + - ipykernel + - ipython + - pip + - pytest + - pytest-cov + - pytest-xdist + - python>=3.8 + - ruamel.yaml + - setuptools-scm + - tomli + - tomlkit + - pip: + - pytest-accept +name: pyproject2conda-env diff --git a/environment/dist-conda.yaml b/environment/dist-conda.yaml new file mode 100644 index 0000000..51b50da --- /dev/null +++ b/environment/dist-conda.yaml @@ -0,0 +1,7 @@ +dependencies: + - anaconda-client + - grayskull + - conda-build + - conda-verify + - boa + - setuptools-scm diff --git a/environment/dist-pypi.yaml b/environment/dist-pypi.yaml new file mode 100644 index 0000000..ca53c88 --- /dev/null +++ b/environment/dist-pypi.yaml @@ -0,0 +1,5 @@ +dependencies: + - setuptools>=61.2 + - setuptools-scm>=7.0 + - twine + - build diff --git a/environment/docs-extras.yaml b/environment/docs-extras.yaml new file mode 100644 index 0000000..2a6d3eb --- /dev/null +++ b/environment/docs-extras.yaml @@ -0,0 +1,28 @@ +channels: + - conda-forge +dependencies: + - setuptools-scm + - ipython + ## package deps + # - matplotlib + # - pandas + - pip + - pip: + - pyenchant + # TODO: something goes wonky with sphinx-book-theme and higher versions of sphinx + - ghp-import + - sphinx + ## themes + - sphinx-book-theme + ## optionals + # sphinx-design + - sphinx-copybutton + - sphinxcontrib-spelling + # sphinxext-rediraffe + ## autobuild + - sphinx-autobuild + ## myst + # myst-parser + - myst-nb + ## others + - autodocsumm diff --git a/environment/docs.yaml b/environment/docs.yaml new file mode 100644 index 0000000..d8d68af --- /dev/null +++ b/environment/docs.yaml @@ -0,0 +1,22 @@ +channels: + - conda-forge + - wpk-nist +dependencies: + - ipython + - pip + - python>=3.8 + - ruamel.yaml + - setuptools-scm + - tomli + - tomlkit + - pip: + - autodocsumm + - ghp-import + - myst-nb + - pyenchant + - sphinx + - sphinx-autobuild + - sphinx-book-theme + - sphinx-copybutton + - sphinxcontrib-spelling +name: pyproject2conda-env diff --git a/environment/lint-extras.yaml b/environment/lint-extras.yaml new file mode 100644 index 0000000..728cfdd --- /dev/null +++ b/environment/lint-extras.yaml @@ -0,0 +1,4 @@ +dependencies: + - mypy + ## stubs + # - pandas-stubs diff --git a/environment/lint.yaml b/environment/lint.yaml new file mode 100644 index 0000000..5b62bf5 --- /dev/null +++ b/environment/lint.yaml @@ -0,0 +1,11 @@ +channels: + - conda-forge + - wpk-nist +dependencies: + - mypy + - pytest + - python>=3.8 + - ruamel.yaml + - tomli + - tomlkit +name: pyproject2conda-env diff --git a/environment/test-extras.yaml b/environment/test-extras.yaml new file mode 100644 index 0000000..473a9ad --- /dev/null +++ b/environment/test-extras.yaml @@ -0,0 +1,2 @@ +dependencies: + - pytest diff --git a/environment/test.yaml b/environment/test.yaml new file mode 100644 index 0000000..a42e04c --- /dev/null +++ b/environment/test.yaml @@ -0,0 +1,10 @@ +channels: + - conda-forge + - wpk-nist +dependencies: + - pytest + - python>=3.8 + - ruamel.yaml + - tomli + - tomlkit +name: pyproject2conda-env diff --git a/environment/tools.yaml b/environment/tools.yaml new file mode 100644 index 0000000..5efd308 --- /dev/null +++ b/environment/tools.yaml @@ -0,0 +1,11 @@ +# Additional tools for development. It is recommended to install these with +# condax/pipx, but you can install them in this environment if you'd prefer. +dependencies: + - pre-commit + - tox + - tox-conda + - cruft + - conda-merge + - scriv + # this isn't needed, but can be helpful + # - commitizen diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..8fe3470 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +# Example notebooks + +Landing spot for example notebooks. Note that notebooks in `usage` are (often) +used in the generated documentation. If the examples have a large file size, +consider spinning them off as a a submodule. diff --git a/examples/usage/demo.ipynb b/examples/usage/demo.ipynb new file mode 100644 index 0000000..8f2aab3 --- /dev/null +++ b/examples/usage/demo.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A demo notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "sys.version" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + } + ], + "source": [ + "print(1 + 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "This is a note!\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "see, e.g., [another_func](generated/pyproject2conda.another_func.rst)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python [conda env:dev38]", + "language": "python", + "name": "conda-env-dev38-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..82e2b83 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,276 @@ +[build-system] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=7.0"] +build-backend = "setuptools.build_meta" + + +[project] +name = "pyproject2conda" +authors = [{ name = "William P. Krekelberg", email = "wpk@nist.gov" }] +license = { text = "NIST-PD" } +description = "A script to convert a Python project declared on a pyproject.toml to a conda environment." +# if using markdown +# long_description_content_type = text/markdown +keywords = ["pyproject2conda"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "License :: Public Domain", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +dynamic = ["readme", "version"] +requires-python = ">=3.8" +dependencies = [ + "tomli", + "ruamel.yaml", + "tomlkit", + "rich-click", + "click-default-group", + # "typing-extensions; python<3.10", +] # additional packages + +[project.urls] +homepage = "https://github.com/wpk-nist-gov/pyproject2conda" +documentation = "https://pages.nist.gov/pyproject2conda/" + +[project.optional-dependencies] +test = ["pytest", "pytest-cov", "pytest-xdist", "pytest-sugar"] + + +# dev = [] +# docs = [] +[project.scripts] +pyproject2conda = "pyproject2conda.cli:app" + +## grayskull still messes some things up, but use scripts/recipe-append.sh for this +[tool.setuptools] +zip-safe = true # if using mypy, must be False +include-package-data = true +license-files = ["LICENSE"] +[tool.setuptools.packages.find] +namespaces = true +where = ["src"] +## include = [] +## exclude = [] +## +[tool.setuptools.dynamic] +readme = { file = [ + "README.md", + "CHANGELOG.md", + "LICENSE" +], content-type = "text/markdown" } + +[tool.setuptools_scm] +fallback_version = "999" + +[tool.aliases] +test = "pytest" + +[tool.pytest.ini_options] +addopts = "--verbose --doctest-modules --doctest-glob='*.md'" +testpaths = ["tests", "README.md"] + +[tool.isort] +profile = "black" +skip_gitignore = true +known_first_party = ["pyproject2conda"] + +[tool.ruff] +fix = true +line-length = 88 +update-check = false +target-version = "py38" +select = [ + # pyflakes + "F", + # pycodestyle + "E", + "W", + # isort + "I", + # pyupgrade + "UP", + # pydocstyle + "D", + # # flake8-2020 + "YTT", + # # flake8-bugbear + # "B", + # flake8-quotes + "Q", + # # pylint + # "PLE", "PLR", "PLW", + # # misc lints + "PIE", + # # tidy imports + "TID", + # # implicit string concatenation + # "ISC", + # # type-checking imports + "TCH", + +] +# Allow autofix for all enabled rules (when `--fix`) is provided. +# fixable = ["A", "B", "C", "D", "E", "F", "..."] +unfixable = [] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "tests/", + "src/pyproject2conda/tests", +] +ignore = [ + # # whitespace before ':' - doesn't work well with black + # "E203", + # module level import not at top of file + "E402", + # line too long - let black worry about that + "E501", + # do not assign a lambda expression, use a def + "E731", + # # line break before binary operator + # "W503", + # allow black line after docstring + "D202", + "D105", + "D205", + # this leads to errors with placing titles in module + # docstrings + "D400", + "D401", + "D415", + "D102", + "D103", + # these are useful, but too many errors + # due to use of docfiller + "D417", + "D107", + "D203", + "D212", + # Allow relative imports + "TID252", +] +per-file-ignores = { } +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.isort] +known-first-party = ["pyproject2conda"] + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + +[tool.nbqa.addopts] +ruff = ["--fix", "--extend-ignore=D100,B018"] + +[tool.flake8] +docstring-convention = "numpy" +ignore = [ + # # whitespace before ':' - doesn't work well with black + "E203", + # module level import not at top of file + "E402", + # line too long - let black worry about that + "E501", + # do not assign a lambda expression, use a def + "E731", + # # line break before binary operator + "W503", + # allow black line after docstring + "D202", + "D105", + "D205", + # this leads to errors with placing titles in module + # docstrings + "D400", + "D401", + "D415", + "D102", + "D103", + # these are useful, but too many errors + # due to use of docfiller + "D417", + "D107", + "D203", + "D212", +] + +[tool.scriv] +format = "md" +md_header_level = "2" +new_fragment_template = "file: changelog.d/templates/new_fragment.md.j2" + +[tool.commitizen] +use_shortcuts = true + +[tool.cruft] + +[tool.mypy] +files = ["src/pyproject2conda"] +show_error_codes = true +warn_unused_ignores = true +warn_return_any = true +warn_unused_configs = true +exclude = [".eggs", ".tox", "doc", "docs"] +check_untyped_defs = true + +[[tool.mypy.overrides]] +ignore_missing_imports = true +module = [] + +[[tool.mypy.overrides]] +ignore_errors = true +module = [] + +[tool.pyright] +include = ["src", "tests"] +exclude = ["**/__pycache__", ".tox/**", "**/.mypy_cache"] +pythonVersion = "3.10" +typeCheckingMode = "basic" +# enable subset of "strict" +reportDuplicateImport = true +reportInvalidStubStatement = true +reportOverlappingOverload = true +reportPropertyTypeMismatch = true +reportUntypedClassDecorator = true +reportUntypedFunctionDecorator = true +reportUntypedNamedTuple = true +reportUnusedImport = true +# disable subset of "basic" +reportGeneralTypeIssues = false +reportMissingModuleSource = false +reportOptionalCall = false +reportOptionalIterable = false +reportOptionalMemberAccess = false +reportOptionalOperand = false +reportOptionalSubscript = false +reportPrivateImportUsage = false +reportUnboundVariable = false + +[tool.pytype] +inputs = ["src", "tests"] diff --git a/scripts/dist-conda.mk b/scripts/dist-conda.mk new file mode 100644 index 0000000..2436794 --- /dev/null +++ b/scripts/dist-conda.mk @@ -0,0 +1,32 @@ + +project_name?=pyproject2conda +sdist_path?=$(project_name) + +.PHONY: help clean-recipe clean-build grayskull recipe-append recipe build command + +help: + @echo Makefile for building conda dist +clean-recipe: + rm -rf dist-conda/$(project_name) + +clean-build: + rm -rf build + +# by default, only use a few sections +grayskull_args ?= --maintainers wpk-nist-gov --sections package source build requirements +grayskull: clean-recipe + grayskull pypi $(sdist_path) $(grayskull_args) -o dist-conda + +# append the rest +recipe_base_path ?= dist-conda/$(project_name)/meta.yaml +recipe_append_path ?= .recipe-append.yaml +recipe-append: + bash scripts/recipe-append.sh $(recipe_base_path) $(recipe_append_path) + +recipe: grayskull recipe-append + +build: clean-build + conda mambabuild --output-folder=dist-conda/build --no-anaconda-upload dist-conda + +command: + $(command) diff --git a/scripts/dist-pypi.mk b/scripts/dist-pypi.mk new file mode 100644 index 0000000..b358db3 --- /dev/null +++ b/scripts/dist-pypi.mk @@ -0,0 +1,18 @@ +.PHONY: help clean build release testrelease command +help: + @echo Makefile for building pypi dist +clean: + -rm -rf dist/* + +build: clean + python -m build --outdir dist/ + +testrelease: + twine upload --repository testpypi dist/* + +release: + twine upload dist/* + +command?= @echo "pass command=..." +command: + $(command) diff --git a/scripts/docs-examples-symlinks.sh b/scripts/docs-examples-symlinks.sh new file mode 100644 index 0000000..ce6b59d --- /dev/null +++ b/scripts/docs-examples-symlinks.sh @@ -0,0 +1,52 @@ +# this creates symlinks to filees in /examples/ directory +# you can link to files using (for example) +# ```{eval-rst} +# usage/notebook +# ``` +# the script will creat a link +# /docs/examples/usage/notebook.ipynb -> /examples/usage/notebook.ipynb +# + +exts=(ipynb md) + +rm -rf docs/examples/usage +for path in $(cat docs/examples/*.md | grep '^usage/'); do + + target="examples/"${path} + name=$(basename $target) + + if [ -f $target ]; then + # has extension + + base=${name%.*} + ext=${name##*.} + + else + # no extension. Try to add one + for ext in ${exts[@]}; do + tmp=${target}.${ext} + if [ -f "${tmp}" ] ; then + base=$name + target=$tmp + break + fi + done + fi + + + new_dir=docs/examples/$(dirname $path) + mkdir -p $new_dir + + total_target=$(realpath --relative-to=${new_dir} $target) + + # echo "target $target" + # echo "base $base" + # echo "ext $ext" + echo "target $total_target" + echo "new_dir $new_dir" + echo "" + + + ln -s $total_target $new_dir + +done diff --git a/scripts/lint.mk b/scripts/lint.mk new file mode 100644 index 0000000..06d3c10 --- /dev/null +++ b/scripts/lint.mk @@ -0,0 +1,19 @@ +.PHONY: help mypy pyright pytype all + +help: + @echo Makefile for linting + +mypy: + -mypy --color-output $(mypy_args) + +pyright: + -pyright $(pyright_args) + +pytype: + -pytype $(pytype_args) + +all: mypy pyright pytype + +command?= @echo 'pass command=...' +command: + $(command) diff --git a/scripts/recipe-append.sh b/scripts/recipe-append.sh new file mode 100644 index 0000000..f8b8c7c --- /dev/null +++ b/scripts/recipe-append.sh @@ -0,0 +1,34 @@ +# This is to fix issues using grayskull with pyproject.toml only projects +# We fall back to using grayskull to create the majority of the recipe +# but add in the final sections +# Edit .recipe-append.yaml +if [ $# -lt 2 ]; then + echo "need recipe_base_path, recipe_append_path" + exit 1 +fi + +base_path=$1 +append_path=$2 + + +if [ ! -f $base_path ]; then + echo "no $base_path" + exit 1 +fi + +if [ ! -f $append_path ]; then + echo "no $append_path" + exit +fi + + +tmp_file=$(mktemp) +cp $base_path $tmp_file + +echo "" >> $tmp_file + +cat $append_path >> $tmp_file + +mv $tmp_file $base_path + +cat $base_path diff --git a/scripts/run-prettier.sh b/scripts/run-prettier.sh new file mode 100644 index 0000000..b11d709 --- /dev/null +++ b/scripts/run-prettier.sh @@ -0,0 +1,2 @@ +# interface to pre-commit prettier +pre-commit run prettier --files $@ &> /dev/null || true diff --git a/scripts/tox-ipykernel-display-name.sh b/scripts/tox-ipykernel-display-name.sh new file mode 100644 index 0000000..57350ad --- /dev/null +++ b/scripts/tox-ipykernel-display-name.sh @@ -0,0 +1,22 @@ +# This adjusts the display name for ipykernels +# As we (assume) use of nb_conda_kernels, this will +# make kernels findable. + +if [ $# -lt 1 ]; then + echo "Usage: $0 display_name_base" + exit 1 +fi + +base=$1 + +eval "$(conda shell.bash hook)" + +for path in .tox/* ; do + + suffix=$(basename $path) + display_name=${base}-${suffix} + + echo $x $display_name + conda activate $path + conda activate $path && python -m ipykernel install --sys-prefix --display-name "$display_name" +done diff --git a/src/pyproject2conda/__init__.py b/src/pyproject2conda/__init__.py new file mode 100644 index 0000000..8b32a88 --- /dev/null +++ b/src/pyproject2conda/__init__.py @@ -0,0 +1,29 @@ +""" +Top level API (:mod:`pyproject2conda`) +====================================== +""" + + +from .parser import PyProject2Conda + +# updated versioning scheme +try: + from importlib.metadata import version as _version +except ImportError: + from importlib_metadata import version as _version # type: ignore[no-redef] + +try: + __version__ = _version("pyproject2conda") +except Exception: + # Local copy or not installed with setuptools. + # Disable minimum version checks on downstream libraries. + __version__ = "999" + + +__author__ = """William P. Krekelberg""" +__email__ = "wpk@nist.gov" + + +__all__ = [ + "__version__", +] diff --git a/src/pyproject2conda/cli.py b/src/pyproject2conda/cli.py new file mode 100644 index 0000000..9825fe3 --- /dev/null +++ b/src/pyproject2conda/cli.py @@ -0,0 +1,151 @@ +"""Console script for pyproject2conda.""" + + +import rich_click as click + +from pyproject2conda.parser import PyProject2Conda + +FILE_CLI = click.option("-f","--file","filename", type=click.Path(), default="pyproject.toml", help="input pyproject.toml file") +EXTRAS_CLI = click.argument("extras", type=str, nargs=-1)#, help="extra depenedencies") +ISOLATED_CLI = click.argument("isolated", type=str, nargs=-1, required=True)#, help="Isolated dependencies (under [tool.pyproject2conda.isolated-dependencies])") +CHANNEL_CLI = click.option("-c","--channel", "channel", type=str, default=None, multiple=True, help="conda channel. Can be specified multiple times. Overrides [tool.pyproject2conda.channels]") +NAME_CLI = click.option("-n","--name", "name", type=str, default=None, help="Name of conda env") +OUTPUT_CLI = click.option("-o","--output", "output", type=click.Path(), default=None, help="File to output results") + +VERBOSE_CLI = click.option("-v", "--verbose", "verbose", is_flag=True, default=False) + + +@click.group() +def app( +): + pass + + +@app.command() +@FILE_CLI +@VERBOSE_CLI +def list( + filename, + verbose, +): + + if verbose: + click.echo(f"filename: {filename}") + + d = PyProject2Conda.from_path(filename) + click.echo(f"extras : {d.list_extras()}") + click.echo(f"isolated: {d.list_isolated()}") + + +@app.command() +@EXTRAS_CLI +@CHANNEL_CLI +@FILE_CLI +@NAME_CLI +@OUTPUT_CLI +def create( + extras, + channel, + filename, + name, + output, +): + """ + Create yaml file from dependencies and optional-dependencies. + """ + + if not channel: + channel = None + d = PyProject2Conda.from_path(filename) + s = d.to_conda_yaml(extras=extras, channels=channel, name=name, stream=output) + if not output: + click.echo(s, nl=False) + + +@app.command() +@ISOLATED_CLI +@CHANNEL_CLI +@FILE_CLI +@NAME_CLI +@OUTPUT_CLI +def isolated( + isolated, + channel, + filename, + name, + output, +): + """ + Create yaml file from [tool.pyproject2conda.isolated-dependencies] + """ + + if not channel: + channel = None + d = PyProject2Conda.from_path(filename) + s = d.to_conda_yaml(isolated=isolated, channels=channel, name=name) + if not output: + click.echo(s, nl=False) + +if __name__ == "__main__": + app() +# from __future__ import annotations +# import typer + +# from typing import Optional + +# from typing_extensions import Annotated + +# from pyproject2conda.parser import PyProject2Conda + +# app = typer.Typer(help="pyproject2conda CLI manager.") + +# FILE_OPT = typer.Option('-f','--file') +# FILE_CLI = Annotated[str | None, FILE_OPT] +# FILE_CLI = Annotated[str | None, typer.Argument()] + +# EXTRAS_OPT = typer.Option('-e','--extras') +# EXTRAS_CLI = Annotated[list[str] | None, EXTRAS_OPT] + +# ISOLATED_OPT = typer.Option('-i','--isolated') +# ISOLATED_CLI = Annotated[list[str] | None, ISOLATED_OPT] + +# @app.command() +# def main(): +# """Console script for pyproject2conda.""" +# print("hello") +# print("Replace this message by putting your code into " +# "pyproject2conda.cli.main") +# print("See click documentation at https://click.palletsprojects.com/") +# return 0 + + +# @app.command() +# def list( +# # file: FILE_CLI = "pyproject.toml", +# file: Annotated[str, typer.Option('-f')] = "pyproject.toml", +# ): + +# d = PyProject2Conda.from_path(file) + +# print('extras', d.list_extras()) +# print('isolated', d.list_isolated()) + + +# @app.command() +# def create( +# file: FILE_CLI = "pyproject.toml", +# extras: EXTRAS_CLI = None, +# isolated: ISOLATED_CLI = None, +# ): +# print(files) +# print(extras) +# print(isolated) + + + +# @app.command() +# def other(name: Annotated[Optional[str], typer.Argument()] = None): +# if name is None: +# print("Hello World!") +# else: +# print(f"Hello {name}") diff --git a/src/pyproject2conda/convert.py b/src/pyproject2conda/convert.py new file mode 100644 index 0000000..e35ee32 --- /dev/null +++ b/src/pyproject2conda/convert.py @@ -0,0 +1,301 @@ +# this is taken directly from poetry2conda.covert + +import argparse +import contextlib +import pathlib +import sys +from datetime import datetime +from typing import Mapping, TextIO, Tuple, Iterable, Optional + +import semver +import toml + +from poetry2conda import __version__ + + +def convert( + file: TextIO, include_dev: bool = False, extras: Optional[Iterable[str]] = None +) -> str: + """ Convert a pyproject.toml file to a conda environment YAML + + This is the main function of poetry2conda, where all parsing, converting, + etc. gets done. + + Parameters + ---------- + file + A file-like object containing a pyproject.toml file. + include_dev + Whether to include the dev dependencies in the resulting environment. + extras + The name of extras to include in the output. Can be None or empty + for no extras. + + Returns + ------- + The contents of an environment.yaml file as a string. + + """ + if extras is None: + extras = [] + poetry2conda_config, poetry_config = parse_pyproject_toml(file) + env_name = poetry2conda_config["name"] + poetry_dependencies = poetry_config.get("dependencies", {}) + if include_dev: + poetry_dependencies.update(poetry_config.get("dev-dependencies", {})) + poetry_extras = poetry_config.get("extras", {}) + # We mark the items listed in the selected extras as non-optional + for extra in extras: + for item in poetry_extras[extra]: + dep = poetry_dependencies[item] + if isinstance(dep, dict): + dep["optional"] = False + conda_constraints = poetry2conda_config.get("dependencies", {}) + + dependencies, pip_dependencies = collect_dependencies( + poetry_dependencies, conda_constraints + ) + conda_yaml = to_yaml_string(env_name, dependencies, pip_dependencies) + return conda_yaml + + +def convert_version(spec_str: str) -> str: + """ Convert a poetry version spec to a conda-compatible version spec. + + Poetry accepts tilde and caret version specs, but conda does not support + them. This function uses the `poetry-semver` package to parse it and + transform it to regular version spec ranges. + + Parameters + ---------- + spec_str + A poetry version specification string. + + Returns + ------- + The same version specification without tilde or caret. + + """ + spec = semver.parse_constraint(spec_str) + if isinstance(spec, semver.Version): + converted = f"=={str(spec)}" + elif isinstance(spec, semver.VersionRange): + converted = str(spec) + elif isinstance(spec, semver.VersionUnion): + raise ValueError("Complex version constraints are not supported at the moment.") + return converted + + +def parse_pyproject_toml(file: TextIO) -> Tuple[Mapping, Mapping]: + """ Parse a pyproject.toml file + + This function assumes that the pyproject.toml contains a poetry and + poetry2conda config sections. + + Parameters + ---------- + file + A file-like object containing a pyproject.toml file. + + Returns + ------- + A tuple with the poetry2conda and poetry config. + + Raises + ------ + RuntimeError + When an expected configuration section is missing. + + + """ + pyproject_toml = toml.loads(file.read()) + poetry_config = pyproject_toml.get("tool", {}).get("poetry", {}) + if not poetry_config: + raise RuntimeError(f"tool.poetry section was not found on {file.name}") + + poetry2conda_config = pyproject_toml.get("tool", {}).get("poetry2conda", {}) + if not poetry2conda_config: + raise RuntimeError(f"tool.poetry2conda section was not found on {file.name}") + + if "name" not in poetry2conda_config or not isinstance( + poetry2conda_config["name"], str + ): + raise RuntimeError(f"tool.poetry2conda.name entry was not found on {file.name}") + + return poetry2conda_config, poetry_config + + +def collect_dependencies( + poetry_dependencies: Mapping, conda_constraints: Mapping +) -> Tuple[Mapping, Mapping]: + """ Organize and apply conda constraints to dependencies + + Parameters + ---------- + poetry_dependencies + A dictionary with dependencies as declared with poetry. + conda_constraints + A dictionary with conda constraints as declared with poetry2conda. + + Returns + ------- + A tuple with the modified dependencies and the dependencies that must be + installed with pip. + + """ + dependencies = {} + pip_dependencies = {} + + # 1. Do a first pass to change pip to conda packages + for name, conda_dict in conda_constraints.items(): + if name in poetry_dependencies and "git" in poetry_dependencies[name]: + poetry_dependencies[name] = conda_dict["version"] + + # 2. Now do the conversion + for name, constraint in poetry_dependencies.items(): + if isinstance(constraint, str): + dependencies[name] = convert_version(constraint) + elif isinstance(constraint, dict): + if constraint.get("optional", False): + continue + if "git" in constraint: + git = constraint["git"] + tag = constraint["tag"] + pip_dependencies[f"git+{git}@{tag}#egg={name}"] = None + elif "version" in constraint: + dependencies[name] = convert_version(constraint["version"]) + else: + raise ValueError( + f"This converter only supports normal dependencies and " + f"git dependencies. No path, url, python restricted, " + f"environment markers or multiple constraints. In your " + f'case, check the "{name}" dependency. Sorry.' + ) + else: + raise ValueError( + f"This converter only supports normal dependencies and " + f"git dependencies. No multiple constraints. In your " + f'case, check the "{name}" dependency. Sorry.' + ) + + if name in conda_constraints: + conda_dict = conda_constraints[name] + if "name" in conda_dict: + new_name = conda_dict["name"] + dependencies[new_name] = dependencies.pop(name) + name = new_name + # do channel last, because it may move from dependencies to pip_dependencies + if "channel" in conda_dict: + channel = conda_dict["channel"] + if channel == "pip": + pip_dependencies[name] = dependencies.pop(name) + else: + new_name = f"{channel}::{name}" + dependencies[new_name] = dependencies.pop(name) + + if pip_dependencies: + dependencies["pip"] = None + + return dependencies, pip_dependencies + + +def to_yaml_string( + env_name: str, dependencies: Mapping, pip_dependencies: Mapping +) -> str: + """ Converts dependencies to a string in YAML format. + + Note that there is no third party library to manage the YAML format. This is + to avoid an additional package dependency (like pyyaml, which is already + one of the packages that behaves badly in conda+pip mixed environments). + But also because our YAML is very simple + + Parameters + ---------- + env_name + Name for the conda environment. + dependencies + Regular conda dependencies. + pip_dependencies + Pure pip dependencies. + + Returns + ------- + A string with an environment.yaml definition usable by conda. + + """ + deps_str = [] + for name, version in dependencies.items(): + version = version or "" + deps_str.append(f" - {name}{version}") + if pip_dependencies: + deps_str.append(f" - pip:") + for name, version in pip_dependencies.items(): + version = version or "" + deps_str.append(f" - {name}{version}") + deps_str = "\n".join(deps_str) + + date_str = datetime.now().strftime("%c") + conda_yaml = f""" +############################################################################### +# NOTE: This file has been auto-generated by poetry2conda +# poetry2conda version = {__version__} +# date: {date_str} +############################################################################### +# If you want to change the contents of this file, you should probably change +# the pyproject.toml file and then use poetry2conda again to update this file. +# Alternatively, stop using (ana)conda. +############################################################################### +name: {env_name} +dependencies: +{deps_str} +""".lstrip() + return conda_yaml + + +def write_file(filename: str, contents: str) -> None: + context = contextlib.ExitStack() + if filename == '-': + f = sys.stdout + else: + environment_yaml = pathlib.Path(filename) + if not environment_yaml.exists(): + environment_yaml.parent.mkdir(parents=True, exist_ok=True) + f = context.enter_context(environment_yaml.open('w')) + + with context: + f.write(contents) + + +def main(): + parser = argparse.ArgumentParser( + description="Convert a poetry-based pyproject.toml " + "to a conda environment.yaml" + ) + parser.add_argument( + "pyproject", + metavar="TOML", + type=argparse.FileType("r"), + help="pyproject.toml input file.", + ) + parser.add_argument( + "environment", + metavar="YAML", + type=str, + help="environment.yaml output file.", + ) + parser.add_argument( + "--dev", action="store_true", help="include dev dependencies", + ) + parser.add_argument( + "--extras", "-E", action="append", help="Add extra requirements", + ) + parser.add_argument( + "--version", action="version", version=f"%(prog)s (version {__version__})" + ) + args = parser.parse_args() + converted_obj = convert(args.pyproject, include_dev=args.dev, extras=args.extras) + write_file(args.environment, converted_obj) + + +if __name__ == "__main__": + main() diff --git a/src/pyproject2conda/parser.py b/src/pyproject2conda/parser.py new file mode 100644 index 0000000..6b1d14b --- /dev/null +++ b/src/pyproject2conda/parser.py @@ -0,0 +1,485 @@ +""" +Parsing (:mod:`pyproject2conda.parser`) +======================================= + +Main parser to turn pyproject.toml -> environment.yaml +""" +from __future__ import annotations +from typing import Any, TypeVar, Type, Optional, Union, Sequence, Mapping +from pathlib import Path +import re +import argparse +import shlex + +from ruamel.yaml import YAML + +import tomlkit + + +# -- typing ---------------------------------------------------------------------------- + +Tstr_opt = Optional[str] +Tstr_seq_opt = Optional[Union[str, Sequence[str]]] + + +# --- Default parser ------------------------------------------------------------------- + +_DEFAULTS = {} + + +def _default_parser(): + if "parser" in _DEFAULTS: + return _DEFAULTS["parser"] + + parser = argparse.ArgumentParser() + + parser.add_argument( + "-c", + "--channel", + type=str, + help="Channel to add to the pyproject requirement", + ) + parser.add_argument( + "-p", + "--pip", + action="store_true", + help="If specified, install dependency on pyproject dependency (on this line) with pip", + ) + parser.add_argument( + "-s", + "--skip", + action="store_true", + help="If specified skip pyproject dependency on this line", + ) + parser.add_argument("package", nargs="*") + + _DEFAULTS["parser"] = parser + + return parser + + +# taken from https://github.com/conda/conda-lock/blob/main/conda_lock/common.py +def get_in( + keys: Sequence[Any], nested_dict: Mapping[Any, Any], default: Any = None +) -> Any: + """ + >>> foo = {'a': {'b': {'c': 1}}} + >>> get_in(['a', 'b'], foo) + {'c': 1} + + """ + import operator + from functools import reduce + + try: + return reduce(operator.getitem, keys, nested_dict) + except (KeyError, IndexError, TypeError): + return default + + +def _iter_value_comment_pairs( + array: tomlkit.items.Array, +) -> list[tuple(Tstr_opt, Tstr_opt)]: + """extract value and comments from array""" + for v in array._value: + if v.value is not None and not isinstance(v.value, tomlkit.items.Null): + value = str(v.value) # .as_string() + else: + value = None + if v.comment: + comment = v.comment.as_string() + else: + comment = None + if value is None and comment is None: + continue + yield (value, comment) + + +def _matches_package_name( + dep: Tstr_opt, + package_name: str, +) -> list[str]: + """ + Check if `dep` matches pattern {package_name}[extra,..] + + If it does, return extras, else return None + """ + + if not dep: + return None + + pattern = rf"{package_name}\[(.*?)\]" + match = re.match(pattern, dep) + + if match: + extras = match.group(1).split(",") + else: + extras = None + return extras + + +def get_value_comment_pairs( + package_name: str, + deps: tomlkit.items.Array, + extras: Tstr_seq_opt = None, + opts: tomlkit.items.Table | None = None, + include_root: bool = True, +) -> list[tuple(Tstr_opt, Tstr_opt)]: + """ + Recursively build dependency, comment pairs from deps and extras. + """ + if include_root: + out = list(_iter_value_comment_pairs(deps)) + else: + out = [] + + if extras is None: + return out + else: + assert opts is not None + + if isinstance(extras, str): + extras = [extras] + + for extra in extras: + for value, comment in _iter_value_comment_pairs(opts[extra]): + if new_extras := _matches_package_name(value, package_name): + out.extend( + get_value_comment_pairs( + package_name=package_name, + extras=new_extras, + deps=deps, + opts=opts, + include_root=False, + ) + ) + else: + out.append((value, comment)) + + return out + + +def _match_p2c_comment(comment: Tstr_opt) -> Tstr_opt: + if not comment or not (match := re.match(r".*?#\s*p2c:\s*([^\#]*)", comment)): + return None + else: + return match.group(1).strip() + + +def _parse_p2c(match: Tstr_opt) -> Tstr_opt: + """Parse match from _match_p2c_comment""" + + if match: + return vars(_default_parser().parse_args(shlex.split(match))) + else: + return None + + +def parse_p2c_comment(comment: Tstr_opt) -> Tstr_opt: + if match := _match_p2c_comment(comment): + return _parse_p2c(match) + else: + return None + + +def value_comment_pairs_to_conda( + value_comment_list: list[tuple(Tstr_opt, Tstr_opt)] +) -> dict[str, Any]: + """ + convert raw value/comment pairs to install lines + """ + + conda_deps = [] + pip_deps = [] + + def _check_value(value): + if not value: + raise ValueError("trying to add value that does not exist") + + for value, comment in value_comment_list: + if comment and (parsed := parse_p2c_comment(comment)): + if parsed["pip"]: + _check_value(value) + pip_deps.append(value) + elif not parsed["skip"]: + _check_value(value) + + if parsed["channel"]: + v = "{}::{}".format(parsed["channel"], value) + else: + v = value + conda_deps.append(v) + + conda_deps.extend(parsed["package"]) + elif value: + conda_deps.append(value) + + return {"dependencies": conda_deps, "pip": pip_deps} + + +def _pyproject_to_value_comment_pairs( + data: tomlkit.toml_document.TOMLDocument, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, +): + + + project = data['project'] + package_name = project['name'] + + deps = project["dependencies"] + + if isolated: + value_comment_list = get_value_comment_pairs( + package_name=package_name, + extras=isolated, + deps=deps, + opts=get_in(["tool","pyproject2conda","isolated-dependencies"], data), + include_root=False, + ) + else: + value_comment_list = get_value_comment_pairs( + package_name=package_name, + extras=extras, + deps=deps, + opts=get_in(["project","optional-dependencies"], data), + ) + + return value_comment_list + + + +def pyproject_to_conda_lists( + data: str | Path | tomlkit.toml_document.TOMLDocument, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, +): + + if python == "get": + python = "python" + get_in(["project","requires-pythong"], data).unwrap() + + if channels is None: + channels = get_in(["tool", "pyproject2conda", "channels"], data, None) + if channels: + channels = channels.unwrap() + if isinstance(channels, str): + channels = [channels] + + + value_comment_list = _pyproject_to_value_comment_pairs( + data=data, extras=extras, isolated=isolated, + ) + + output = value_comment_pairs_to_conda(value_comment_list) + + if python: + output["dependencies"].insert(0, python) + + if channels: + output["channels"] = channels + + return output + + +def pyproject_to_conda( + data: str | Path | tomlkit.toml_document.TOMLDocument, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, + channels: Tstr_seq_opt = None, + name: Tstr_opt = None, + python: Tstr_opt = None, + stream: str | Path | None = None, +): + output = pyproject_to_conda_lists( + data=data, + extras=extras, + isolated=isolated, + channels=channels, + python=python, + ) + return _output_to_yaml(**output, name=name, stream=stream) + + +def _yaml_to_string(yaml, data, add_final_eol=False) -> str: + import io + + buf = io.BytesIO() + yaml.dump(data, buf) + + val = buf.getvalue() + + if not add_final_eol: + val = val[:-1] + + return val.decode("utf-8") + + +def _output_to_yaml( + dependencies: list[str] | None, + channels: list[str] | None = None, + pip: list[str] | None = None, + name: Tstr_opt = None, + stream: str | Path | None = None, +): + data = {} + + if name: + data["name"] = name + + if channels: + data["channels"] = channels + + data["dependencies"] = [] + if dependencies: + data["dependencies"].extend(dependencies) + if pip: + data["dependencies"].append("pip") + data["dependencies"].append({"pip": pip}) + + # return data + + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + + if stream is None: + return _yaml_to_string(yaml, data, add_final_eol=True) + else: + if isinstance(stream, (str, Path)): + with open(stream, "wb") as f: + yaml.dump(data, f) + else: + yaml.dump(data, stream) + + +T = TypeVar("T", bound="PyProject2Conda") + +class PyProject2Conda: + """ + Wrapper class to transform pyproject.toml -> environment.yaml + """ + + def __init__( + self, + data: tomlkit.toml_document.TOMLDocument, + name: Tstr_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, + ) -> None: + self.data = data + self.name = name + self.channels = channels + self.python = python + + def to_conda_yaml( + self, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, + name: Tstr_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, + stream: str | Path | None = None, + ): + + self._check_extras_isolated(extras, isolated) + + return pyproject_to_conda( + data=self.data, + extras=extras, + isolated=isolated, + name=name or self.name, + channels=channels or self.channels, + python=python or self.python, + stream=stream, + ) + + + def to_conda_lists( + self, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, + ) -> dict[str, Any]: + + self._check_extras_isolated(extras, isolated) + + return pyproject_to_conda_lists( + data=self.data, + extras=extras, + isolated=isolated, + channels=channels or self.channels, + python=python or self.python, + ) + + def to_requirement_list( + self, + extras: Tstr_seq_opt = None, + isolated: Tstr_seq_opt = None, + ) -> list[str]: + + self._check_extras_isolated(extras, isolated) + + values = _pyproject_to_value_comment_pairs( + data=self.data, extras=extras, isolated=isolated + ) + + return [x for x,y in values if x is not None] + + + def _check_extras_isolated(self, extras, isolated): + def _do_test(sent, available): + if isinstance(sent, str): + sent = [sent] + for s in sent: + if s not in available: + raise ValueError(f"{s} not in {available}") + + + if extras: + _do_test(extras, self.list_extras()) + + if isolated: + _do_test(isolated, self.list_isolated()) + + def _get_opts(self, *keys): + opts = get_in(keys, self.data, None) + if opts: + return list(opts.keys()) + else: + return [] + + def list_extras(self): + return self._get_opts('project','optional-dependencies') + + def list_isolated(self): + return self._get_opts('tool','pyproject2conda','isolated-dependencies') + + @classmethod + def from_string( + cls: Type[T], + toml_string: str, + name: Tstr_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, + ) -> T: + data = tomlkit.parse(toml_string) + return cls(data=data, name=name, channels=channels, python=python) + + @classmethod + def from_path( + cls: Type[T], + path: str | Path, + name: Tstr_opt = None, + channels: Tstr_seq_opt = None, + python: Tstr_opt = None, + ) -> T: + path = Path(path) + + if not path.exists(): + raise ValueError(f"{path} does not exist") + + with open(path, "rb") as f: + data = tomlkit.load(f) + return cls(data=data, name=name, channels=channels, python=python) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..45f33a3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Unit test package for pyproject2conda.""" diff --git a/tests/test-pyproject.toml b/tests/test-pyproject.toml new file mode 100644 index 0000000..1142838 --- /dev/null +++ b/tests/test-pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "hello" +requires-python = ">=3.8,<3.11" +dependencies = [ + "athing", # p2c: -p # a comment + "bthing", # p2c: -s bthing-conda + "cthing", # p2c: -c conda-forge + +] + +[project.optional-dependencies] +test = [ + "pandas", + "pytest", # p2c: -c conda-forge + +] +dev-extras = [ + # p2c: -s additional-thing # this is an additional conda package + "matplotlib", # p2c: -s conda-matplotlib + +] +dev = ["hello[test]", "hello[dev-extras]"] + +[tool.pyproject2conda] +channels = ['conda-forge'] + +[tool.pyproject2conda.isolated-dependencies] +dist-pypi = [ + "setuptools", + "build", # p2c: -p + +] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..bb2396d --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,112 @@ +from pyproject2conda.cli import app +from click.testing import CliRunner + +from textwrap import dedent + +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +def test_list(): + + runner = CliRunner() + result = runner.invoke(app, ["list", "-f", str(ROOT / "test-pyproject.toml")]) + expected = """\ + extras : ['test', 'dev-extras', 'dev'] + isolated: ['dist-pypi'] + """ + + assert result.output == dedent(expected) + + + +def test_create(): + + + runner = CliRunner() + result = runner.invoke(app, ["create", "-f", str(ROOT / "test-pyproject.toml")]) + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing + """ + + + assert (result.output) == dedent(expected) + + result = runner.invoke(app, ["create", "-f", str(ROOT / "test-pyproject.toml"), "dev"]) + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - additional-thing + - conda-matplotlib + - pip + - pip: + - athing + """ + + assert result.output == dedent(expected) + + + result = runner.invoke(app, ["create", "-f", str(ROOT / "test-pyproject.toml"), "-c","hello"]) + + expected = """\ +channels: + - hello +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing + """ + + assert dedent(expected) == result.output + + + + result = runner.invoke(app, ["create", "-f", str(ROOT / "test-pyproject.toml"), "test"]) + + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - pip + - pip: + - athing + """ + + assert dedent(expected) == result.output + + + + result = runner.invoke(app, ["isolated", "-f", str(ROOT / "test-pyproject.toml"), "dist-pypi"]) + + expected = """\ +channels: + - conda-forge +dependencies: + - setuptools + - pip + - pip: + - build + """ + + assert result.output == dedent(expected) diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..873eef6 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,167 @@ + + +from pyproject2conda import parser +from textwrap import dedent + +def test_match_p2c_comment(): + expected = "-c -d" + for comment in [ + "#p2c: -c -d", + "# p2c: -c -d", + " #p2c: -c -d", + "# p2c: -c -d # some other thing", + "# some other thing # p2c: -c -d # another thing", + ]: + + match = parser._match_p2c_comment(comment) + + + assert match == expected + +def test_parse_p2c(): + + def get_expected(pip=False, skip=False, channel=None, package=None): + if package is None: + package = [] + return {"pip": pip, "skip": skip, "channel": channel, "package": package} + + assert parser._parse_p2c("--pip") == get_expected(pip=True) + assert parser._parse_p2c("-p") == get_expected(pip=True) + + assert parser._parse_p2c("--skip") == get_expected(skip=True) + assert parser._parse_p2c("-s") == get_expected(skip=True) + + assert parser._parse_p2c("-s -c conda-forge") == get_expected(skip=True, channel="conda-forge") + + assert parser._parse_p2c("athing>=0.3,<0.2 ") == get_expected( + package=["athing>=0.3,<0.2"]) + + assert parser._parse_p2c("athing>=0.3,<0.2 bthing ") == get_expected( + package=["athing>=0.3,<0.2", "bthing"] + ) + + + +def test_complete(): + + from tomlkit import parse + + toml = dedent("""\ + [project] + name = "hello" + requires-python = ">=3.8,<3.11" + dependencies = [ + "athing", # p2c: -p # a comment + "bthing", # p2c: -s bthing-conda + "cthing", # p2c: -c conda-forge + ] + + [project.optional-dependencies] + test = [ + "pandas", + "pytest", # p2c: -c conda-forge + ] + dev-extras = [ + # p2c: -s additional-thing # this is an additional conda package + "matplotlib", # p2c: -s conda-matplotlib + ] + dev = [ + "hello[test]", + "hello[dev-extras]", + ] + + [tool.pyproject2conda] + channels = ['conda-forge'] + + [tool.pyproject2conda.isolated-dependencies] + dist-pypi = [ + "setuptools", + "build", # p2c: -p + ] + """) + + d = parser.PyProject2Conda.from_string(toml) + + out = d.to_conda_yaml() + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing + """ + + assert dedent(expected) == out + + + + out = d.to_conda_yaml(channels="hello") + + expected = """\ +channels: + - hello +dependencies: + - bthing-conda + - conda-forge::cthing + - pip + - pip: + - athing + """ + + assert dedent(expected) == out + + + + out = d.to_conda_yaml(extras="test") + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - pip + - pip: + - athing + """ + + assert dedent(expected) == out + + out = d.to_conda_yaml(isolated="dist-pypi") + + expected = """\ +channels: + - conda-forge +dependencies: + - setuptools + - pip + - pip: + - build + """ + + assert out == dedent(expected) + + + + expected = """\ +channels: + - conda-forge +dependencies: + - bthing-conda + - conda-forge::cthing + - pandas + - conda-forge::pytest + - additional-thing + - conda-matplotlib + - pip + - pip: + - athing + """ + + assert dedent(expected) == d.to_conda_yaml("dev") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..45a0f79 --- /dev/null +++ b/tox.ini @@ -0,0 +1,145 @@ +[tox] +isolated_build = True +requires = tox-conda +envlist = + # test + test-py3{8, 9, 10, 11} + +[base] +package_name = pyproject2conda +import_name = pyproject2conda +build_python = python3.10 +conda_env = {toxinidir}/environment.yaml +conda_env_dev = {toxinidir}/environment/dev.yaml +conda_env_test = {toxinidir}/environment/test.yaml +conda_env_docs = {toxinidir}/environment/docs.yaml +conda_env_dist_pypi = {toxinidir}/environment/dist-pypi.yaml +conda_env_dist_conda = {toxinidir}/environment/dist-conda.yaml +conda_env_lint = {toxinidir}/environment/lint.yaml +conda_channels = + wpk-nist + conda-forge +conda_deps_test = +allowlist_externals = + bash + make +commands_test_check = + python --version + python -c 'import {[base]import_name}; print( {[base]import_name}.__version__)' + bash -ec 'echo $PWD' + +[testenv] +passenv = + SETUPTOOLS_SCM_PRETEND_VERSION + TEST_VERSION + # general command + command + # linting + mypy_args + pyright_args + pytype_args + release_args + # dist-conda stuff + project_name + sdist_path + grayskull_args + recipe_base_path + recipe_append_path +usedevelop = + test: True +conda_env = + test: {[base]conda_env_test} +allowlist_externals = + {[base]allowlist_externals} +commands = + {[base]commands_test_check} + {posargs:pytest} + +[testenv:dev] +description = + Create development environment. +usedevelop = True +basepython = {[base]build_python} +conda_env = {[base]conda_env_dev} +envdir = {toxworkdir}/dev +commands = + {posargs:bash -ec 'conda list'} + +[testenv:docs] +description = + Runs make in docs directory. + For example, 'tox -e docs -- html' -> 'make -C docs html'. + With 'release' option, you can set the message with 'message=...' in posargs. +usedevelop = True +envdir = {toxworkdir}/docs +basepython = {[base]build_python} +conda_env = {[base]conda_env_docs} +changedir = {toxinidir}/docs +commands = + make {posargs:html} + +[testenv:dist-pypi] +description = + Runs make -f scrips/dist-pypi.mk posargs + For example, 'tox -e dist-pypi -- build' -> 'make -f scripts/dist-pypi.mk build' +skip_install = True +envdir = {toxworkdir}/dist-pypi +basepython = {[base]build_python} +conda_env = {[base]conda_env_dist_pypi} +changedir = {toxinidir} +commands = + make -f {toxinidir}/scripts/dist-pypi.mk {posargs:build} + +[testenv:dist-conda] +description = + Runs make -C dist-conda posargs + recipe: build conda recipe using grayskull (can optionally pass a local sdist) + build: build conda distribution + command: run arbitrary command +skip_install = True +envdir = {toxworkdir}/dist-conda +basepython = {[base]build_python} +conda_env = {[base]conda_env_dist_conda} +changedir = {toxinidir} +commands = + make -f {toxinidir}/scripts/dist-conda.mk {posargs} project_name={env:project_name:{[base]package_name}} + +[testenv:testdist-{pypi, conda, condaforge}-{local,remote}-py3{8, 9, 10, 11}] +conda_channels = + conda: {[base]conda_channels} + condaforge: conda-forge +description = + Test install from + pypi: pypi + conda: conda (user channel) + condaforge: conda (conda-forge channel) + using either + local: local + remote: remote + versions. +skip_install = True +conda_env = {toxinidir}/environment/test-extras.yaml +conda_deps = + conda-remote,condaforge-remote: {[base]package_name}{env:TEST_VERSION:''} + conda-local,condaforge-local: {posargs} +deps = + pypi-remote: {[base]package_name}{env:TEST_VERSION:''} + pypi-local: {posargs} + +[testenv:testpip-py3{8, 9, 10, 11}] +description = + Test package against pip installed packages +usedevelop = True +extras = test +conda_env = {toxinidir}/environment/test-extras.yaml + +[testenv:lint] +description = + Run linters + For example, 'tox -e lint -- mypy mypy_args=...' runs 'mypy $mypy_args' +conda_env = {[base]conda_env_lint} +usedevelop = True +envdir = {toxworkdir}/lint +basepython = {[base]build_python} +commands = + make -f {toxinidir}/scripts/lint.mk {posargs:mypy}