diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 60fdf693..00000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Black - -on: [push, pull_request] - -jobs: - - lint: - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. Without this if check, checks are duplicated since - # internal PRs match both the push and pull_request events. - if: - github.event_name == 'push' || github.event.pull_request.head.repo.full_name != - github.repository - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable - with: - options: "--check --verbose --diff" - src: "setup.py babelizer tests" diff --git a/.github/workflows/build-test-ci.yml b/.github/workflows/build-test-ci.yml index 9ad13bcd..a52b5c9e 100644 --- a/.github/workflows/build-test-ci.yml +++ b/.github/workflows/build-test-ci.yml @@ -20,12 +20,16 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - uses: conda-incubator/setup-miniconda@v2 with: @@ -36,60 +40,16 @@ jobs: channel-priority: true - name: Install dependencies - run: | - pip install coverage[toml] - mamba install --file=requirements.txt --file=requirements-testing.txt --file=external/requirements.txt - conda list - - - name: Install build system - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' - run: | - mamba install cython numpy setuptools wheel - - - name: Install babelizer - run: | - pip install . - - - name: Run diagnostics - run: | - babelize --version - babelize --help - babelize init --help - babelize update --help - babelize generate --help - babelize generate - python -c 'import babelizer; print(babelizer.__version__)' - - - name: Make C example - run: | - mkdir buildc && pushd buildc - cmake ../external/bmi-example-c -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX - make install - popd - - - name: Make C++ example - run: | - mkdir buildcxx && pushd buildcxx - cmake ../external/bmi-example-cxx -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX - make install - popd + run: python -m pip install nox tomli - - name: Make Fortran example - run: | - mkdir buildf && pushd buildf - cmake ../external/bmi-example-fortran -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX - make install - popd + - name: Install compilers + run: mamba install c-compiler cxx-compiler fortran-compiler cmake - - name: Make Python example - run: | - pushd ./external/bmi-example-python - make install - popd + - name: Run the tests + run: nox --session test test-cli --python ${{ matrix.python-version }} --verbose - - name: Test babelizer + language examples and generate coverage report - run: | - pytest --cov=babelizer --cov-report=xml:./coverage.xml -vvv babelizer tests external/tests + - name: Run the language tests + run: nox --non-interactive --error-on-missing-interpreter --session test-langs --python ${{ matrix.python-version }} --verbose - name: Coveralls if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000..ed0d4d3a --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,25 @@ +name: Check + +on: + pull_request: + types: [labeled, unlabeled, opened, reopened, synchronize] + +jobs: + check-changelog-entry: + name: changelog entry + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + # `towncrier check` runs `git diff --name-only origin/main...`, which + # needs a non-shallow clone. + fetch-depth: 0 + + - name: Check changelog + if: "!contains(github.event.pull_request.labels.*.name, 'Skip Changelog')" + run: | + if ! pipx run towncrier check --compare-with origin/${{ github.base_ref }}; then + echo "Please see https://landlab.readthedocs.io/en/master/development/contribution/index.html?highlight=towncrier#news-entries for guidance." + false + fi diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 939bb140..e051f467 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,9 +26,9 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - + runs-on: ubuntu-latest - + defaults: run: shell: bash -l {0} @@ -43,16 +43,11 @@ jobs: channel-priority: true - name: Show conda installation info - run: | - conda info - conda list + run: conda info - name: Install dependencies run: | - conda install mamba - mamba install --file=requirements.txt - pip install -r requirements-docs.txt - pip install -e . + pip install nox - name: Build documentation - run: make -C docs clean html + run: nox -s build-docs diff --git a/.github/workflows/flake8.yml b/.github/workflows/lint.yml similarity index 78% rename from .github/workflows/flake8.yml rename to .github/workflows/lint.yml index 3802dac1..7c719fae 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/lint.yml @@ -1,10 +1,11 @@ -name: Flake8 +name: Lint on: [push, pull_request] jobs: lint: + name: Check for lint # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -15,12 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Lint run: | - pip install flake8 - flake8 + pip install nox tomli + nox -s lint diff --git a/.gitignore b/.gitignore index 7bbc71c0..54107c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ ENV/ # mypy .mypy_cache/ + +# nox virtual envs +.nox/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..cd8f9928 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,84 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + name: black + description: "Black: The uncompromising Python code formatter" + entry: black + language: python + language_version: python3 + minimum_pre_commit_version: 2.9.2 + require_serial: true + types_or: [python, pyi] + exclude: ^babelizer/data + - id: black-jupyter + name: black-jupyter + description: + "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" + entry: black + language: python + minimum_pre_commit_version: 2.9.2 + require_serial: true + types_or: [python, pyi, jupyter] + additional_dependencies: [".[jupyter]"] + exclude: ^babelizer/data + +- repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify + exclude: ^babelizer/data + +- repo: https://github.com/asottile/pyupgrade + rev: v2.34.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + exclude: ^babelizer/data + +- repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + files: \.py$ + exclude: ^babelizer/data + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-builtin-literals + exclude: ^babelizer/data + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + exclude: ^babelizer/data + - id: check-yaml + exclude: ^babelizer/data + - id: debug-statements + exclude: ^babelizer/data + - id: end-of-file-fixer + - id: forbid-new-submodules + - id: trailing-whitespace + +- repo: https://github.com/PyCQA/pydocstyle + rev: 6.1.1 + hooks: + - id: pydocstyle + files: babelizer/.*\.py$ + args: + - --convention=numpy + - --add-select=D417 + exclude: ^babelizer/data + additional_dependencies: [".[toml]"] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.982 + hooks: + - id: mypy + additional_dependencies: [types-all] + exclude: ^babelizer/data diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f8dd6ae2..18f88626 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,6 @@ formats: - htmlzip python: - version: 3.8 install: - requirements: requirements-docs.txt - requirements: requirements.txt @@ -18,4 +17,9 @@ python: system_packages: false build: - image: latest + os: ubuntu-22.04 + tools: + python: "3.9" + jobs: + pre_build: + - sphinx-apidoc -e -force --no-toc --module-first -o docs/source/api babelizer diff --git a/CHANGES.rst b/CHANGES.rst index d3ec014e..7a1a84f2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,9 @@ Changelog ========= -.. towncrier release notes start - -0.3.10 (unreleased) -------------------- - -- Nothing changed yet. +.. towncrier-draft-entries:: Not yet released +.. towncrier release notes start 0.3.9 (2022-03-04) ------------------ @@ -242,4 +238,3 @@ Improved Documentation ------------------ - Initial release - diff --git a/LICENSE.rst b/LICENSE.rst index a01c8436..8c80f6df 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -23,4 +23,3 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Makefile b/Makefile deleted file mode 100644 index 6a06a119..00000000 --- a/Makefile +++ /dev/null @@ -1,122 +0,0 @@ -.PHONY: clean clean-test clean-pyc clean-build docs help -.DEFAULT_GOAL := help - -define BROWSER_PYSCRIPT -import os, webbrowser, sys - -try: - from urllib import pathname2url -except: - 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" -PY_PATH := "$(shell python -c 'import sys; print(sys.prefix)')" - -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 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 - -lint: ## check style with flake8 - flake8 babelizer tests - -pretty: ## reformat files to make them look pretty - isort babelizer tests - black setup.py babelizer tests docs/source/conf.py --exclude=babelizer/data - -test: ## run tests quickly with the default Python - pytest tests --disable-warnings -vvv - -test-languages: ## run tests on babelizer languages - pytest external/tests --disable-warnings -vvv - -coverage: ## check code coverage quickly with the default Python - coverage run --source babelizer --omit */babelizer/data/* -m pytest tests external/tests - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs - sphinx-apidoc -o docs/source/api babelizer --separate - rm -f docs/source/api/babelizer.rst - rm -f docs/source/api/modules.rst - $(MAKE) -C docs clean - $(MAKE) -C docs html - $(BROWSER) docs/build/html/index.html - -servedocs: docs ## compile the docs watching for changes - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -test-release: dist ## package and upload a release to TestPyPI - twine upload --repository testpypi dist/* - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python -m build - ls -l dist - twine check dist/* - -install: clean ## install the package to the active Python's site-packages - pip install -e . - -joss: ## make the paper - docker run --rm \ - --volume $(shell pwd)/paper:/data \ - --user $(shell id -u):$(shell id -g) \ - --env JOURNAL=joss \ - openjournals/paperdraft - open paper/paper.pdf - -examples: c-example cxx-example fortran-example python-example ## build the language examples - -c-example: - cmake -S external/bmi-example-c -B external/build/c -DCMAKE_INSTALL_PREFIX=$(PY_PATH) - make -C external/build/c install - -cxx-example: - cmake -S external/bmi-example-cxx -B external/build/cxx -DCMAKE_INSTALL_PREFIX=$(PY_PATH) - make -C external/build/cxx install - -fortran-example: - export - cmake -S external/bmi-example-fortran -B external/build/fortran -DCMAKE_INSTALL_PREFIX=$(PY_PATH) - make -C external/build/fortran install - -python-example: - make -C external/bmi-example-python install diff --git a/README.rst b/README.rst index e298c530..5d2883aa 100644 --- a/README.rst +++ b/README.rst @@ -448,4 +448,3 @@ see the User Guide of the `documentation`_. .. _BMI example C++: https://github.com/csdms/bmi-example-cxx/ .. _BMI example Fortran: https://github.com/csdms/bmi-example-fortran/ .. _BMI example Python: https://github.com/csdms/bmi-example-python/ - diff --git a/babelizer/__init__.py b/babelizer/__init__.py index 26d23bad..78120efc 100644 --- a/babelizer/__init__.py +++ b/babelizer/__init__.py @@ -1,3 +1,4 @@ +"""The *babelizer*.""" from ._version import __version__ __all__ = ["__version__"] diff --git a/babelizer/cli.py b/babelizer/cli.py index f2d1f3be..a85bd3dd 100644 --- a/babelizer/cli.py +++ b/babelizer/cli.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +"""The command line interface to the babelizer.""" import fnmatch import os import pathlib @@ -94,7 +94,7 @@ def init(meta, template, quiet, verbose, package_version): if not quiet: out( - "Don't forget to drop model metadata files into {0}".format( + "Don't forget to drop model metadata files into {}".format( new_folder / "meta" ) ) @@ -124,9 +124,7 @@ def init(meta, template, quiet, verbose, package_version): help="Location of cookiecutter template", ) def update(template, quiet, verbose): - """Update an existing babelized project.""" - package_path = pathlib.Path(".").resolve() for fname in ("babel.toml", "babel.yaml", "plugin.yaml"): @@ -182,7 +180,7 @@ def update(template, quiet, verbose): if not quiet: out( - "Don't forget to drop model metadata files into {0}".format( + "Don't forget to drop model metadata files into {}".format( package_path / "meta" ) ) @@ -256,7 +254,6 @@ def generate( os_name, ): """Generate the babelizer configuration file.""" - meta = _gather_input( prompt=prompt, package=package, @@ -302,7 +299,6 @@ def _gather_input( the value of ``no_input``, either a default value is used or the user will be prompted for a value. """ - if prompt: ask = partial(click.prompt, show_default=True, err=True) else: diff --git a/babelizer/data/hooks/post_gen_project.py b/babelizer/data/hooks/post_gen_project.py index 68776545..29961d8b 100644 --- a/babelizer/data/hooks/post_gen_project.py +++ b/babelizer/data/hooks/post_gen_project.py @@ -4,7 +4,6 @@ from collections import defaultdict from pathlib import Path - PROJECT_DIRECTORY = Path.cwd().resolve() LIB_DIRECTORY = Path("{{ cookiecutter.package_name }}", "lib") @@ -44,7 +43,7 @@ def clean_folder(folderpath, keep=None): def split_file(filepath, include_preamble=False): filepath = Path(filepath) - SPLIT_START_REGEX = re.compile("\s*#\s*start:\s*(?P\S+)\s*") + SPLIT_START_REGEX = re.compile(r"\s*#\s*start:\s*(?P\S+)\s*") files = defaultdict(list) fname = "preamble" diff --git a/babelizer/data/hooks/pre_gen_project.py b/babelizer/data/hooks/pre_gen_project.py index d5342108..e03b932d 100644 --- a/babelizer/data/hooks/pre_gen_project.py +++ b/babelizer/data/hooks/pre_gen_project.py @@ -1,7 +1,6 @@ import re import sys - MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" module_name = "{{ cookiecutter.package_name }}" diff --git a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml b/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml index 0823780f..f69190dd 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml +++ b/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml @@ -55,4 +55,4 @@ jobs: python -c 'import {{ cookiecutter.package_name }}' {%- for babelized_class in cookiecutter.components %} bmi-test {{ cookiecutter.package_name }}.bmi:{{ babelized_class }} -vvv - {%- endfor %} \ No newline at end of file + {%- endfor %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst b/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst index 718d8d02..8e176aa1 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst +++ b/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst @@ -9,4 +9,3 @@ Changelog for {{cookiecutter.package_name}} ------------------ - Initial release - diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py b/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py index b3e8ba4e..8c9f2821 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py +++ b/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # {{ cookiecutter.package_name }} documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. @@ -20,6 +19,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('..')) import {{ cookiecutter.package_name }} @@ -47,9 +47,9 @@ master_doc = 'index' # General information about the project. -project = u'{{ cookiecutter.package_name }}' -copyright = u"{% now 'local', '%Y' %}, {{ cookiecutter.info.full_name }}" -author = u"{{ cookiecutter.info.full_name }}" +project = '{{ cookiecutter.package_name }}' +copyright = "{% now 'local', '%Y' %}, {{ cookiecutter.info.full_name }}" +author = "{{ cookiecutter.info.full_name }}" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -129,8 +129,8 @@ # [howto, manual, or own class]). latex_documents = [ (master_doc, '{{ cookiecutter.package_name }}.tex', - u'{{ cookiecutter.package_name }} Documentation', - u'{{ cookiecutter.info.full_name }}', 'manual'), + '{{ cookiecutter.package_name }} Documentation', + '{{ cookiecutter.info.full_name }}', 'manual'), ] @@ -140,7 +140,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, '{{ cookiecutter.package_name }}', - u'{{ cookiecutter.package_name }} Documentation', + '{{ cookiecutter.package_name }} Documentation', [author], 1) ] @@ -152,12 +152,9 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, '{{ cookiecutter.package_name }}', - u'{{ cookiecutter.package_name }} Documentation', + '{{ cookiecutter.package_name }} Documentation', author, '{{ cookiecutter.package_name }}', 'One line description of project.', 'Miscellaneous'), ] - - - diff --git a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml b/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml index 80670bd5..437aaaa4 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml +++ b/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["cython", "numpy", "setuptools", "wheel"] +requires = ["cython", "numpy", "setuptools<60", "wheel"] [tool.pytest.ini_options] minversion = "5.0" diff --git a/babelizer/data/{{cookiecutter.package_name}}/setup.py b/babelizer/data/{{cookiecutter.package_name}}/setup.py index b58eae5e..60d8f81d 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/setup.py +++ b/babelizer/data/{{cookiecutter.package_name}}/setup.py @@ -1,19 +1,23 @@ #! /usr/bin/env python import os import sys + {%- if cookiecutter.language == 'fortran' %} import contextlib import subprocess + {%- endif -%} {%- if cookiecutter.language in ['c', 'c++', 'fortran'] %} import numpy as np + {% endif %} from setuptools import Extension, find_packages, setup {% if cookiecutter.language == 'fortran' -%} -from setuptools.command.build_ext import build_ext as _build_ext from numpy.distutils.fcompiler import new_fcompiler +from setuptools.command.build_ext import build_ext as _build_ext + {% endif %} {% if cookiecutter.language in ['c', 'c++', 'fortran'] -%} @@ -137,7 +141,7 @@ def read(filename): return fp.read() -long_description = u'\n\n'.join( +long_description = '\n\n'.join( [read('README.rst'), read('CREDITS.rst'), read('CHANGES.rst')] ) diff --git a/babelizer/errors.py b/babelizer/errors.py index e846beea..8a06edbb 100644 --- a/babelizer/errors.py +++ b/babelizer/errors.py @@ -1,11 +1,14 @@ -class BabelizeError(Exception): +"""Exceptions raised by the *babelizer*.""" + +class BabelizeError(Exception): """An exception that the babelizer can handle and show to the user.""" def __init__(self, message): self._message = message def __str__(self): + """Render a user-readable error message.""" return self._message diff --git a/babelizer/metadata.py b/babelizer/metadata.py index 03be4d76..1de02e78 100644 --- a/babelizer/metadata.py +++ b/babelizer/metadata.py @@ -1,7 +1,9 @@ +"""Library metadata used by the babelizer to wrap libraries.""" import pathlib import re import warnings from collections import OrderedDict, defaultdict +from contextlib import suppress import tomlkit as toml import yaml @@ -10,7 +12,10 @@ def _setup_yaml_with_canonical_dict(): - """https://stackoverflow.com/a/8661021""" + """Add a pyyaml handler to create canonical dictionaries. + + From https://stackoverflow.com/a/8661021 + """ yaml.add_representer( OrderedDict, lambda self, data: self.represent_mapping( @@ -67,13 +72,19 @@ def repr_tuple(dumper, data): def validate_dict(meta, required=None, optional=None): """Validate babelizer configuration metadata. - Args: - meta (dict): Configuration metadata - required (dict, optional): Required keys in configuration. Defaults to None. - optional (dict, optional): Optional keys in configuration. Defaults to None. - - Raises: - ValidationError: Raised for invalid metadata + Parameters + ---------- + meta : dict + Configuration metadata + required : dict, optional + Required keys in configuration. + optional : dict, optional + Optional keys in configuration. + + Raises + ------ + ValidationError + Raised for invalid metadata. """ actual = set(meta) required = set() if required is None else set(required) @@ -82,14 +93,14 @@ def validate_dict(meta, required=None, optional=None): if missing := required - actual: raise ValidationError( - "missing required key{0}: {1}".format( + "missing required key{}: {}".format( "s" if len(missing) > 1 else "", ", ".join(missing) ) ) if unknown := actual - valid: raise ValidationError( - "unknown key{0}: {1}".format( + "unknown key{}: {}".format( "s" if len(unknown) > 1 else "", ", ".join(unknown) ) ) @@ -106,6 +117,7 @@ def _norm_os(name): class BabelMetadata: + """Library metadata.""" LOADERS = {"yaml": yaml.safe_load, "toml": toml.parse} @@ -114,13 +126,20 @@ def __init__( ): """Metadata used by the babelizer to wrap a library. - Args: - library (dict, optional): Information about the library being babelized. Defaults to None. - build (dict, optional): User-specified compiler flags. Defaults to None. - package (dict, optional): Name and requirements to build the babelized library. Defaults to None. - info (dict, optional): Descriptive information about the package. Defaults to None. - plugin (dict, optional): Deprecated, use package. Defaults to None. - ci (dict, optional): Information about how to set up continuous integration. Defaults to None. + Parameters + ---------- + library : dict, optional + Information about the library being babelized. + build : dict, optional + User-specified compiler flags. + package : dict, optional + Name and requirements to build the babelized library. + info : dict, optional + Descriptive information about the package. + plugin : dict, optional + Deprecated, use package. + ci : dict, optional + Information about how to set up continuous integration. """ if plugin is not None: warnings.warn("use 'package' instead of 'plugin'", DeprecationWarning) @@ -144,11 +163,16 @@ def __init__( def from_stream(cls, stream, fmt="yaml"): """Create an instance of BabelMetadata from a file-like object. - Args: - stream (file object): File object with a babelizer configuration - fmt (str, optional): File format. Defaults to "yaml". + Parameters + ---------- + stream : file-like + File object with a babelizer configuration + fmt : str, optional + File format. - Returns: + Returns + ------- + BabelMetadata A BabelMetadata instance. """ try: @@ -171,25 +195,33 @@ def from_stream(cls, stream, fmt="yaml"): def from_path(cls, filepath): """Create an instance of BabelMetadata from a path-like object. - Args: - filepath (str): Path to a babelizer configuration file. + Parameters + ---------- + filepath : str + Path to a babelizer configuration file. - Returns: + Returns + ------- A BabelMetadata instance. """ path = pathlib.Path(filepath) - with open(filepath, "r") as fp: + with open(filepath) as fp: return BabelMetadata.from_stream(fp, fmt=path.suffix[1:]) def get(self, section, value): """Get a metadata value from the given section. - Args: - section (str): Section name. - value (str): Key name. + Parameters + ---------- + section : str + Section name. + value : str + Key name. - Returns: + Returns + ------- + value Metadata value. """ return self._meta[section][value] @@ -198,11 +230,15 @@ def get(self, section, value): def validate(config): """Ensure babelizer configuration metadata are valid. - Args: - config (dict): Metadata to babelize a library. + Parameters + ---------- + config : dict + Metadata to babelize a library. - Raises: - ValidationError: if metadata are not valid. + Raises + ------ + ValidationError + If metadata are not valid. """ libraries = config["library"] if "entry_point" in libraries: @@ -213,7 +249,7 @@ def validate(config): except ValidationError: raise ValidationError(f"poorly-formed entry point ({entry_point})") else: - for babelized_class, library in libraries.items(): + for _babelized_class, library in libraries.items(): validate_dict( library, required={"language", "library", "header", "entry_point"}, @@ -301,17 +337,19 @@ def _handle_old_style_info(info): def norm(config): """Ensure current style metadata are used in babelizer configuration. - Args: - config (dict): Metadata to babelize a library. + Parameters + ---------- + config : dict + Metadata to babelize a library. - Returns: + Return + ------ + dict A dict of babelizer configuration metadata. """ build = defaultdict(list) - try: + with suppress(KeyError): build.update(config["build"]) - except KeyError: - pass if "entry_point" in config["library"]: libraries = BabelMetadata._handle_old_style_entry_points(config["library"]) @@ -350,19 +388,26 @@ def norm(config): def dump(self, fp, fmt="yaml"): """Write serialized metadata to a file. - Args: - fp (file object): File object for output. - fmt (str, optional): [description]. Defaults to "yaml". + Parameters + ---------- + fp : file-like + File object for output. + fmt : str, optional + Format to serialize data. """ print(self.format(fmt=fmt), file=fp) def format(self, fmt="yaml"): - """Serialize metadata to output format + """Serialize metadata to output format. - Args: - fmt (str, optional): Output format. Defaults to "yaml". + Parameters + ---------- + fmt : str, optional + Output format. - Returns: + Returns + ------- + metadata : str Serialized metadata. """ return getattr(self, f"format_{fmt}")() @@ -370,8 +415,10 @@ def format(self, fmt="yaml"): def format_yaml(self): """Serialize metadata as YAML. - Returns: - str: Serialized metadata as a YAML-formatted string + Returns + ------- + str + Serialized metadata as a YAML-formatted string """ import io @@ -382,8 +429,10 @@ def format_yaml(self): def format_toml(self): """Serialize metadata as TOML. - Returns: - str: Serialized metadata as a TOML-formatted string + Returns + ------- + str + Serialized metadata as a TOML-formatted string """ return toml.dumps(self._meta) @@ -418,8 +467,8 @@ def parse_entry_point(specifier): babelizer.errors.ValidationError: bad entry point specifier (bar:Baz). specifier must be of the form name=module:class """ try: - name, value = [item.strip() for item in specifier.split("=")] - module, obj = [item.strip() for item in value.split(":")] + name, value = (item.strip() for item in specifier.split("=")) + module, obj = (item.strip() for item in value.split(":")) except ValueError: raise ValidationError( f"bad entry point specifier ({specifier}). specifier must be of the form name=module:class" @@ -430,8 +479,10 @@ def parse_entry_point(specifier): def as_cookiecutter_context(self): """Format metadata in cookiecutter context. - Returns: - dict: Metadata in cookiecutter context. + Returns + ------- + dict + Metadata in cookiecutter context. """ languages = [lib["language"] for lib in self._meta["library"].values()] language = languages[0] diff --git a/babelizer/render.py b/babelizer/render.py index 28b576a2..34f13919 100644 --- a/babelizer/render.py +++ b/babelizer/render.py @@ -1,3 +1,4 @@ +"""Render a new babelized project.""" import contextlib import os import pathlib @@ -16,18 +17,28 @@ def render(plugin_metadata, output, template=None, clobber=False, version="0.1"): """Generate a babelized library. - Args: - plugin_metadata (BabelMetadata obj): The metadata used to babelize the library. - output (str): Name of the directory that will be the new repository. - template (str, optional): Path (or URL) to the cookiecutter template to use. Defaults to None. - clobber (bool, optional): If a like-named repository already exists, overwrite it. Defaults to False. - version (str, optional): Version of babelized library. Defaults to "0.1". + Parameters + ---------- + plugin_metadata : BabelMetadata + The metadata used to babelize the library. + output : str + Name of the directory that will be the new repository. + template : str, optional + Path (or URL) to the cookiecutter template to use. + clobber : bool, optional + If a like-named repository already exists, overwrite it. + version : str, optional + Version of babelized library. - Raises: - OutputDirExistsError: Raised if output directory exists and clobber is not set. + Returns + ------- + str + Path to babelized library - Returns: - str: Path to babelized library + Raises + ------ + OutputDirExistsError + Raised if output directory exists and clobber is not set. """ if template is None: template = pkg_resources.resource_filename("babelizer", "data") @@ -91,7 +102,7 @@ def render_plugin_repo(template, context=None, output_dir=".", clobber=False): # if not os.path.isdir(path): path = output_dir / f"{name}" if not path.is_dir(): - raise RenderError("error creating {0}".format(path)) + raise RenderError(f"error creating {path}") git.Repo.init(path) @@ -102,8 +113,10 @@ def render_plugin_repo(template, context=None, output_dir=".", clobber=False): def as_cwd(path): """Change directory context. - Args: - path (str): Path-like object to a directory. + Parameters + ---------- + path : str + Path-like object to a directory. """ prev_cwd = os.getcwd() os.chdir(path) @@ -114,10 +127,12 @@ def as_cwd(path): def blacken_file(filepath): """Format a Python file with ``black``. - Args: - filepath (str): Path-like object to a Python file. + Parameters + ---------- + filepath : str + Path-like object to a Python file. """ - with open(filepath, "r") as fp: + with open(filepath) as fp: try: new_contents = blk.format_file_contents( fp.read(), fast=True, mode=blk.FileMode() @@ -132,8 +147,10 @@ def blacken_file(filepath): def prettify_python(path_to_repo): """Format files in babelized project with ``black``. - Args: - path_to_repo (str): Path-like object to babelized project. + Parameters + ---------- + path_to_repo : str + Path-like object to babelized project. """ path_to_repo = pathlib.Path(path_to_repo) with open(path_to_repo / "babel.toml") as fp: diff --git a/babelizer/utils.py b/babelizer/utils.py index 650c1615..e5779abd 100644 --- a/babelizer/utils.py +++ b/babelizer/utils.py @@ -1,7 +1,8 @@ +"""Utility functions used by the babelizer.""" import pathlib import subprocess import sys -from contextlib import contextmanager +from contextlib import contextmanager, suppress from .errors import SetupPyError @@ -9,11 +10,15 @@ def execute(args): """Run a command through the ``subprocess`` module. - Args: - args (list): Command and arguments to command. + Parameters + ---------- + args : list + Command and arguments to command. - Returns: - results from ``subprocess.run``. + Returns + ------- + ~subprocess.CompletedProcess + results from :func:`subprocess.run`. """ return subprocess.run(args, capture_output=True, check=True) @@ -21,8 +26,10 @@ def execute(args): def setup_py(*args): """Format the command to build/install the babelized package. - Returns: - list of str: The build/install command. + Returns + ------- + list of str + The build/install command. """ return [sys.executable, "setup.py"] + list(args) @@ -30,11 +37,15 @@ def setup_py(*args): def get_setup_py_version(): """Get babelized package version. - Raises: - SetupPyError: if calling ``python setup.py`` raises an exception. + Returns + ------- + str or None + Package version. - Returns: - str or None: Package version. + Raises + ------ + SetupPyError + If calling ``python setup.py`` raises an exception. """ if pathlib.Path("setup.py").exists(): try: @@ -54,19 +65,20 @@ def get_setup_py_version(): def save_files(files): """Generate repository files through a context. - Args: - files (list of str): List of path-like objects. + Parameters + ---------- + files : list of str + List of path-like objects. - Yields: + Yields + ------ + str Generator for repository files. """ contents = {} for file_ in files: - try: - with open(file_, "r") as fp: - contents[file_] = fp.read() - except FileNotFoundError: - pass + with suppress(FileNotFoundError), open(file_) as fp: + contents[file_] = fp.read() yield contents for file_ in contents: with open(file_, "w") as fp: diff --git a/babelizer/wrap.py b/babelizer/wrap.py deleted file mode 100644 index 7013daf6..00000000 --- a/babelizer/wrap.py +++ /dev/null @@ -1,20 +0,0 @@ -#! /usr/bin/env python -import os - -import pkg_resources -from cookiecutter.main import cookiecutter - - -def render( - language, context=None, output_dir=None, no_input=True, overwrite_if_exists=False -): - # template = pkg_resources.resource_filename( - # __name__, os.path.join('templates', language)) - template = pkg_resources.resource_filename(__name__, os.path.join("templates")) - return cookiecutter( - template, - overwrite_if_exists=overwrite_if_exists, - no_input=no_input, - output_dir=output_dir, - extra_context=context, - ) diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html index 378f0e64..0daabfa1 100644 --- a/docs/source/_templates/links.html +++ b/docs/source/_templates/links.html @@ -5,4 +5,3 @@

Useful Links

  • CSDMS Workbench
  • CSDMS Homepage
  • - diff --git a/docs/source/api/.gitignore b/docs/source/api/.gitignore new file mode 100644 index 00000000..5273fbb0 --- /dev/null +++ b/docs/source/api/.gitignore @@ -0,0 +1,2 @@ +# auto-generated with sphinx-apidoc +babelizer*.rst diff --git a/docs/source/api/babelizer.cli.rst b/docs/source/api/babelizer.cli.rst deleted file mode 100644 index a6dd96da..00000000 --- a/docs/source/api/babelizer.cli.rst +++ /dev/null @@ -1,11 +0,0 @@ -babelizer.cli module -==================== - -.. automodule:: babelizer.cli - :members: - :undoc-members: - :show-inheritance: - -.. click:: babelizer.cli:babelize - :prog: babelize - :nested: full diff --git a/docs/source/api/babelizer.errors.rst b/docs/source/api/babelizer.errors.rst deleted file mode 100644 index 39bdef99..00000000 --- a/docs/source/api/babelizer.errors.rst +++ /dev/null @@ -1,7 +0,0 @@ -babelizer.errors module -======================= - -.. automodule:: babelizer.errors - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/babelizer.metadata.rst b/docs/source/api/babelizer.metadata.rst deleted file mode 100644 index f2bea133..00000000 --- a/docs/source/api/babelizer.metadata.rst +++ /dev/null @@ -1,8 +0,0 @@ -babelizer.metadata module -========================= - -.. automodule:: babelizer.metadata - :members: - :undoc-members: - :special-members: __init__ - :show-inheritance: diff --git a/docs/source/api/babelizer.render.rst b/docs/source/api/babelizer.render.rst deleted file mode 100644 index a5e496eb..00000000 --- a/docs/source/api/babelizer.render.rst +++ /dev/null @@ -1,7 +0,0 @@ -babelizer.render module -======================= - -.. automodule:: babelizer.render - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/babelizer.utils.rst b/docs/source/api/babelizer.utils.rst deleted file mode 100644 index 127e02b4..00000000 --- a/docs/source/api/babelizer.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -babelizer.utils module -====================== - -.. automodule:: babelizer.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/babelizer.wrap.rst b/docs/source/api/babelizer.wrap.rst deleted file mode 100644 index 96b04369..00000000 --- a/docs/source/api/babelizer.wrap.rst +++ /dev/null @@ -1,10 +0,0 @@ -babelizer.wrap module -===================== - -.. deprecated:: 0.2 - Use `babelizer.render` instead. - -.. automodule:: babelizer.wrap - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 0b3beb8a..ed03946d 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -4,11 +4,6 @@ Developer Documentation API documentation for each *babelizer* module is listed on a separate page. .. toctree:: - :maxdepth: 4 + :maxdepth: 2 - babelizer.cli - babelizer.errors - babelizer.metadata - babelizer.render - babelizer.utils - babelizer.wrap + babelizer diff --git a/docs/source/babel_heatc.toml b/docs/source/babel_heatc.toml index b0565dea..c6f3786f 100644 --- a/docs/source/babel_heatc.toml +++ b/docs/source/babel_heatc.toml @@ -27,4 +27,3 @@ summary = "PyMT plugin for the C heat model" [ci] python_version = ["3.9"] os = ["linux", "mac", "windows"] - diff --git a/docs/source/cli.rst b/docs/source/cli.rst new file mode 100644 index 00000000..347a8ba1 --- /dev/null +++ b/docs/source/cli.rst @@ -0,0 +1,7 @@ +====================== +Command line interface +====================== + +.. click:: babelizer.cli:babelize + :prog: babelize + :nested: full diff --git a/docs/source/conf.py b/docs/source/conf.py index 8b914200..0fd7a70f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,18 +4,21 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -import datetime -import pkg_resources - -# -- Path setup -------------------------------------------------------------- - # 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. +import datetime import os +import pathlib import sys +import pkg_resources + +# -- Path setup -------------------------------------------------------------- + + sys.path.insert(0, os.path.abspath("../..")) +docs_dir = os.path.dirname(__file__) # The master toctree document. @@ -28,7 +31,7 @@ version = pkg_resources.get_distribution("babelizer").version release = version this_year = datetime.date.today().year -copyright = "%s, %s" % (this_year, author) +copyright = f"{this_year}, {author}" # -- General configuration --------------------------------------------------- @@ -36,7 +39,21 @@ # 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.napoleon", "sphinx_click"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "sphinx_inline_tabs", + "sphinx_click", + "sphinx_copybutton", + "sphinxcontrib.towncrier", +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -44,7 +61,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +exclude_patterns: list[str] = [] # -- Options for HTML output ------------------------------------------------- @@ -53,6 +70,9 @@ # a list of builtin themes. # html_theme = "alabaster" +# html_theme = "furo" +html_title = "babelizer" +language = "en" # 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, @@ -78,3 +98,36 @@ "searchbox.html", ], } + +# html_theme_options = { +# "announcement": None, +# "source_repository": "https://github.com/csdms/babelizer/", +# "source_branch": "develop", +# "source_directory": "docs/source", +# "sidebar_hide_name": False, +# "footer_icons": [ +# { +# "name": "power", +# "url": "https://csdms.colorado.edu", +# "html": """ +# +# Powered by CSDMS +# """, +# "class": "", +# }, +# ], +# } +# -- Options for intersphinx extension --------------------------------------- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + + +# -- Options for towncrier_draft extension -------------------------------------------- + +towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-release', 'sphinx-version' +towncrier_draft_include_empty = True +towncrier_draft_working_directory = pathlib.Path(docs_dir).parent.parent + +autodoc_default_options = {"special-members": "__init__", "undoc-members": True} diff --git a/docs/source/example.rst b/docs/source/example.rst index 911190bf..78977ae9 100644 --- a/docs/source/example.rst +++ b/docs/source/example.rst @@ -130,7 +130,7 @@ of the library containing the compiled *heat* model: .. code:: - > if not exist %LIBRARY_INC%\\bmi_heat.h exit 1 + > if not exist %LIBRARY_INC%\\bmi_heat.h exit 1 Create the *babelizer* input file --------------------------------- @@ -277,7 +277,7 @@ ending with .. code:: bash 🎉 All tests passed! - + if everything has been built correctly. diff --git a/docs/source/examples/heatc_ex.py b/docs/source/examples/heatc_ex.py index dc955dd7..28ca05c9 100644 --- a/docs/source/examples/heatc_ex.py +++ b/docs/source/examples/heatc_ex.py @@ -2,7 +2,6 @@ import numpy as np from pymt_heatc import HeatModel - config_file = "config.txt" np.set_printoptions(formatter={"float": "{: 6.1f}".format}) @@ -18,7 +17,7 @@ # Get the grid_id for the plate_surface__temperature variable. var_name = "plate_surface__temperature" -print("Variable {}".format(var_name)) +print(f"Variable {var_name}") grid_id = m.get_var_grid(var_name) print(" - grid id:", grid_id) diff --git a/docs/source/examples/pymt_heatc_ex.py b/docs/source/examples/pymt_heatc_ex.py index 3e2f4fed..44d79546 100644 --- a/docs/source/examples/pymt_heatc_ex.py +++ b/docs/source/examples/pymt_heatc_ex.py @@ -1,8 +1,6 @@ """Run the heat model in pymt.""" -import numpy as np from pymt.models import HeatModel - # Instantiate the component and get its name. m = HeatModel() print(m.name) @@ -14,14 +12,14 @@ # List the model's exchange items. print("Number of input vars:", len(m.input_var_names)) for var in m.input_var_names: - print(" - {}".format(var)) + print(f" - {var}") print("Number of output vars:", len(m.output_var_names)) for var in m.output_var_names: - print(" - {}".format(var)) + print(f" - {var}") # Get variable info. var_name = m.output_var_names[0] -print("Variable {}".format(var_name)) +print(f"Variable {var_name}") print(" - variable type:", m.var_type(var_name)) print(" - units:", m.var_units(var_name)) print(" - itemsize:", m.var_itemsize(var_name)) @@ -44,9 +42,9 @@ print("Time units:", m.time_units) # Get the initial values of the variable. -print("Get values of {}...".format(var_name)) +print(f"Get values of {var_name}...") val = m.var[var_name].data -print(" - values at time {}:".format(m.time)) +print(f" - values at time {m.time}:") print(val) # Advance the model by one time step. diff --git a/docs/source/index.rst b/docs/source/index.rst index 604c806b..758e9e33 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,7 +31,7 @@ The *babelizer* is an element of the `CSDMS Workbench`_, an integrated system of software tools, technologies, and standards for building and coupling models. - + User Guide ========== @@ -39,6 +39,7 @@ User Guide :maxdepth: 2 readme + cli example glossary diff --git a/external/requirements.txt b/external/requirements.txt index 85899c4b..496776c5 100644 --- a/external/requirements.txt +++ b/external/requirements.txt @@ -7,4 +7,6 @@ cmake c-compiler cxx-compiler fortran-compiler +make pkg-config +pip diff --git a/external/tests/test_c.py b/external/tests/test_c.py index 4d439494..d6a4093e 100644 --- a/external/tests/test_c.py +++ b/external/tests/test_c.py @@ -5,16 +5,24 @@ import shutil import subprocess import sys +from functools import partial from click.testing import CliRunner from babelizer.cli import babelize - -extra_opts = [] +extra_opts: list[str] = [] if sys.platform.startswith("linux") and int(platform.python_version_tuple()[1]) <= 8: extra_opts += ["--no-build-isolation"] +run = partial( + subprocess.run, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, +) + def test_babelize_init_c(tmpdir, datadir): runner = CliRunner() @@ -27,25 +35,28 @@ def test_babelize_init_c(tmpdir, datadir): assert pathlib.Path("pymt_heat").exists() assert (pathlib.Path("pymt_heat") / "babel.toml").is_file() + print(run(["which", "python"]).stdout) + print(run(["which", "pip"]).stdout) + try: - result = subprocess.run( - ["pip", "install", "-e", "."] + extra_opts, + result = run(["python", "setup.py", "build_ext"], cwd="pymt_heat") + except subprocess.CalledProcessError as err: + assert err.output is None, err.output + + try: + result = run( + ["python", "-m", "pip", "install", "-e", "."] + extra_opts, cwd="pymt_heat", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output - assert result.returncode == 0 os.mkdir("_test") shutil.copy(datadir / "config.txt", "_test/") try: - result = subprocess.run( + result = run( [ "bmi-test", "--config-file=config.txt", @@ -54,12 +65,7 @@ def test_babelize_init_c(tmpdir, datadir): "-vvv", ], cwd="_test", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output - assert result.returncode == 0 diff --git a/external/tests/test_cxx.py b/external/tests/test_cxx.py index a0b2fa24..06815c7b 100644 --- a/external/tests/test_cxx.py +++ b/external/tests/test_cxx.py @@ -5,16 +5,24 @@ import shutil import subprocess import sys +from functools import partial from click.testing import CliRunner from babelizer.cli import babelize - -extra_opts = [] +extra_opts: list[str] = [] if sys.platform.startswith("linux") and int(platform.python_version_tuple()[1]) <= 8: extra_opts += ["--no-build-isolation"] +run = partial( + subprocess.run, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, +) + def test_babelize_init_cxx(tmpdir, datadir): runner = CliRunner() @@ -28,13 +36,9 @@ def test_babelize_init_cxx(tmpdir, datadir): assert (pathlib.Path("pymt_heat") / "babel.toml").is_file() try: - result = subprocess.run( - ["pip", "install", "-e", "."] + extra_opts, + result = run( + ["python", "-m", "pip", "install", "-e", "."] + extra_opts, cwd="pymt_heat", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output @@ -45,7 +49,7 @@ def test_babelize_init_cxx(tmpdir, datadir): shutil.copy(datadir / "config.txt", "_test/") try: - result = subprocess.run( + result = run( [ "bmi-test", "--config-file=config.txt", @@ -54,10 +58,6 @@ def test_babelize_init_cxx(tmpdir, datadir): "-vvv", ], cwd="_test", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output diff --git a/external/tests/test_fortran.py b/external/tests/test_fortran.py index f1a5fba9..d8f739ea 100644 --- a/external/tests/test_fortran.py +++ b/external/tests/test_fortran.py @@ -5,16 +5,24 @@ import shutil import subprocess import sys +from functools import partial from click.testing import CliRunner from babelizer.cli import babelize - -extra_opts = [] +extra_opts: list[str] = [] if sys.platform.startswith("linux") and int(platform.python_version_tuple()[1]) <= 8: extra_opts += ["--no-build-isolation"] +run = partial( + subprocess.run, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, +) + def test_babelize_init_fortran(tmpdir, datadir): runner = CliRunner() @@ -28,13 +36,9 @@ def test_babelize_init_fortran(tmpdir, datadir): assert (pathlib.Path("pymt_heatf") / "babel.toml").is_file() try: - result = subprocess.run( - ["pip", "install", "-e", "."] + extra_opts, + result = run( + ["python", "-m", "pip", "install", "-e", "."] + extra_opts, cwd="pymt_heatf", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output @@ -45,7 +49,7 @@ def test_babelize_init_fortran(tmpdir, datadir): shutil.copy(datadir / "sample.cfg", "_test/") try: - result = subprocess.run( + result = run( [ "bmi-test", "--config-file=sample.cfg", @@ -54,10 +58,6 @@ def test_babelize_init_fortran(tmpdir, datadir): "-vvv", ], cwd="_test", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output diff --git a/external/tests/test_python.py b/external/tests/test_python.py index 138bcc3e..800f92db 100644 --- a/external/tests/test_python.py +++ b/external/tests/test_python.py @@ -3,8 +3,9 @@ import pathlib import shutil import subprocess -import pytest +from functools import partial +import pytest from click.testing import CliRunner from babelizer.cli import babelize @@ -16,6 +17,15 @@ def sessiondir(tmpdir_factory): return sdir +run = partial( + subprocess.run, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, +) + + def test_babelize_init_python(sessiondir, datadir): runner = CliRunner() @@ -28,14 +38,11 @@ def test_babelize_init_python(sessiondir, datadir): assert (pathlib.Path("pymt_heatpy") / "babel.toml").is_file() try: - result = subprocess.run( - ["pip", "install", "-e", "."], + result = run( + ["python", "-m", "pip", "install", "-e", "."], cwd="pymt_heatpy", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) + except subprocess.CalledProcessError as err: assert err.output is None, err.output @@ -47,7 +54,7 @@ def test_babelize_init_python(sessiondir, datadir): shutil.copy(datadir / "heat.yaml", "_test/") try: - result = subprocess.run( + result = run( [ "bmi-test", "--config-file=heat.yaml", @@ -56,10 +63,6 @@ def test_babelize_init_python(sessiondir, datadir): "-vvv", ], cwd="_test", - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as err: assert err.output is None, err.output diff --git a/news/.gitignore b/news/.gitignore new file mode 100644 index 00000000..f935021a --- /dev/null +++ b/news/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/news/83.bugfix.1 b/news/83.bugfix.1 new file mode 100644 index 00000000..692cd44d --- /dev/null +++ b/news/83.bugfix.1 @@ -0,0 +1,4 @@ + +Changed the template *pyproject.toml* to require *setuptools < 60* for the +build system. Newer versions of *setuptools* caused fortran builds to break +because *numpy* does not support newer versions of *setuptools*. diff --git a/news/83.misc.1 b/news/83.misc.1 new file mode 100644 index 00000000..f491342b --- /dev/null +++ b/news/83.misc.1 @@ -0,0 +1,4 @@ + +Added a [nox](https://nox.thea.codes/en/stable) file that automates routine +project maintenance tasks (e.g. running the linters, building the docs, +running the tests, etc.). diff --git a/news/83.misc.2 b/news/83.misc.2 new file mode 100644 index 00000000..ea912289 --- /dev/null +++ b/news/83.misc.2 @@ -0,0 +1,2 @@ + +Removed lots of lint that was uncovered by the new linters. diff --git a/news/83.misc.3 b/news/83.misc.3 new file mode 100644 index 00000000..369dbabe --- /dev/null +++ b/news/83.misc.3 @@ -0,0 +1,2 @@ + +Set up *towncrier* for managing the changelog. diff --git a/news/83.removal.1 b/news/83.removal.1 new file mode 100644 index 00000000..aad60204 --- /dev/null +++ b/news/83.removal.1 @@ -0,0 +1,3 @@ + +Removed the deprecated and unused ``babelizer.wrap`` module (use +:func:`~babelizer.render.render` instead). diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..331681dc --- /dev/null +++ b/noxfile.py @@ -0,0 +1,287 @@ +import os +import pathlib +import shutil +import sys +from itertools import chain + +import nox + +PROJECT = "babelizer" +ROOT = pathlib.Path(__file__).parent +ALL_LANGS = {"c", "cxx", "fortran", "python"} +PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] + + +@nox.session(python=PYTHON_VERSIONS) +def test(session: nox.Session) -> None: + """Run the tests.""" + session.install(".[dev,testing]") + + args = session.posargs or ["-n", "auto", "--cov", PROJECT, "-vvv"] + if "CI" in os.environ: + args.append("--cov-report=xml:$(pwd)/coverage.xml") + session.run("pytest", *args) + + +@nox.session( + name="test-langs", + python=PYTHON_VERSIONS, + venv_backend="mamba", +) +@nox.parametrize("lang", ["c", "cxx", "fortran", "python"]) +def test_langs(session: nox.session, lang) -> None: + datadir = ROOT / "external" / "tests" / f"test_{lang}" + tmpdir = pathlib.Path(session.create_tmp()) + testdir = tmpdir / "_test" + testdir.mkdir() + + package, library, config_file = _get_package_metadata(datadir) + session.debug(package) + session.debug(library) + session.debug(config_file) + + build_examples(session, lang) + + session.conda_install("pip", "bmi-tester>=0.5.4") + session.install(".[testing]") + + with session.chdir(tmpdir): + session.run( + "babelize", + "init", + str(datadir / "babel.toml"), + ) + + for k, v in sorted(session.env.items()): + session.debug(f"{k}: {v!r}") + + with session.chdir(package): + session.run("python", "-m", "pip", "install", "-e", ".") + + with session.chdir(testdir): + shutil.copy(datadir / config_file, ".") + session.run( + "bmi-test", + f"--config-file={config_file}", + "--root-dir=.", + f"{package}:{library}", + "-vvv", + ) + + +def _get_package_metadata(datadir): + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + + with open(datadir / "babel.toml", "rb") as fp: + config = tomllib.load(fp) + package = config["package"]["name"] + library = list(config["library"])[0] + config_files = [ + fname.name for fname in datadir.iterdir() if fname.name != "babel.toml" + ] + return package, library, config_files[0] + + +@nox.session(name="build-examples", venv_backend="mamba") +@nox.parametrize("lang", ["c", "cxx", "fortran", "python"]) +def build_examples(session: nox.Session, lang): + """Build the language examples.""" + srcdir = ROOT / "external" / f"bmi-example-{lang}" + builddir = pathlib.Path(session.create_tmp()) / "_build" + + if lang == "python": + session.conda_install("bmipy", "make") + else: + session.conda_install(f"bmi-{lang}", "cmake", "make", "pkg-config") + + for k, v in sorted(session.env.items()): + session.debug(f"{k}: {v!r}") + + if lang == "python": + session.run("make", "-C", str(srcdir), "install") + else: + builddir.mkdir() + with session.chdir(builddir): + session.run( + "cmake", + "-S", + str(srcdir), + "-B", + ".", + f"-DCMAKE_INSTALL_PREFIX={session.env['CONDA_PREFIX']}", + ) + session.run("make", "install") + + +@nox.session(name="test-cli") +@nox.session(python=PYTHON_VERSIONS) +def test_cli(session: nox.Session) -> None: + """Test the command line interface.""" + session.install(".") + session.run("babelize", "--version") + session.run("babelize", "--help") + session.run("babelize", "init", "--help") + session.run("babelize", "update", "--help") + session.run("babelize", "generate", "--help") + + +@nox.session +def lint(session: nox.Session) -> None: + """Look for lint.""" + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files", "--verbose") + + +@nox.session +def towncrier(session: nox.Session) -> None: + """Check that there is a news fragment.""" + session.install("towncrier") + session.run("towncrier", "check", "--compare-with", "origin/develop") + + +@nox.session(name="build-requirements", reuse_venv=True) +def build_requirements(session: nox.Session) -> None: + """Create requirements files from pyproject.toml.""" + session.install("tomli") + + with open("requirements.txt", "w") as fp: + session.run("python", "requirements.py", stdout=fp) + + for extra in ["dev", "docs", "testing"]: + with open(f"requirements-{extra}.txt", "w") as fp: + session.run("python", "requirements.py", extra, stdout=fp) + + +@nox.session(name="build-docs", reuse_venv=True) +def build_docs(session: nox.Session) -> None: + """Build the docs.""" + with session.chdir(ROOT): + session.install(".[docs]") + + clean_docs(session) + + with session.chdir(ROOT): + session.run( + "sphinx-apidoc", + "-e", + "-force", + "--no-toc", + "--module-first", + # "--templatedir", + # "docs/_templates", + "-o", + "docs/source/api", + "babelizer", + ) + session.run( + "sphinx-build", + "-b", + "html", + # "-W", + "docs/source", + "build/html", + ) + + +@nox.session(name="live-docs", reuse_venv=True) +def live_docs(session: nox.Session) -> None: + """Build the docs with sphinx-autobuild""" + session.install("sphinx-autobuild") + session.install(".[docs]") + session.run( + "sphinx-apidoc", + "-e", + "-force", + "--no-toc", + "--module-first", + "--templatedir", + "docs/source/_templates", + "-o", + "docs/source/api", + "babelizer", + ) + session.run( + "sphinx-autobuild", + "-b", + "dirhtml", + "docs/source", + "build/html", + "--open-browser", + ) + + +@nox.session +def build(session: nox.Session) -> None: + """Build sdist and wheel dists.""" + session.install("pip") + session.install("wheel") + session.install("setuptools") + session.run("python", "--version") + session.run("pip", "--version") + session.run( + "python", "setup.py", "bdist_wheel", "sdist", "--dist-dir", "./wheelhouse" + ) + + +@nox.session +def release(session): + """Tag, build and publish a new release to PyPI.""" + session.install("zest.releaser[recommended]") + session.install("zestreleaser.towncrier") + session.run("fullrelease") + + +@nox.session +def publish_testpypi(session): + """Publish wheelhouse/* to TestPyPI.""" + session.run("twine", "check", "wheelhouse/*") + session.run( + "twine", + "upload", + "--skip-existing", + "--repository-url", + "https://test.pypi.org/legacy/", + "wheelhouse/*.tar.gz", + ) + + +@nox.session +def publish_pypi(session): + """Publish wheelhouse/* to PyPI.""" + session.run("twine", "check", "wheelhouse/*") + session.run( + "twine", + "upload", + "--skip-existing", + "wheelhouse/*.tar.gz", + ) + + +@nox.session(python=False) +def clean(session): + """Remove all .venv's, build files and caches in the directory.""" + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("wheelhouse", ignore_errors=True) + shutil.rmtree(f"{PROJECT}.egg-info", ignore_errors=True) + shutil.rmtree(".pytest_cache", ignore_errors=True) + shutil.rmtree(".venv", ignore_errors=True) + for p in chain(ROOT.rglob("*.py[co]"), ROOT.rglob("__pycache__")): + if p.is_dir(): + p.rmdir() + else: + p.unlink() + + +@nox.session(python=False, name="clean-docs") +def clean_docs(session: nox.Session) -> None: + """Clean up the docs folder.""" + with session.chdir(ROOT / "build"): + if os.path.exists("html"): + shutil.rmtree("html") + + with session.chdir(ROOT / "docs" / "source"): + for p in pathlib.Path("api").rglob("babelizer*.rst"): + p.unlink() diff --git a/paper/language_fig.svg b/paper/language_fig.svg index 01327602..627443d4 100644 --- a/paper/language_fig.svg +++ b/paper/language_fig.svg @@ -22,141 +22,141 @@ - - - - - - - - - - - - - - - @@ -166,25 +166,25 @@ z - @@ -195,101 +195,101 @@ z - - - - @@ -304,18 +304,18 @@ z - @@ -328,64 +328,64 @@ z - - @@ -399,133 +399,133 @@ z - - - - - - - @@ -544,56 +544,56 @@ z - - @@ -622,36 +622,36 @@ z - @@ -666,50 +666,50 @@ z - - - @@ -735,68 +735,68 @@ z - - - @@ -820,39 +820,39 @@ z - - diff --git a/paper/paper.md b/paper/paper.md index 4f14cd8a..1591c1fe 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -60,7 +60,7 @@ more innovation and experimentation driven from the bottom up by a community. It reduces redundancy--rather than reinventing software, scientists can find and use models that suit their needs--and it allows scientists -to focus on new and unsolved problems. +to focus on new and unsolved problems. There are disadvantages, however. Without a single group to guide model development, there is a @@ -188,5 +188,3 @@ under Grant No. 1831623, *Community Facility Support: The Community Surface Dynamics Modeling System (CSDMS)*. # References - - diff --git a/pyproject.toml b/pyproject.toml index 6c6b185e..9a988b3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,19 +55,30 @@ changelog = "https://github.com/csdms/babelizer/blob/master/CHANGES.rst" [project.optional-dependencies] dev = [ + "black", "flake8", "flake8-bugbear", + "isort", + "nox", "pre-commit", + "towncrier", ] docs = [ - "sphinx>=1.5.1, <3", + "sphinx>=4", "sphinx-click", + "sphinx-copybutton", + "sphinx-inline-tabs", + "sphinxcontrib.towncrier", + "pygments>=2.4", + "sphinx-inline-tabs", + "furo", ] testing = [ "pytest", "pytest-cov", "pytest-datadir", - "coverage", + "pytest-xdist", + "coverage[toml]", "coveralls", ] diff --git a/requirements-dev.txt b/requirements-dev.txt index 3ab6614d..bfbd367b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,9 @@ +# Requirements extracted from pyproject.toml +# [project.optional-dependencies.dev] black flake8 flake8-bugbear isort +nox pre-commit +towncrier diff --git a/requirements-docs.txt b/requirements-docs.txt index bdaf80b9..2f7798f6 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,10 @@ -sphinx>=1.5.1, <3 +# Requirements extracted from pyproject.toml +# [project.optional-dependencies.docs] +sphinx>=4 sphinx-click +sphinx-copybutton +sphinx-inline-tabs +sphinxcontrib.towncrier +pygments>=2.4 +sphinx-inline-tabs +furo diff --git a/requirements-testing.txt b/requirements-testing.txt index 2c50c55f..0956cbaa 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,8 @@ +# Requirements extracted from pyproject.toml +# [project.optional-dependencies.testing] pytest pytest-cov pytest-datadir -coverage +pytest-xdist +coverage[toml] coveralls diff --git a/requirements.py b/requirements.py new file mode 100755 index 00000000..dbe42cd7 --- /dev/null +++ b/requirements.py @@ -0,0 +1,44 @@ +#! /usr/bin/env python +import argparse +import os + + +def _find_tomllib(): + try: + import tomllib + except ModuleNotFoundError: + import tomli as tomllib + return tomllib + + +def requirements(extras): + + tomllib = _find_tomllib() + + with open("pyproject.toml", "rb") as fp: + project = tomllib.load(fp)["project"] + + dependencies = {} + if extras: + optional_dependencies = project.get("optional-dependencies", {}) + for extra in extras: + dependencies[ + f"[project.optional-dependencies.{extra}]" + ] = optional_dependencies[extra] + else: + dependencies["[project.dependencies]"] = project["dependencies"] + + print("# Requirements extracted from pyproject.toml") + for section, packages in dependencies.items(): + print(f"# {section}") + print(os.linesep.join(packages)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Extract requirements information from pyproject.toml" + ) + parser.add_argument("extras", type=str, nargs="*") + args = parser.parse_args() + + requirements(args.extras) diff --git a/requirements.txt b/requirements.txt index bf0f2875..8ea48b4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +# Requirements extracted from pyproject.toml +# [project.dependencies] black click gitpython diff --git a/setup.py b/setup.py index e147d05f..aabcef4a 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #! /usr/bin/env python from setuptools import setup - setup()