diff --git a/.gitignore b/.gitignore index 54107c0b..43fd6fab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,8 @@ -# 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/ +*.py[cod] .coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# 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/ - -# nox virtual envs .nox/ +__pycache__/ +build/ +dist/ +docs/source/api/babelizer*rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27b394d6..85d775c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black name: black @@ -35,7 +35,7 @@ repos: exclude: ^babelizer/data - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py310-plus] @@ -65,9 +65,18 @@ repos: - id: end-of-file-fixer - id: forbid-new-submodules - id: trailing-whitespace + - id: name-tests-test + exclude: ^external + - id: file-contents-sorter + files: | + (?x)^( + requirements(-\w+)?.(in|txt)| + external/requirements(-\w+)?.(in|txt)| + .gitignore + ) - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b9ced9be..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,19 +0,0 @@ -include *.rst -include requirements*.txt -include Makefile -exclude .gitmodules -exclude .readthedocs.yaml -recursive-include babelizer/data * -recursive-exclude docs * -recursive-exclude paper * -recursive-include tests * -recursive-include external/bmi-example-c * -recursive-include external/bmi-example-cxx * -recursive-include external/bmi-example-fortran * -recursive-include external/bmi-example-python * -recursive-include external/tests * -include external/requirements.txt -exclude external/bmi-example-c/.git* -exclude external/bmi-example-cxx/.git* -exclude external/bmi-example-fortran/.git* -exclude external/bmi-example-python/.git* diff --git a/babelizer/_cookiecutter.py b/babelizer/_cookiecutter.py new file mode 100644 index 00000000..8d068f3f --- /dev/null +++ b/babelizer/_cookiecutter.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import os +from collections.abc import Iterable +from datetime import datetime +from typing import Any + +from jinja2 import Environment +from jinja2 import FileSystemLoader +from jinja2 import StrictUndefined +from jinja2 import Template + +from babelizer._datadir import get_template_dir +from babelizer._post_hook import run +from babelizer._utils import as_cwd + + +def cookiecutter( + template: str, + context: dict[str, Any] | None = None, + output_dir: str = ".", +) -> None: + if context is None: + context = {} + env = babelizer_environment(template) + + def datetime_format(value: datetime, format_: str = "%Y-%M-%D") -> str: + return value.strftime(format_) + + env.filters["datetimeformat"] = datetime_format + + for dirpath, _dirnames, filenames in os.walk(template): + rel_path = os.path.relpath(dirpath, template) + target_dir = os.path.join(output_dir, render_path(rel_path, context)) + + if not os.path.exists(target_dir): + os.makedirs(target_dir) + + for filename in filenames: + target_path = os.path.join(target_dir, render_path(filename, context)) + + with open(target_path, "w") as fp: + fp.write( + env.get_template(os.path.join(rel_path, filename)).render(**context) + ) + + with as_cwd(output_dir): + run(context) + + +def babelizer_environment(template: str | None = None) -> Environment: + if template is None: + template = get_template_dir() + + return Environment(loader=FileSystemLoader(template), undefined=StrictUndefined) + + +def render_path( + path: str, + context: dict[str, Any], + remove_extension: Iterable[str] = (".jinja", ".jinja2", ".j2"), +) -> str: + """Render a path as though it were a jinja template. + + Parameters + ---------- + path : str + A path. + context : dict + Context to use for substitution. + remove_extension : iterable of str, optional + If the provided path ends with one of these exensions, + the extension will be removed from the rendered path. + + Examples + -------- + >>> from babelizer._cookiecutter import render_path + >>> render_path("{{foo}}.py", {"foo": "bar"}) + 'bar.py' + >>> render_path("{{foo}}.py.jinja", {"foo": "bar"}) + 'bar.py' + >>> render_path("bar.py.j2", {"foo": "bar"}) + 'bar.py' + >>> render_path("{{bar}}.py.jinja", {"foo": "bar"}) + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'bar' is undefined + """ + rendered_path = Template(path, undefined=StrictUndefined).render(**context) + + root, ext = os.path.splitext(rendered_path) + return rendered_path if ext not in remove_extension else root diff --git a/babelizer/_datadir.py b/babelizer/_datadir.py index afb08e36..00fc70b5 100644 --- a/babelizer/_datadir.py +++ b/babelizer/_datadir.py @@ -10,3 +10,7 @@ def get_datadir() -> str: return str(importlib_resources.files("babelizer") / "data") + + +def get_template_dir() -> str: + return str(importlib_resources.files("babelizer") / "data" / "templates") diff --git a/babelizer/_files/bmi_py.py b/babelizer/_files/bmi_py.py index 1d00e5b3..e032fcde 100644 --- a/babelizer/_files/bmi_py.py +++ b/babelizer/_files/bmi_py.py @@ -5,30 +5,30 @@ from typing import Any -def render(plugin_metadata: Mapping[str, Any]) -> str: +def render(context: Mapping[str, Any]) -> str: """Render _bmi.py.""" - languages = {library["language"] for library in plugin_metadata["library"].values()} + languages = {library["language"] for library in context["library"].values()} assert len(languages) == 1 language = languages.pop() if language == "python": - return _render_bmi_py(plugin_metadata) + return _render_bmi_py(context) else: - return _render_bmi_c(plugin_metadata) + return _render_bmi_c(context) -def _render_bmi_c(plugin_metadata: Mapping[str, Any]) -> str: +def _render_bmi_c(context: Mapping[str, Any]) -> str: """Render _bmi.py for a non-python library.""" - languages = [library["language"] for library in plugin_metadata["library"].values()] + languages = [library["language"] for library in context["library"].values()] language = languages[0] assert language in ("c", "c++", "fortran") imports = [ - f"from {plugin_metadata['package']['name']}.lib import {cls}" - for cls in plugin_metadata["library"] + f"from {context['package']['name']}.lib import {cls}" + for cls in context["library"] ] - names = [f" {cls!r},".replace("'", '"') for cls in plugin_metadata["library"]] + names = [f" {cls!r},".replace("'", '"') for cls in context["library"]] return f"""\ {os.linesep.join(sorted(imports))} @@ -39,9 +39,9 @@ def _render_bmi_c(plugin_metadata: Mapping[str, Any]) -> str: """ -def _render_bmi_py(plugin_metadata: Mapping[str, Any]) -> str: +def _render_bmi_py(context: Mapping[str, Any]) -> str: """Render _bmi.py for a python library.""" - languages = [library["language"] for library in plugin_metadata["library"].values()] + languages = [library["language"] for library in context["library"].values()] language = languages[0] assert language == "python" @@ -56,7 +56,7 @@ def _render_bmi_py(plugin_metadata: Mapping[str, Any]) -> str: imports = [ f"from {component['library']} import {component['entry_point']} as {cls}" - for cls, component in plugin_metadata["library"].items() + for cls, component in context["library"].items() ] rename = [ @@ -66,10 +66,10 @@ def _render_bmi_py(plugin_metadata: Mapping[str, Any]) -> str: """.replace( "'", '"' ) - for cls in plugin_metadata["library"] + for cls in context["library"] ] - names = [f" {cls!r},".replace("'", '"') for cls in plugin_metadata["library"]] + names = [f" {cls!r},".replace("'", '"') for cls in context["library"]] return f"""\ {header} diff --git a/babelizer/_files/gitignore.py b/babelizer/_files/gitignore.py index 12961061..51cbbe8d 100644 --- a/babelizer/_files/gitignore.py +++ b/babelizer/_files/gitignore.py @@ -5,11 +5,11 @@ from typing import Any -def render(plugin_metadata: Mapping[str, Any]) -> str: +def render(context: Mapping[str, Any]) -> str: """Render a .gitignore file.""" - package_name = plugin_metadata["package"]["name"] + package_name = context["package"]["name"] - languages = {library["language"] for library in plugin_metadata["library"].values()} + languages = {library["language"] for library in context["library"].values()} ignore = { "*.egg-info/", "*.py[cod]", @@ -22,7 +22,7 @@ def render(plugin_metadata: Mapping[str, Any]) -> str: if "python" not in languages: ignore |= {"*.o", "*.so"} | { - f"{package_name}/lib/{cls.lower()}.c" for cls in plugin_metadata["library"] + f"{package_name}/lib/{cls.lower()}.c" for cls in context["library"] } if "fortran" in languages: diff --git a/babelizer/_files/init_py.py b/babelizer/_files/init_py.py index 5999df84..3e692715 100644 --- a/babelizer/_files/init_py.py +++ b/babelizer/_files/init_py.py @@ -5,16 +5,14 @@ from typing import Any -def render(plugin_metadata: Mapping[str, Any]) -> str: +def render(context: Mapping[str, Any]) -> str: """Render __init__.py.""" - package_name = plugin_metadata["package"]["name"] + package_name = context["package"]["name"] imports = [f"from {package_name}._version import __version__"] - imports += [ - f"from {package_name}._bmi import {cls}" for cls in plugin_metadata["library"] - ] + imports += [f"from {package_name}._bmi import {cls}" for cls in context["library"]] - names = [f" {cls!r},".replace("'", '"') for cls in plugin_metadata["library"]] + names = [f" {cls!r},".replace("'", '"') for cls in context["library"]] return f"""\ {os.linesep.join(sorted(imports))} diff --git a/babelizer/_files/lib_init_py.py b/babelizer/_files/lib_init_py.py index 269b655c..e726f141 100644 --- a/babelizer/_files/lib_init_py.py +++ b/babelizer/_files/lib_init_py.py @@ -5,15 +5,15 @@ from typing import Any -def render(plugin_metadata: Mapping[str, Any]) -> str: +def render(context: Mapping[str, Any]) -> str: """Render lib/__init__.py.""" - package_name = plugin_metadata["package"]["name"] + package_name = context["package"]["name"] imports = [ f"from {package_name}.lib.{cls.lower()} import {cls}" - for cls in plugin_metadata["library"] + for cls in context["library"] ] - names = [f" {cls!r},".replace("'", '"') for cls in plugin_metadata["library"]] + names = [f" {cls!r},".replace("'", '"') for cls in context["library"]] return f"""\ {os.linesep.join(sorted(imports))} diff --git a/babelizer/_files/license_rst.py b/babelizer/_files/license_rst.py index 1d5bc49d..beb65bb1 100644 --- a/babelizer/_files/license_rst.py +++ b/babelizer/_files/license_rst.py @@ -5,13 +5,13 @@ from typing import Any -def render(plugin_metadata: Mapping[str, Any]) -> str: +def render(context: Mapping[str, Any]) -> str: """Render LICENSE.rst.""" - license_name = plugin_metadata["info"]["package_license"] + license_name = context["info"]["package_license"] kwds = { - "full_name": plugin_metadata["info"]["package_author"], + "full_name": context["info"]["package_author"], "year": datetime.now().year, - "project_short_description": plugin_metadata["info"]["summary"], + "project_short_description": context["info"]["summary"], } return LICENSE[license_name].format(**kwds) diff --git a/babelizer/_files/readme.py b/babelizer/_files/readme.py index f9cdbf92..bd2eec57 100644 --- a/babelizer/_files/readme.py +++ b/babelizer/_files/readme.py @@ -2,14 +2,10 @@ from typing import Any -from jinja2 import Environment -from jinja2 import FileSystemLoader - -from babelizer._datadir import get_datadir +from babelizer._cookiecutter import babelizer_environment def render(context: dict[str, Any]) -> str: - env = Environment(loader=FileSystemLoader(get_datadir())) - template = env.get_template("{{cookiecutter.package_name}}/README.rst") + template = babelizer_environment().get_template("README.rst") return template.render(**context) diff --git a/babelizer/_post_hook.py b/babelizer/_post_hook.py new file mode 100644 index 00000000..6f48e7ac --- /dev/null +++ b/babelizer/_post_hook.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import errno +import os +import re +from collections import defaultdict +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +from logoizer import logoize + +# PROJECT_DIRECTORY = Path.cwd().resolve() + + +# def remove_file(filepath: Path) -> None: +# filepath.unlink(filepath) +# # (PROJECT_DIRECTORY / filepath).unlink(filepath) + + +# def remove_folder(folderpath): +# shutil.rmtree(PROJECT_DIRECTORY / folderpath) + + +# def make_folder(folderpath): +# try: +# # (PROJECT_DIRECTORY / folderpath).mkdir(parents=True, exist_ok=True) +# folderpath.mkdir(parents=True, exist_ok=True) +# except OSError: +# pass + + +def clean_folder(folderpath: Path, keep: Iterable[str | Path] = ()) -> None: + keep = {str((folderpath / path).resolve()) for path in keep} + # if keep: + # keep = set([str((folderpath / path).resolve()) for path in keep]) + # else: + # keep = set() + + # folderpath = PROJECT_DIRECTORY / folderpath + for fname in folderpath.glob("*"): + if not fname.is_dir() and str(fname.resolve()) not in keep: + fname.unlink() + + try: + folderpath.rmdir() + except OSError as err: + if err.errno != errno.ENOTEMPTY: + raise + + +def split_file(filepath: Path, include_preamble: bool = False) -> set[str]: + filepath = Path(filepath) + SPLIT_START_REGEX = re.compile(r"\s*#\s*start:\s*(?P\S+)\s*") + + files = defaultdict(list) + fname = "preamble" + with open(filepath) as fp: + for line in fp: + m = SPLIT_START_REGEX.match(line) + if m: + fname = m["fname"] + files[fname].append(line) + + preamble = files.pop("preamble") + folderpath = filepath.parent + for name, contents in files.items(): + with open(folderpath / name, "w") as fp: + if include_preamble: + fp.write("".join(preamble)) + print("".join(contents).strip(), file=fp) + # fp.write("".join(contents).strip()) + + return set(files) + + +def write_api_yaml(folderpath: Path, **kwds: str) -> Path: + # make_folder(folderpath) + os.makedirs(folderpath, exist_ok=True) + + # api_yaml = PROJECT_DIRECTORY / folderpath / "api.yaml" + api_yaml = folderpath / "api.yaml" + contents = """\ +name: {package_name} +language: {language} +package: {package_name} +class: {plugin_class} +""".format( + **kwds + ) + with open(api_yaml, "w") as fp: + fp.write(contents) + + return api_yaml + + +def remove_trailing_whitespace(path: str | Path) -> None: + with open(path) as fp: + lines = [line.rstrip() for line in fp] + with open(path, "w") as fp: + print(os.linesep.join(lines), file=fp) + + +def run(context: dict[str, Any]) -> None: + PROJECT_DIRECTORY = Path.cwd().resolve() + + package_name = context["package"]["name"] + language = context["language"] + + LIB_DIRECTORY = PROJECT_DIRECTORY / Path(package_name, "lib") + + keep = set() + + static_dir = PROJECT_DIRECTORY / "docs" / "_static" + # make_folder(PROJECT_DIRECTORY / static_dir) + os.makedirs(PROJECT_DIRECTORY / static_dir, exist_ok=True) + + logoize(package_name, static_dir / "logo-light.svg", light=True) + logoize(package_name, static_dir / "logo-dark.svg", light=False) + + remove_trailing_whitespace(static_dir / "logo-dark.svg") + remove_trailing_whitespace(static_dir / "logo-light.svg") + + if language == "c": + keep |= {"__init__.py", "bmi.c", "bmi.h"} + keep |= split_file(LIB_DIRECTORY / "_c.pyx", include_preamble=True) + elif language == "c++": + keep |= {"__init__.py", "bmi.hxx"} + keep |= split_file(LIB_DIRECTORY / "_cxx.pyx", include_preamble=True) + elif language == "fortran": + keep |= { + "__init__.py", + "bmi.f90", + "bmi_interoperability.f90", + "bmi_interoperability.h", + } + keep |= split_file(LIB_DIRECTORY / "_fortran.pyx", include_preamble=True) + + clean_folder(LIB_DIRECTORY, keep=keep) + + if language == "python": + os.remove(PROJECT_DIRECTORY / "meson.build") + + datadir = Path("meta") + package_datadir = Path(package_name) / "data" + if not package_datadir.exists(): + package_datadir.symlink_to(".." / datadir, target_is_directory=True) + + for babelized_class in context["components"]: + write_api_yaml( + PROJECT_DIRECTORY / datadir / babelized_class, + language=language, + plugin_class=babelized_class, + package_name=package_name, + ) diff --git a/babelizer/_utils.py b/babelizer/_utils.py new file mode 100644 index 00000000..7b18b670 --- /dev/null +++ b/babelizer/_utils.py @@ -0,0 +1,192 @@ +"""Utility functions used by the babelizer.""" + +from __future__ import annotations + +import os +import pathlib +import subprocess +import sys +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Sequence +from contextlib import contextmanager +from contextlib import suppress +from typing import Any + +from babelizer.errors import SetupPyError +from babelizer.errors import ValidationError + + +def execute(args: Sequence[str]) -> subprocess.CompletedProcess[bytes]: + """Run a command through the ``subprocess`` module. + + Parameters + ---------- + args : list + Command and arguments to command. + + Returns + ------- + ~subprocess.CompletedProcess + results from :func:`subprocess.run`. + """ + return subprocess.run(args, capture_output=True, check=True) + + +def setup_py(*args: str) -> list[str]: + """Format the command to build/install the babelized package. + + Returns + ------- + list of str + The build/install command. + """ + return [sys.executable, "setup.py"] + list(args) + + +def get_setup_py_version() -> str | None: + """Get babelized 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: + execute(setup_py("egg_info")) + except subprocess.CalledProcessError as err: + stderr = err.stderr.decode("utf-8") + if "Traceback" in stderr: + raise SetupPyError(stderr) from None + return None + result = execute(setup_py("--version")) + return result.stdout.splitlines()[0].decode("utf-8") + else: + return None + + +@contextmanager +def save_files(files: Iterable[str]) -> Generator[dict[str, str], None, None]: + """Generate repository files through a context. + + Parameters + ---------- + files : list of str + List of path-like objects. + + Yields + ------ + str + Generator for repository files. + """ + contents = {} + for file_ in files: + with suppress(FileNotFoundError), open(file_) as fp: + contents[file_] = fp.read() + yield contents + for file_ in contents: + with open(file_, "w") as fp: + fp.write(contents[file_]) + + +@contextmanager +def as_cwd(path: str) -> Generator[None, None, None]: + """Change directory context. + + Parameters + ---------- + path : str + Path-like object to a directory. + """ + prev_cwd = os.getcwd() + os.chdir(path) + yield + os.chdir(prev_cwd) + + +def parse_entry_point(specifier: str) -> tuple[str, str, str]: + """Parse an entry point specifier into its parts. + + Parameters + ---------- + specifier : str + An entry-point specifier. + + Returns + ------- + tuple of str + The parts of the entry point as (*name*, *module*, *class*). + + Raises + ------ + ValidationError + If the entry point cannot be parsed. + + Examples + -------- + >>> from babelizer._utils import parse_entry_point + >>> parse_entry_point("Foo=bar:Baz") + ('Foo', 'bar', 'Baz') + + >>> parse_entry_point("bar:Baz") + Traceback (most recent call last): + ... + 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(":")) + except ValueError: + raise ValidationError( + f"bad entry point specifier ({specifier}). specifier must be of" + " the form name=module:class" + ) from None + + return name, module, obj + + +def validate_dict_keys( + meta: dict[str, Any], + required: Iterable[str] | None = None, + optional: Iterable[str] | None = None, +) -> None: + """Validate the keys of a dict. + + Parameters + ---------- + meta : dict + Dictionary to validate. + required : dict, optional + Required keys in configuration. + optional : dict, optional + Optional keys in configuration. + + Raises + ------ + ValidationError + Raised for invalid dict. + """ + actual = set(meta) + required = set() if required is None else set(required) + optional = required if optional is None else set(optional) + valid = required | optional + + if missing := required - actual: + raise ValidationError( + "missing required key{}: {}".format( + "s" if len(missing) > 1 else "", ", ".join(missing) + ) + ) + + if unknown := actual - valid: + raise ValidationError( + "unknown key{}: {}".format( + "s" if len(unknown) > 1 else "", ", ".join(unknown) + ) + ) diff --git a/babelizer/cli.py b/babelizer/cli.py index 42b49ac8..029f191d 100644 --- a/babelizer/cli.py +++ b/babelizer/cli.py @@ -14,19 +14,19 @@ import click import git -from babelizer._datadir import get_datadir +from babelizer._datadir import get_template_dir from babelizer._files.gitignore import render as render_gitignore from babelizer._files.license_rst import render as render_license from babelizer._files.meson_build import render as render_meson_build from babelizer._files.readme import render as render_readme +from babelizer._utils import get_setup_py_version +from babelizer._utils import save_files +from babelizer.config import BabelConfig from babelizer.errors import OutputDirExistsError from babelizer.errors import ScanError from babelizer.errors import SetupPyError from babelizer.errors import ValidationError -from babelizer.metadata import BabelMetadata from babelizer.render import render -from babelizer.utils import get_setup_py_version -from babelizer.utils import save_files out = partial(click.secho, bold=True, err=True) err = partial(click.secho, fg="red", err=True) @@ -68,7 +68,7 @@ def babelize(cd: str) -> None: @click.option( "--template", default=None, - help="Location of cookiecutter template", + help="Location of templates", ) @click.option( "--package-version", @@ -83,21 +83,22 @@ def init( META is babelizer configuration information, usually saved to a file. """ - output = "." - template = template or get_datadir() + template = template or get_template_dir() if not quiet: out(f"reading template from {template}") fmt = pathlib.Path(meta.name).suffix[1:] or "toml" try: - babel_metadata = BabelMetadata.from_stream(cast(io.TextIOBase, meta), fmt=fmt) + babel_config = BabelConfig.from_stream(cast(io.TextIOBase, meta), fmt=fmt) except (ScanError, ValidationError) as error: raise BabelizerAbort(str(error)) + output = babel_config["package"]["name"] + try: new_folder = render( - babel_metadata, + babel_config, output, template=template, clobber=False, @@ -111,6 +112,7 @@ def init( "Don't forget to drop model metadata files into" f" {os.path.join(new_folder, 'meta')}" ) + repo = git.Repo(new_folder) repo.git.add("--all") repo.index.commit("Initial commit") @@ -134,7 +136,7 @@ def init( @click.option( "--template", default=None, - help="Location of cookiecutter template", + help="Location of templates", ) @click.option( "--set-version", default=None, help="Set the version of the updated package" @@ -147,24 +149,24 @@ def update( package_path = os.path.realpath(".") for fname in ("babel.toml", "babel.yaml", "plugin.yaml"): # if (package_path / fname).is_file(): - # metadata_path = package_path / fname + # config_path = package_path / fname if os.path.isfile(os.path.join(package_path, fname)): - metadata_path = os.path.join(package_path, fname) + config_path = os.path.join(package_path, fname) break else: - metadata_path = None + config_path = None - if not metadata_path: + if not config_path: err("this does not appear to be a babelized folder (missing 'babel.toml')") raise click.Abort() - template = template or get_datadir() + template = template or get_template_dir() if not quiet: out(f"reading template from {template}") try: - babel_metadata = BabelMetadata.from_path(metadata_path) + babel_config = BabelConfig.from_path(config_path) except ValidationError as error: raise BabelizerAbort(str(error)) @@ -185,7 +187,7 @@ def update( out(f"re-rendering {package_path}") with save_files(["CHANGES.rst", "CREDITS.rst"]): render( - babel_metadata, + babel_config, os.path.dirname(package_path), # package_path.parent, template=template, @@ -194,7 +196,7 @@ def update( ) extra_files = _repo_contents(package_path) - _generated_files( - babel_metadata, template=template, version=version + babel_config, template=template, version=version ) ignore = ["meta*", "notebooks*", "docs*", "**/data"] @@ -272,21 +274,19 @@ def sample_meson_build(extension: Collection[str]) -> None: @babelize.command() def sample_readme() -> None: context = { - "cookiecutter": { - "language": "python", - "open_source_license": "MIT License", - "package_name": "springfield_monorail", - "info": { - "github_username": "lyle-lanley", - "package_author": "Lyle Lanley", - "summary": "A Monorail!", - "package_license": "MIT License", - }, - "components": { - "Monorail": {"library": "monorail"}, - "Rail": {"library": "rail"}, - }, - } + "language": "python", + "open_source_license": "MIT License", + "info": { + "github_username": "lyle-lanley", + "package_author": "Lyle Lanley", + "summary": "A Monorail!", + "package_license": "MIT License", + }, + "components": { + "Monorail": {"library": "monorail"}, + "Rail": {"library": "rail"}, + }, + "package": {"name": "springfield_monorail"}, } print(render_readme(context)) @@ -310,11 +310,11 @@ def _repo_contents(base: str) -> set[str]: def _generated_files( - babel_metadata: BabelMetadata, template: str | None = None, version: str = "0.1" + babel_config: BabelConfig, template: str | None = None, version: str = "0.1" ) -> set[str]: with tempfile.TemporaryDirectory() as tmpdir: new_folder = render( - babel_metadata, + babel_config, tmpdir, template=template, version=version, diff --git a/babelizer/metadata.py b/babelizer/config.py similarity index 56% rename from babelizer/metadata.py rename to babelizer/config.py index c407cae1..1e466be8 100644 --- a/babelizer/metadata.py +++ b/babelizer/config.py @@ -1,4 +1,4 @@ -"""Library metadata used by the babelizer to wrap libraries.""" +"""Library configuration used by the babelizer to wrap libraries.""" from __future__ import annotations @@ -9,7 +9,6 @@ from collections import defaultdict from collections.abc import Callable from collections.abc import Generator -from collections.abc import Iterable from collections.abc import Mapping from contextlib import suppress from typing import Any @@ -22,63 +21,14 @@ else: # pragma: no cover ( None: - """Validate babelizer configuration 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) - optional = required if optional is None else set(optional) - valid = required | optional - - if missing := required - actual: - raise ValidationError( - "missing required key{}: {}".format( - "s" if len(missing) > 1 else "", ", ".join(missing) - ) - ) - - if unknown := actual - valid: - raise ValidationError( - "unknown key{}: {}".format( - "s" if len(unknown) > 1 else "", ", ".join(unknown) - ) - ) - - -def _norm_os(name: str) -> str: - if name == "linux": - name = "ubuntu" - elif name == "mac": - name = "macos" - if not name.endswith("-latest"): - name += "-latest" - return name - - -class BabelMetadata(Mapping[str, Any]): - """Library metadata.""" +class BabelConfig(Mapping[str, Any]): + """Babelizer configuration.""" LOADERS: dict[str, Callable[[str], dict[str, Any]]] = { "yaml": yaml.safe_load, @@ -94,7 +44,7 @@ def __init__( plugin: dict[str, Any] | None = None, ci: dict[str, Any] | None = None, ): - """Metadata used by the babelizer to wrap a library. + """Configuration used by the babelizer to wrap a library. Parameters ---------- @@ -127,11 +77,11 @@ def __init__( "ci": dict(ci or {}), } - BabelMetadata.validate(config) + BabelConfig.validate(config) - self._meta = BabelMetadata.norm(config) + self._meta = BabelConfig.norm(config) - def __getitem__(self, key: str) -> str: + def __getitem__(self, key: str) -> dict[str, Any]: return self._meta[key] def __iter__(self) -> Generator[str, None, None]: @@ -141,8 +91,8 @@ def __len__(self) -> int: return len(self._meta) @classmethod - def from_stream(cls, stream: io.TextIOBase, fmt: str = "toml") -> BabelMetadata: - """Create an instance of BabelMetadata from a file-like object. + def from_stream(cls, stream: io.TextIOBase, fmt: str = "toml") -> BabelConfig: + """Create an instance of BabelConfig from a file-like object. Parameters ---------- @@ -153,28 +103,28 @@ def from_stream(cls, stream: io.TextIOBase, fmt: str = "toml") -> BabelMetadata: Returns ------- - BabelMetadata - A BabelMetadata instance. + BabelConfig + A BabelConfig instance. """ try: - loader = BabelMetadata.LOADERS[fmt] + loader = BabelConfig.LOADERS[fmt] except KeyError: raise ValueError(f"unrecognized format ({fmt})") try: meta = loader(stream.read()) except yaml.scanner.ScannerError as error: - raise ScanError(f"unable to scan yaml-formatted metadata file:\n{error}") + raise ScanError(f"unable to scan yaml-formatted config file:\n{error}") except tomllib.TOMLDecodeError as error: - raise ScanError(f"unable to scan toml-formatted metadata file:\n{error}") + raise ScanError(f"unable to scan toml-formatted config file:\n{error}") else: if not isinstance(meta, dict): - raise ValidationError("metadata file does not contain a mapping object") + raise ValidationError("config file does not contain a mapping object") return cls(**meta) @classmethod - def from_path(cls, filepath: str) -> BabelMetadata: - """Create an instance of BabelMetadata from a path-like object. + def from_path(cls, filepath: str) -> BabelConfig: + """Create an instance of BabelConfig from a path-like object. Parameters ---------- @@ -183,44 +133,46 @@ def from_path(cls, filepath: str) -> BabelMetadata: Returns ------- - A BabelMetadata instance. + A BabelConfig instance. """ path = pathlib.Path(filepath) with open(filepath) as fp: - return BabelMetadata.from_stream(fp, fmt=path.suffix[1:]) + return BabelConfig.from_stream(fp, fmt=path.suffix[1:]) @staticmethod def validate(config: dict[str, Any]) -> None: - """Ensure babelizer configuration metadata are valid. + """Ensure babelizer configuration is valid. Parameters ---------- config : dict - Metadata to babelize a library. + Configuration to babelize a library. Raises ------ ValidationError - If metadata are not valid. + If configuration is not valid. """ libraries = config["library"] if "entry_point" in libraries: - validate_dict(libraries, required=("language", "entry_point"), optional={}) + validate_dict_keys( + libraries, required=("language", "entry_point"), optional={} + ) for entry_point in libraries["entry_point"]: try: - BabelMetadata.parse_entry_point(entry_point) + parse_entry_point(entry_point) except ValidationError: raise ValidationError(f"poorly-formed entry point ({entry_point})") else: for _babelized_class, library in libraries.items(): - validate_dict( + validate_dict_keys( library, required={"language", "library", "header", "entry_point"}, optional={}, ) - validate_dict( + validate_dict_keys( config["build"], required=None, optional=( @@ -232,11 +184,13 @@ def validate(config: dict[str, Any]) -> None: "extra_compile_args", ), ) - validate_dict(config["package"], required=("name", "requirements"), optional={}) - validate_dict(config["ci"], required=("python_version", "os"), optional={}) + validate_dict_keys( + config["package"], required=("name", "requirements"), optional={} + ) + validate_dict_keys(config["ci"], required=("python_version", "os"), optional={}) try: - validate_dict( + validate_dict_keys( config["info"], required=( "package_author", @@ -248,7 +202,7 @@ def validate(config: dict[str, Any]) -> None: optional={}, ) except ValidationError: - validate_dict( + validate_dict_keys( config["info"], required=( "plugin_author", @@ -277,9 +231,7 @@ def _header_ext(language: str) -> str: libraries = {} for entry_point in entry_points: - babelized_class, library_name, class_name = BabelMetadata.parse_entry_point( - entry_point - ) + babelized_class, library_name, class_name = parse_entry_point(entry_point) libraries[babelized_class] = { "language": language, "library": library_name, @@ -301,37 +253,41 @@ def _handle_old_style_info(info: dict[str, Any]) -> dict[str, Any]: @staticmethod def norm(config: dict[str, Any]) -> dict[str, Any]: - """Ensure current style metadata are used in babelizer configuration. + """Ensure current style is used in babelizer configuration. Parameters ---------- config : dict - Metadata to babelize a library. + Configuration to babelize a library. Return ------ dict - A dict of babelizer configuration metadata. + A dict of babelizer configuration. """ build: dict[str, list[str]] = defaultdict(list) with suppress(KeyError): build.update(config["build"]) if "entry_point" in config["library"]: - libraries = BabelMetadata._handle_old_style_entry_points(config["library"]) + libraries = BabelConfig._handle_old_style_entry_points(config["library"]) else: libraries = {k: dict(v) for k, v in config["library"].items()} if "plugin_author" in config["info"]: - info = BabelMetadata._handle_old_style_info(config["info"]) + info = BabelConfig._handle_old_style_info(config["info"]) else: info = config["info"] if "all" in config["ci"]["os"]: config["ci"] = ["linux", "mac", "windows"] + languages = [lib["language"] for lib in config["library"].values()] + language = languages[0] + return { "library": libraries, + "components": config["library"], "build": { "undef_macros": build["undef_macros"], "define_macros": build["define_macros"], @@ -349,10 +305,11 @@ def norm(config: dict[str, Any]) -> dict[str, Any]: "python_version": sorted(config["ci"]["python_version"]), "os": sorted(config["ci"]["os"]), }, + "language": language, } def dump(self, fp: io.TextIOBase, fmt: str = "toml") -> None: - """Write serialized metadata to a file. + """Write serialized configuration to a file. Parameters ---------- @@ -364,7 +321,7 @@ def dump(self, fp: io.TextIOBase, fmt: str = "toml") -> None: print(self.format(fmt=fmt), file=fp, end="") def format(self, fmt: str = "toml") -> str: - """Serialize metadata to output format. + """Serialize configuration to output format. Parameters ---------- @@ -373,95 +330,17 @@ def format(self, fmt: str = "toml") -> str: Returns ------- - metadata : str - Serialized metadata. + config : str + Serialized configuration. """ return getattr(self, f"format_{fmt}")() def format_toml(self) -> str: - """Serialize metadata as TOML. + """Serialize configuration as TOML. Returns ------- str - Serialized metadata as a TOML-formatted string + Serialized configuration as a TOML-formatted string """ return tomli_w.dumps(self._meta, multiline_strings=True) - - @staticmethod - def parse_entry_point(specifier: str) -> tuple[str, str, str]: - """Parse an entry point specifier into its parts. - - Parameters - ---------- - specifier : str - An entry-point specifier. - - Returns - ------- - tuple of str - The parts of the entry point as (*name*, *module*, *class*). - - Raises - ------ - ValidationError - If the entry point cannot be parsed. - - Examples - -------- - >>> from babelizer.metadata import BabelMetadata - >>> BabelMetadata.parse_entry_point("Foo=bar:Baz") - ('Foo', 'bar', 'Baz') - - >>> BabelMetadata.parse_entry_point("bar:Baz") - Traceback (most recent call last): - ... - 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(":")) - except ValueError: - raise ValidationError( - f"bad entry point specifier ({specifier}). specifier must be of the form name=module:class" - ) from None - - return name, module, obj - - def as_cookiecutter_context(self) -> dict[str, Any]: - """Format metadata in cookiecutter context. - - Returns - ------- - dict - Metadata in cookiecutter context. - """ - languages = [lib["language"] for lib in self._meta["library"].values()] - language = languages[0] - platforms = [_norm_os(name) for name in self._meta["ci"]["os"]] - - return { - "components": self._meta["library"], - "build": { - "undef_macros": self._meta["build"]["undef_macros"], - "define_macros": self._meta["build"]["define_macros"], - "libraries": self._meta["build"]["libraries"], - "library_dirs": self._meta["build"]["library_dirs"], - "include_dirs": self._meta["build"]["include_dirs"], - "extra_compile_args": self._meta["build"]["extra_compile_args"], - }, - "info": { - "full_name": self._meta["info"]["package_author"], - "email": self._meta["info"]["package_author_email"], - "github_username": self._meta["info"]["github_username"], - "project_short_description": self._meta["info"]["summary"], - }, - "ci": { - "os": platforms, - "python_version": self._meta["ci"]["python_version"], - }, - "package_name": self._meta["package"]["name"], - "package_requirements": ",".join(self._meta["package"]["requirements"]), - "language": language, - "open_source_license": self._meta["info"]["package_license"], - } diff --git a/babelizer/data/cookiecutter.json b/babelizer/data/cookiecutter.json deleted file mode 100644 index f64b3dc1..00000000 --- a/babelizer/data/cookiecutter.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "components": {}, - "build": { - "undef_macros": [], - "define_macros": [], - "libraries": [], - "library_dirs": [], - "include_dirs": [], - "extra_compile_args": [] - }, - "info": { - "full_name": "CSDMS", - "email": "csdms@colorado.edu", - "github_username": "csdms", - "project_short_description": "PyMT plugin for {{cookiecutter.package_name}}" - }, - "ci": { - "os": ["ubuntu", "macos", "windows"], - "python_version": ["3.10", "3.11", "3.12"] - }, - "package_name": "package", - "package_requirements": "", - "package_version": "0.1", - "language": ["c", "c++", "fortran", "python"], - "open_source_license": ["MIT License", "BSD License", "ISC License", "Apache Software License 2.0", "GNU General Public License v3", "Not open source"], - "files": {} -} diff --git a/babelizer/data/hooks/post_gen_project.py b/babelizer/data/hooks/post_gen_project.py deleted file mode 100644 index 0be9b268..00000000 --- a/babelizer/data/hooks/post_gen_project.py +++ /dev/null @@ -1,147 +0,0 @@ -#! /usr/bin/env python -import errno -import os -import re -from collections import defaultdict -from pathlib import Path - -from logoizer import logoize - - -PROJECT_DIRECTORY = Path.cwd().resolve() -LIB_DIRECTORY = Path("{{ cookiecutter.package_name }}", "lib") - - -def remove_file(filepath): - (PROJECT_DIRECTORY / filepath).unlink(filepath) - - -def remove_folder(folderpath): - shutil.rmtree(PROJECT_DIRECTORY / folderpath) - - -def make_folder(folderpath): - try: - (PROJECT_DIRECTORY / folderpath).mkdir(parents=True, exist_ok=True) - except OSError: - pass - - -def clean_folder(folderpath, keep=None): - if keep: - keep = set([str((folderpath / path).resolve()) for path in keep]) - else: - keep = set() - - folderpath = PROJECT_DIRECTORY / folderpath - for fname in folderpath.glob("*"): - if not fname.is_dir() and str(fname.resolve()) not in keep: - fname.unlink() - - try: - folderpath.rmdir() - except OSError as err: - if err.errno != errno.ENOTEMPTY: - raise - - -def split_file(filepath, include_preamble=False): - filepath = Path(filepath) - SPLIT_START_REGEX = re.compile(r"\s*#\s*start:\s*(?P\S+)\s*") - - files = defaultdict(list) - fname = "preamble" - with open(filepath, "r") as fp: - for line in fp: - m = SPLIT_START_REGEX.match(line) - if m: - fname = m["fname"] - files[fname].append(line) - - preamble = files.pop("preamble") - folderpath = filepath.parent - for name, contents in files.items(): - with open(folderpath / name, "w") as fp: - if include_preamble: - fp.write("".join(preamble)) - print("".join(contents).strip(), file=fp) - # fp.write("".join(contents).strip()) - - return set(files) - - -def write_api_yaml(folderpath, **kwds): - make_folder(folderpath) - - api_yaml = PROJECT_DIRECTORY / folderpath / "api.yaml" - contents = """\ -name: {package_name} -language: {language} -package: {package_name} -class: {plugin_class} -""".format(**kwds) - with open(api_yaml, "w") as fp: - fp.write(contents) - - return api_yaml - - -def remove_trailing_whitespace(path): - with open(path) as fp: - lines = [line.rstrip() for line in fp] - with open(path, "w") as fp: - print(os.linesep.join(lines), file=fp) - - -if __name__ == "__main__": - keep = set() - - static_dir = PROJECT_DIRECTORY / "docs" / "_static" - make_folder(static_dir) - - logoize("{{ cookiecutter.package_name }}", static_dir / "logo-light.svg", light=True) - logoize("{{ cookiecutter.package_name }}", static_dir / "logo-dark.svg", light=False) - - remove_trailing_whitespace(static_dir / "logo-dark.svg") - remove_trailing_whitespace(static_dir / "logo-light.svg") - - {%- if cookiecutter.language == 'c' %} - - keep |= set(["__init__.py", "bmi.c", "bmi.h"]) - keep |= split_file(LIB_DIRECTORY / "_c.pyx", include_preamble=True) - - {%- elif cookiecutter.language == 'c++' %} - - keep |= set(["__init__.py", "bmi.hxx"]) - keep |= split_file(LIB_DIRECTORY / "_cxx.pyx", include_preamble=True) - - {%- elif cookiecutter.language == 'fortran' %} - - keep |= set(["__init__.py", "bmi.f90", "bmi_interoperability.f90", - "bmi_interoperability.h"]) - keep |= split_file(LIB_DIRECTORY / "_fortran.pyx", include_preamble=True) - - {%- endif %} - - clean_folder(LIB_DIRECTORY, keep=keep) - - if "Not open source" == "{{ cookiecutter.open_source_license }}": - remove_file("LICENSE") - - {%- if cookiecutter.language == 'python' %} - remove_file("meson.build") - {%- endif %} - - datadir = Path("meta") - package_datadir = Path("{{ cookiecutter.package_name }}") / "data" - if not package_datadir.exists(): - package_datadir.symlink_to(".." / datadir, target_is_directory=True) - -{%- for babelized_class, component in cookiecutter.components|dictsort %} - write_api_yaml( - datadir / "{{ babelized_class }}", - language="{{ component.language }}", - plugin_class="{{ babelized_class }}", - package_name="{{ cookiecutter.package_name }}", - ) -{% endfor %} diff --git a/babelizer/data/hooks/pre_gen_project.py b/babelizer/data/hooks/pre_gen_project.py deleted file mode 100644 index e03b932d..00000000 --- a/babelizer/data/hooks/pre_gen_project.py +++ /dev/null @@ -1,45 +0,0 @@ -import re -import sys - -MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" - -module_name = "{{ cookiecutter.package_name }}" - - -if not re.match(MODULE_REGEX, module_name): - print( - "ERROR: The project slug (%s) is not a valid Python module name. Please do not use a - and use _ instead" - % module_name - ) - - # Exit to cancel project - sys.exit(1) - - -def is_valid_entry_point(entry_point): - try: - babelized_class, plugin_entry_point = entry_point.split("=") - except ValueError: - return False - try: - plugin_module, plugin_class = plugin_entry_point.split(":") - except ValueError: - return False - - if not re.match(MODULE_REGEX, babelized_class): - return False - for module_name in plugin_module.split("."): - if not re.match(MODULE_REGEX, module_name): - return False - if not re.match(MODULE_REGEX, plugin_class): - return False - - return True - - -{%- for babelized_class, component in cookiecutter.components|dictsort %} -if not is_valid_entry_point(entry_point := "{{ babelized_class }}={{ component.library }}:{{ component.entry_point}}"): - print(f"ERROR: The entry point ({entry_point}) is not a valid Python entry point.") - - sys.exit(2) -{% endfor %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/lint.yml b/babelizer/data/templates/.github/workflows/lint.yml similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/.github/workflows/lint.yml rename to babelizer/data/templates/.github/workflows/lint.yml diff --git a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml b/babelizer/data/templates/.github/workflows/test.yml similarity index 79% rename from babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml rename to babelizer/data/templates/.github/workflows/test.yml index 35416232..469cd6c0 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/.github/workflows/test.yml +++ b/babelizer/data/templates/.github/workflows/test.yml @@ -19,8 +19,8 @@ jobs: strategy: matrix: - os: [{{ cookiecutter.ci.os | join(", ") }}] - python-version: [{{ cookiecutter.ci.python_version | join(", ") }}] + os: [{{ ci.os | join(", ") }}] + python-version: [{{ ci.python_version | join(", ") }}] steps: - uses: actions/checkout@v4 @@ -51,7 +51,7 @@ jobs: - name: Test run: | - python -c 'import {{ cookiecutter.package_name }}' - {%- for babelized_class in cookiecutter.components %} - bmi-test {{ cookiecutter.package_name }}.bmi:{{ babelized_class }} -vvv + python -c 'import {{ package.name }}' + {%- for babelized_class in components %} + bmi-test {{ package.name }}.bmi:{{ babelized_class }} -vvv {%- endfor %} diff --git a/babelizer/data/templates/.gitignore b/babelizer/data/templates/.gitignore new file mode 100644 index 00000000..2ef470b7 --- /dev/null +++ b/babelizer/data/templates/.gitignore @@ -0,0 +1 @@ +{{ files['.gitignore'] }} diff --git a/babelizer/data/{{cookiecutter.package_name}}/.pre-commit-config.yaml b/babelizer/data/templates/.pre-commit-config.yaml similarity index 97% rename from babelizer/data/{{cookiecutter.package_name}}/.pre-commit-config.yaml rename to babelizer/data/templates/.pre-commit-config.yaml index 519c7dea..372fa385 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/.pre-commit-config.yaml +++ b/babelizer/data/templates/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black name: black @@ -35,7 +35,7 @@ repos: - flake8-simplify - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py310-plus] @@ -61,7 +61,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst b/babelizer/data/templates/CHANGES.rst similarity index 76% rename from babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst rename to babelizer/data/templates/CHANGES.rst index e6e128b6..6d925e80 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/CHANGES.rst +++ b/babelizer/data/templates/CHANGES.rst @@ -6,7 +6,7 @@ Release Notes .. towncrier release notes start -0.1.0 ({% now 'local', '%Y-%m-%d' %}) +0.1.0 ({{ now|datetimeformat('%Y-%m-%d') }}) ------------------ - Initial release diff --git a/babelizer/data/templates/CREDITS.rst b/babelizer/data/templates/CREDITS.rst new file mode 100644 index 00000000..26e2150b --- /dev/null +++ b/babelizer/data/templates/CREDITS.rst @@ -0,0 +1,4 @@ +Credits +======= + +* {{ info.package_author }} <{{ info.package_author_email }}> diff --git a/babelizer/data/templates/LICENSE.rst b/babelizer/data/templates/LICENSE.rst new file mode 100644 index 00000000..79121ef7 --- /dev/null +++ b/babelizer/data/templates/LICENSE.rst @@ -0,0 +1 @@ +{{ files['LICENSE.rst'] }} diff --git a/babelizer/data/{{cookiecutter.package_name}}/README.rst b/babelizer/data/templates/README.rst similarity index 56% rename from babelizer/data/{{cookiecutter.package_name}}/README.rst rename to babelizer/data/templates/README.rst index 63353026..204291ff 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/README.rst +++ b/babelizer/data/templates/README.rst @@ -1,30 +1,26 @@ -{{ '=' * cookiecutter.package_name | length }} -{{ cookiecutter.package_name }} -{{ '=' * cookiecutter.package_name | length }} +{{ '=' * package.name | length }} +{{ package.name }} +{{ '=' * package.name | length }} -{% set is_open_source = cookiecutter.open_source_license != 'Not open source' -%} - -{% if is_open_source %} .. image:: https://img.shields.io/badge/CSDMS-Basic%20Model%20Interface-green.svg :target: https://bmi.readthedocs.io/ :alt: Basic Model Interface -.. image:: https://img.shields.io/badge/recipe-{{ cookiecutter.package_name }}-green.svg - :target: https://anaconda.org/conda-forge/{{ cookiecutter.package_name }} +.. image:: https://img.shields.io/badge/recipe-{{ package.name }}-green.svg + :target: https://anaconda.org/conda-forge/{{ package.name }} -.. image:: https://readthedocs.org/projects/{{ cookiecutter.package_name | replace("_", "-") }}/badge/?version=latest - :target: https://{{ cookiecutter.package_name | replace("_", "-") }}.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/{{ package.name | replace("_", "-") }}/badge/?version=latest + :target: https://{{ package.name | replace("_", "-") }}.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/test.yml/badge.svg - :target: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/test.yml +.. image:: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/test.yml/badge.svg + :target: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/test.yml -.. image:: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/flake8.yml/badge.svg - :target: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/flake8.yml +.. image:: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/flake8.yml/badge.svg + :target: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/flake8.yml -.. image:: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/black.yml/badge.svg - :target: https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/actions/workflows/black.yml -{%- endif %} +.. image:: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/black.yml/badge.svg + :target: https://github.com/{{ info.github_username }}/{{ package.name }}/actions/workflows/black.yml .. start-intro @@ -42,9 +38,9 @@ Python and the Python Modeling Toolkit, PyMT. * - Library - Component - PyMT - {% for babelized_class, component in cookiecutter.components|dictsort -%} + {% for babelized_class, component in components|dictsort -%} * - {{ component.library }} - - :class:`~{{ cookiecutter.package_name }}.{{ babelized_class }}` + - :class:`~{{ package.name }}.{{ babelized_class }}` - .. code-block:: pycon @@ -54,10 +50,8 @@ Python and the Python Modeling Toolkit, PyMT. .. end-intro -{% if is_open_source %} -* Free software: {{ cookiecutter.open_source_license }} -* Documentation: https://{{ cookiecutter.package_name | replace("_", "-") }}.readthedocs.io. -{% endif %} +* Free software: {{ info.package_license }} +* Documentation: https://{{ package.name | replace("_", "-") }}.readthedocs.io. Quickstart @@ -65,22 +59,22 @@ Quickstart .. start-quickstart -To get started you will need to install the *{{ cookiecutter.package_name }}* package. +To get started you will need to install the *{{ package.name }}* package. Here are two ways to do so. Install from conda-forge ------------------------ -If the *{{ cookiecutter.package_name }}* package is distributed on *conda-forge*, install it into your current environment with *conda*. +If the *{{ package.name }}* package is distributed on *conda-forge*, install it into your current environment with *conda*. .. code:: bash - conda install -c conda-forge {{ cookiecutter.package_name }} + conda install -c conda-forge {{ package.name }} Install from source ------------------- -You can build and install the *{{ cookiecutter.package_name }}* package from source using *conda* and *pip*. +You can build and install the *{{ package.name }}* package from source using *conda* and *pip*. First, from the source directory, install package dependencies into your current environment with *conda*. @@ -89,7 +83,7 @@ First, from the source directory, install package dependencies into your current conda install -c conda-forge --file requirements.txt --file requirements-build.txt --file requirements-library.txt Then install the package itself with *pip*. -{%- if cookiecutter.language == 'python' %} +{%- if language == 'python' %} .. code:: bash @@ -115,16 +109,16 @@ There are two ways to use the components provided by this package: directly thro Model Interface (BMI), or as a PyMT plugin. A BMI is provided by each component in this package: -{%- for babelized_class, component in cookiecutter.components|dictsort -%} -:class:`~{{ cookiecutter.package_name}}.{{ babelized_class }}` +{%- for babelized_class, component in components|dictsort -%} +:class:`~{{ package.name}}.{{ babelized_class }}` {% endfor %}. -{% for babelized_class, component in cookiecutter.components|dictsort -%} +{% for babelized_class, component in components|dictsort -%} .. code-block:: pycon - >>> from {{ cookiecutter.package_name}} import {{ babelized_class }} + >>> from {{ package.name}} import {{ babelized_class }} >>> model = {{ babelized_class }}() >>> model.get_component_name() # Get the name of the component >>> model.get_output_var_names() # Get a list of the component's output variables diff --git a/babelizer/data/templates/docs/api/.gitignore b/babelizer/data/templates/docs/api/.gitignore new file mode 100644 index 00000000..1c3775f5 --- /dev/null +++ b/babelizer/data/templates/docs/api/.gitignore @@ -0,0 +1,2 @@ +# auto-generated with sphinx-apidoc +{{ package.name }}*.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/authors.rst b/babelizer/data/templates/docs/authors.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/authors.rst rename to babelizer/data/templates/docs/authors.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/babel.rst b/babelizer/data/templates/docs/babel.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/babel.rst rename to babelizer/data/templates/docs/babel.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/changelog.rst b/babelizer/data/templates/docs/changelog.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/changelog.rst rename to babelizer/data/templates/docs/changelog.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py b/babelizer/data/templates/docs/conf.py.jinja similarity index 88% rename from babelizer/data/{{cookiecutter.package_name}}/docs/conf.py rename to babelizer/data/templates/docs/conf.py.jinja index 57c39b28..e1512332 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/conf.py +++ b/babelizer/data/templates/docs/conf.py.jinja @@ -1,4 +1,4 @@ -# {{ cookiecutter.package_name }} documentation build configuration file, created by +# {{ package.name }} 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 @@ -18,7 +18,7 @@ import os import pathlib -import {{ cookiecutter.package_name }} +import {{ package.name }} docs_dir = os.path.dirname(__file__) @@ -58,18 +58,18 @@ master_doc = "index" # General information about the project. -project = "{{ cookiecutter.package_name }}" -copyright = "{% now 'local', '%Y' %}, {{ cookiecutter.info.full_name }}" -author = "{{ cookiecutter.info.full_name }}" +project = "{{ package.name }}" +copyright = "{{ now|datetimeformat('%Y') }}, {{ info.package_author }}" +author = "{{ info.package_author }}" # 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. -version = {{ cookiecutter.package_name }}.__version__ +version = {{ package.name }}.__version__ # The full version, including alpha/beta/rc tags. -release = {{ cookiecutter.package_name }}.__version__ +release = {{ package.name }}.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -96,7 +96,7 @@ # a list of builtin themes. # html_theme = "furo" -html_title = "{{ cookiecutter.package_name }}" +html_title = "{{ package.name }}" # Theme options are theme-specific and customize the look and feel of a @@ -105,7 +105,7 @@ # html_theme_options = { "announcement": None, - "source_repository": "https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/", + "source_repository": "https://github.com/{{ info.github_username }}/{{ package.name }}/", "source_branch": "main", "source_directory": "docs", "sidebar_hide_name": False, @@ -132,7 +132,7 @@ # -- Options for HTMLHelp output --------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = "{{ cookiecutter.package_name }}doc" +htmlhelp_basename = "{{ package.name }}doc" # -- Options for intersphinx extension --------------------------------------- diff --git a/babelizer/data/templates/docs/developer_install.rst b/babelizer/data/templates/docs/developer_install.rst new file mode 100644 index 00000000..200b2840 --- /dev/null +++ b/babelizer/data/templates/docs/developer_install.rst @@ -0,0 +1,58 @@ +.. _developer_install: + +================= +Developer Install +================= + +.. important:: + + The following commands will install *{{ package.name }}* into your current environment. Although + not necessary, we **highly recommend** you install *{{ package.name }}* into its own + :ref:`virtual environment `. + +If you will be modifying code or contributing new code to *{{ package.name }}*, you will first +need to get *{{ package.name }}*'s source code and then install *{{ package.name }}* from that code. + +Source Install +-------------- + +*{{ package.name }}* is actively being developed on GitHub, where the code is freely available. +If you would like to modify or contribute code, you can either clone our +repository + +.. code-block:: bash + + git clone git://github.com/pymt-lab/{{ package.name }}.git + +or download the `tarball `_ +(a zip file is available for Windows users): + +.. code-block:: bash + + curl -OL https://github.com/{{ info.github_username }}/{{ package.name }}/tarball/master + +Once you have a copy of the source code, you can install it into your current +Python environment, + +.. tab:: mamba + + .. code-block:: bash + + cd {{ package.name }} + mamba install --file=requirements.txt + pip install -e . + +.. tab:: conda + + .. code-block:: bash + + cd {{ package.name }} + conda install --file=requirements.txt + pip install -e . + +.. tab:: pip + + .. code-block:: bash + + cd {{ package.name }} + pip install -e . diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/environments.rst b/babelizer/data/templates/docs/environments.rst similarity index 85% rename from babelizer/data/{{cookiecutter.package_name}}/docs/environments.rst rename to babelizer/data/templates/docs/environments.rst index 7b1f4c09..e94c3c3f 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/environments.rst +++ b/babelizer/data/templates/docs/environments.rst @@ -18,15 +18,15 @@ should stick with *pip*. .. code-block:: bash conda install mamba -c conda-forge - mamba create -n {{ cookiecutter.package_name }} - mamba activate {{ cookiecutter.package_name }} + mamba create -n {{ package.name }} + mamba activate {{ package.name }} .. tab:: conda .. code-block:: bash - conda create -n {{ cookiecutter.package_name }} - conda activate {{ cookiecutter.package_name }} + conda create -n {{ package.name }} + conda activate {{ package.name }} .. tab:: venv diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/index.rst b/babelizer/data/templates/docs/index.rst similarity index 66% rename from babelizer/data/{{cookiecutter.package_name}}/docs/index.rst rename to babelizer/data/templates/docs/index.rst index b3c2c429..6ea496b2 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/index.rst +++ b/babelizer/data/templates/docs/index.rst @@ -1,15 +1,15 @@ .. image:: _static/logo-light.svg :align: center :scale: 15% - :alt: {{ cookiecutter.package_name }} - :target: https://{{ cookiecutter.package_name }}.readthedocs.org/ + :alt: {{ package.name }} + :target: https://{{ package.name }}.readthedocs.org/ :class: only-light .. image:: _static/logo-dark.svg :align: center :scale: 15% - :alt: {{ cookiecutter.package_name }} - :target: https://{{ cookiecutter.package_name }}.readthedocs.org/ + :alt: {{ package.name }} + :target: https://{{ package.name }}.readthedocs.org/ :class: only-dark .. include:: ../README.rst @@ -23,7 +23,7 @@ quickstart usage - API + API babel .. contributing diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/license.rst b/babelizer/data/templates/docs/license.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/license.rst rename to babelizer/data/templates/docs/license.rst diff --git a/babelizer/data/templates/docs/quickstart.rst b/babelizer/data/templates/docs/quickstart.rst new file mode 100644 index 00000000..aa878fcf --- /dev/null +++ b/babelizer/data/templates/docs/quickstart.rst @@ -0,0 +1,18 @@ +Quickstart +========== + +.. note:: + + The following commands will install *{{ package.name}}* into your current + environment. Although not necessary, we **highly recommend** you install + *{{ package.name}}* into its own + :ref:`virtual environment `. + +.. include:: ../README.rst + :start-after: .. start-quickstart + :end-before: .. end-quickstart + +If you would like the very latest development version of *{{ package.name}}* +or want to modify or contribute code to the *{{ package.name}}* project, +you will need to do a :ref:`developer installation ` of +*{{ package.name }}* from source. diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/updating.rst b/babelizer/data/templates/docs/updating.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/updating.rst rename to babelizer/data/templates/docs/updating.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/usage.rst b/babelizer/data/templates/docs/usage.rst similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/docs/usage.rst rename to babelizer/data/templates/docs/usage.rst diff --git a/babelizer/data/templates/meson.build b/babelizer/data/templates/meson.build new file mode 100644 index 00000000..8275e9ce --- /dev/null +++ b/babelizer/data/templates/meson.build @@ -0,0 +1,99 @@ +project( + '{{ package.name }}', +{%- if language == 'c' %} + 'c', +{%- elif language == 'c++' %} + 'cpp', +{%- elif language == 'fortran' %} + 'fortran', +{%- endif %} + 'cython', + version: '{{ package_version }}', +) + +py = import('python').find_installation(pure: false) + +{%- if language == 'c' %} +compiler = meson.get_compiler('c') +{%- elif language == 'c++' %} +compiler = meson.get_compiler('cpp') +{%- elif language == 'fortran' %} +compiler = meson.get_compiler('fortran') +{%- endif %} + +# python_inc = py.get_path('data') / 'include' +numpy_inc = run_command( + py, + [ + '-c', + 'import numpy; print(numpy.get_include())' + ], + check: true +).stdout().strip() +incs = include_directories( + [ + '{{ package.name }}/lib', + # python_inc, + numpy_inc, + ] +) + +{% set dependency_list = package.requirements -%} +deps = [ +{%- for dependency in dependency_list if dependency != '' %} + compiler.find_library('{{ dependency }}'), +{%- endfor %} +] + +# Files get copied to /site-packages/ +install_pkg_srcs = [ + '{{ package.name }}/__init__.py', + '{{ package.name }}/_bmi.py', + '{{ package.name }}/_version.py', +] +py.install_sources( + install_pkg_srcs, + subdir: '{{ package.name }}', +) + +install_lib_srcs = [ + '{{ package.name }}/lib/__init__.py', +{%- for babelized_class in components|list|sort %} + '{{ package.name }}/lib/{{ babelized_class|lower }}.pyx', +{%- endfor %} +] +py.install_sources( + install_lib_srcs, + subdir: '{{ package.name }}/lib', +) + + +{%- for babelized_class, component in components|dictsort %} +py.extension_module( + '{{ babelized_class|lower }}', + [ +{%- if language == 'fortran' %} + '{{ package.name }}/lib/bmi_interoperability.f90', +{%- endif %} + '{{ package.name }}/lib/{{ babelized_class|lower }}.pyx', + ], + dependencies: [ + dependency('{{ component.library }}', method : 'pkg-config'), + ], + include_directories: incs, + install: true, + subdir: '{{ package.name }}/lib', +{%- if language == 'c++' %} + override_options : ['cython_language=cpp'], +{%- endif %} +) + +install_subdir( + 'meta/{{ babelized_class }}', + install_dir: py.get_install_dir() / '{{ package.name }}/data', +) + +{%- endfor %} + +# This is a temporary fix for editable installs. +run_command('cp', '-r', '{{ package.name }}/data', 'build') diff --git a/babelizer/data/{{cookiecutter.package_name}}/noxfile.py b/babelizer/data/templates/noxfile.py.jinja similarity index 93% rename from babelizer/data/{{cookiecutter.package_name}}/noxfile.py rename to babelizer/data/templates/noxfile.py.jinja index 756f3c99..ce09dd6d 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/noxfile.py +++ b/babelizer/data/templates/noxfile.py.jinja @@ -5,7 +5,7 @@ import nox -PROJECT = "{{ cookiecutter.package_name }}" +PROJECT = "{{ package.name }}" ROOT = pathlib.Path(__file__).parent PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] @@ -19,10 +19,10 @@ def test(session: nox.Session) -> None: session.install(".[testing]") -{%- for babelized_class, _ in cookiecutter.components|dictsort %} +{%- for babelized_class, _ in components|dictsort %} session.run( "bmi-test", - "{{ cookiecutter.package_name }}.bmi:{{ babelized_class }}", + "{{ package.name }}.bmi:{{ babelized_class }}", "-vvv", ) {%- endfor %} @@ -80,7 +80,7 @@ def build_docs(session: nox.Session) -> None: "--module-first", "-o", "docs/api", - "{{ cookiecutter.package_name }}", + "{{ package.name }}", ) session.run( "sphinx-build", @@ -106,7 +106,7 @@ def live_docs(session: nox.Session) -> None: "docs/_templates", "-o", "docs/api", - "{{ cookiecutter.package_name }}", + "{{ package.name }}", ) session.run( "sphinx-autobuild", @@ -188,5 +188,5 @@ def clean_docs(session: nox.Session) -> None: shutil.rmtree("html") with session.chdir(ROOT / "docs"): - for p in pathlib.Path("api").rglob("{{ cookiecutter.package_name }}*.rst"): + for p in pathlib.Path("api").rglob("{{ package.name }}*.rst"): p.unlink() diff --git a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml b/babelizer/data/templates/pyproject.toml similarity index 60% rename from babelizer/data/{{cookiecutter.package_name}}/pyproject.toml rename to babelizer/data/templates/pyproject.toml index 7df13701..8dfedece 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/pyproject.toml +++ b/babelizer/data/templates/pyproject.toml @@ -1,28 +1,28 @@ [build-system] -{%- if cookiecutter.language == 'python' %} +{%- if language == 'python' %} build-backend = "setuptools.build_meta" requires = [ "setuptools >=61", ] -{% else %} +{%- else %} build-backend = "mesonpy" requires = ["cython", "numpy", "meson-python", "wheel"] -{% endif %} +{%- endif %} [project] -name = "{{cookiecutter.package_name}}" +name = "{{package.name}}" authors = [ - {name = "{{cookiecutter.info.full_name}}", email = "{{cookiecutter.info.email}}"}, + {name = "{{info.package_author}}", email = "{{info.package_author_email}}"}, ] maintainers = [ - {name = "{{cookiecutter.info.full_name}}", email = "{{cookiecutter.info.email}}"}, + {name = "{{info.package_author}}", email = "{{info.package_author_email}}"}, ] -description = "PyMT plugin for {{cookiecutter.package_name}}" +description = "PyMT plugin for {{package.name}}" license = {text = "MIT License"} classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", - "License :: OSI Approved :: {{ cookiecutter.open_source_license }}", + "License :: OSI Approved :: {{ info.package_license }}", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", @@ -38,11 +38,11 @@ dependencies = [ ] [project.urls] -homepage = "https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}" +homepage = "https://github.com/{{ info.github_username }}/{{ package.name }}" [project.entry-points."pymt.plugins"] -{%- for babelized_class, _ in cookiecutter.components|dictsort %} -{{ babelized_class }} = "{{ cookiecutter.package_name }}.bmi:{{ babelized_class }}" +{%- for babelized_class, _ in components|dictsort %} +{{ babelized_class }} = "{{ package.name }}.bmi:{{ babelized_class }}" {%- endfor %} [project.optional-dependencies] @@ -65,15 +65,24 @@ testing = [ "bmi-tester>=0.5.4", ] -{%- if cookiecutter.language == 'python' %} +{%- if language == 'python' %} + +[tool.setuptools.dynamic.version] +attr = "{{package.name}}._version.__version__" + +[tool.setuptools.package-data] +{{package.name}} = [ + "data/**/*", +] + [tool.setuptools.packages.find] where = ["."] -include = ["{{cookiecutter.package_name}}*"] -{% endif %} +include = ["{{package.name}}*"] +{%- endif %} [tool.pytest.ini_options] minversion = "5.0" -testpaths = ["{{ cookiecutter.package_name }}", "tests"] +testpaths = ["{{ package.name }}", "tests"] norecursedirs = [".*", "*.egg*", "build", "dist"] addopts = """ --ignore setup.py @@ -98,20 +107,20 @@ line_length = 88 [tool.check-manifest] ignore = [ - "{{ cookiecutter.package_name }}/data", - "{{ cookiecutter.package_name }}/data/**/*", + "{{ package.name }}/data", + "{{ package.name }}/data/**/*", ] [tool.towncrier] directory = "news" -package = "{{ cookiecutter.package_name }}" +package = "{{ package.name }}" filename = "CHANGES.rst" single_file = true underlines = "-^\"" -issue_format = "`#{issue} `_" +issue_format = "`#{issue} `_" title_format = "{version} ({project_date})" wrap = true [tool.zest-releaser] tag-format = "v{version}" -python-file-with-version = "{{ cookiecutter.package_name }}/_version.py" +python-file-with-version = "{{ package.name }}/_version.py" diff --git a/babelizer/data/templates/requirements-build.txt b/babelizer/data/templates/requirements-build.txt new file mode 100644 index 00000000..c5683652 --- /dev/null +++ b/babelizer/data/templates/requirements-build.txt @@ -0,0 +1,11 @@ +# conda requirements needed to build the project +{% if language == 'c' -%} +bmi-c +c-compiler +{%- elif language == 'c++' -%} +bmi-cxx +cxx-compiler +{%- elif language == 'fortran' -%} +bmi-fortran +fortran-compiler +{%- endif %} diff --git a/babelizer/data/templates/requirements-library.txt b/babelizer/data/templates/requirements-library.txt new file mode 100644 index 00000000..dd03ffab --- /dev/null +++ b/babelizer/data/templates/requirements-library.txt @@ -0,0 +1,3 @@ +{%- for requirement in package.requirements -%} +{{ requirement|trim }} +{%- endfor %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/requirements-testing.txt b/babelizer/data/templates/requirements-testing.txt similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/requirements-testing.txt rename to babelizer/data/templates/requirements-testing.txt diff --git a/babelizer/data/{{cookiecutter.package_name}}/requirements.txt b/babelizer/data/templates/requirements.txt similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/requirements.txt rename to babelizer/data/templates/requirements.txt diff --git a/babelizer/data/{{cookiecutter.package_name}}/setup.cfg b/babelizer/data/templates/setup.cfg similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/setup.cfg rename to babelizer/data/templates/setup.cfg diff --git a/babelizer/data/templates/{{package.name}}/__init__.py.jinja b/babelizer/data/templates/{{package.name}}/__init__.py.jinja new file mode 100644 index 00000000..87338d61 --- /dev/null +++ b/babelizer/data/templates/{{package.name}}/__init__.py.jinja @@ -0,0 +1 @@ +{{ files['__init__.py'] }} diff --git a/babelizer/data/templates/{{package.name}}/_bmi.py.jinja b/babelizer/data/templates/{{package.name}}/_bmi.py.jinja new file mode 100644 index 00000000..6055b3d9 --- /dev/null +++ b/babelizer/data/templates/{{package.name}}/_bmi.py.jinja @@ -0,0 +1 @@ +{{ files['_bmi.py'] }} diff --git a/babelizer/data/templates/{{package.name}}/_version.py.jinja b/babelizer/data/templates/{{package.name}}/_version.py.jinja new file mode 100644 index 00000000..43998175 --- /dev/null +++ b/babelizer/data/templates/{{package.name}}/_version.py.jinja @@ -0,0 +1 @@ +__version__ = "{{ package_version }}" diff --git a/babelizer/data/templates/{{package.name}}/lib/__init__.py.jinja b/babelizer/data/templates/{{package.name}}/lib/__init__.py.jinja new file mode 100644 index 00000000..637cf898 --- /dev/null +++ b/babelizer/data/templates/{{package.name}}/lib/__init__.py.jinja @@ -0,0 +1 @@ +{{ files['lib/__init__.py'] }} diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_c.pyx b/babelizer/data/templates/{{package.name}}/lib/_c.pyx similarity index 99% rename from babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_c.pyx rename to babelizer/data/templates/{{package.name}}/lib/_c.pyx index 6c9216c9..57ce1d7f 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_c.pyx +++ b/babelizer/data/templates/{{package.name}}/lib/_c.pyx @@ -90,7 +90,7 @@ def ok_or_raise(status): if status != 0: raise RuntimeError('error code {status}'.format(status=status)) -{%- for babelized_class, component in cookiecutter.components|dictsort %} +{%- for babelized_class, component in components|dictsort %} # start: {{ babelized_class|lower }}.pyx diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_cxx.pyx b/babelizer/data/templates/{{package.name}}/lib/_cxx.pyx similarity index 98% rename from babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_cxx.pyx rename to babelizer/data/templates/{{package.name}}/lib/_cxx.pyx index 01c4bdf6..56d83df5 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_cxx.pyx +++ b/babelizer/data/templates/{{package.name}}/lib/_cxx.pyx @@ -7,7 +7,7 @@ from libcpp.string cimport string from libcpp.vector cimport vector import numpy as np -{%- for babelized_class, component in cookiecutter.components|dictsort %} +{%- for babelized_class, component in components|dictsort %} # start: {{ babelized_class|lower }}.pyx diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_fortran.pyx b/babelizer/data/templates/{{package.name}}/lib/_fortran.pyx similarity index 99% rename from babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_fortran.pyx rename to babelizer/data/templates/{{package.name}}/lib/_fortran.pyx index 47726f2b..674749c6 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/_fortran.pyx +++ b/babelizer/data/templates/{{package.name}}/lib/_fortran.pyx @@ -123,7 +123,7 @@ cpdef to_string(bytes): except AttributeError: return bytes -{%- for babelized_class in cookiecutter.components %} +{%- for babelized_class in components %} # start: {{ babelized_class|lower }}.pyx diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/bmi_interoperability.f90 b/babelizer/data/templates/{{package.name}}/lib/bmi_interoperability.f90 similarity index 99% rename from babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/bmi_interoperability.f90 rename to babelizer/data/templates/{{package.name}}/lib/bmi_interoperability.f90 index 91ba3049..6995dcc2 100644 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/bmi_interoperability.f90 +++ b/babelizer/data/templates/{{package.name}}/lib/bmi_interoperability.f90 @@ -3,13 +3,13 @@ ! module bmi_interoperability -{%- for _, component in cookiecutter.components|dictsort %} +{%- for _, component in components|dictsort %} use, intrinsic :: iso_c_binding use bmif_2_0 -{%- if cookiecutter.build.libraries %} -{%- for lib in cookiecutter.build.libraries %} +{%- if build.libraries %} +{%- for lib in build.libraries %} use {{ lib }} {%- endfor %} {%- endif %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/bmi_interoperability.h b/babelizer/data/templates/{{package.name}}/lib/bmi_interoperability.h similarity index 100% rename from babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/bmi_interoperability.h rename to babelizer/data/templates/{{package.name}}/lib/bmi_interoperability.h diff --git a/babelizer/data/{{cookiecutter.package_name}}/.gitignore b/babelizer/data/{{cookiecutter.package_name}}/.gitignore deleted file mode 100644 index 0d0df21e..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/.gitignore +++ /dev/null @@ -1 +0,0 @@ -{{cookiecutter.files['.gitignore']}} diff --git a/babelizer/data/{{cookiecutter.package_name}}/CREDITS.rst b/babelizer/data/{{cookiecutter.package_name}}/CREDITS.rst deleted file mode 100644 index c8314f99..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/CREDITS.rst +++ /dev/null @@ -1,4 +0,0 @@ -Credits -======= - -* {{ cookiecutter.info.full_name }} <{{ cookiecutter.info.email }}> diff --git a/babelizer/data/{{cookiecutter.package_name}}/LICENSE.rst b/babelizer/data/{{cookiecutter.package_name}}/LICENSE.rst deleted file mode 100644 index 10583352..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/LICENSE.rst +++ /dev/null @@ -1 +0,0 @@ -{{cookiecutter.files['LICENSE.rst']}} diff --git a/babelizer/data/{{cookiecutter.package_name}}/MANIFEST.in b/babelizer/data/{{cookiecutter.package_name}}/MANIFEST.in deleted file mode 100644 index fb1c8abe..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/MANIFEST.in +++ /dev/null @@ -1,15 +0,0 @@ -recursive-include {{ cookiecutter.package_name }}/data * -include LICENSE -include requirements.txt -include *.rst -include *.txt -include Makefile -include babel.toml -recursive-include {{ cookiecutter.package_name }} *.pyx -recursive-include docs *.py -recursive-include docs *.rst -recursive-include docs Makefile -recursive-exclude meta * -recursive-exclude recipe * -recursive-exclude {{ cookiecutter.package_name }} *.cpp -recursive-exclude {{ cookiecutter.package_name }} *.c diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/Makefile b/babelizer/data/{{cookiecutter.package_name}}/docs/Makefile deleted file mode 100644 index ed1a43da..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = {{ cookiecutter.package_name }} -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# 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)" $(SPHINXOPTS) $(O) diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/api/.gitignore b/babelizer/data/{{cookiecutter.package_name}}/docs/api/.gitignore deleted file mode 100644 index 49355355..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/api/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# auto-generated with sphinx-apidoc -{{ cookiecutter.package_name }}*.rst diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/developer_install.rst b/babelizer/data/{{cookiecutter.package_name}}/docs/developer_install.rst deleted file mode 100644 index a6dcc70a..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/developer_install.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. _developer_install: - -================= -Developer Install -================= - -.. important:: - - The following commands will install *{{ cookiecutter.package_name }}* into your current environment. Although - not necessary, we **highly recommend** you install *{{ cookiecutter.package_name }}* into its own - :ref:`virtual environment `. - -If you will be modifying code or contributing new code to *{{ cookiecutter.package_name }}*, you will first -need to get *{{ cookiecutter.package_name }}*'s source code and then install *{{ cookiecutter.package_name }}* from that code. - -Source Install --------------- - -*{{ cookiecutter.package_name }}* is actively being developed on GitHub, where the code is freely available. -If you would like to modify or contribute code, you can either clone our -repository - -.. code-block:: bash - - git clone git://github.com/pymt-lab/{{ cookiecutter.package_name }}.git - -or download the `tarball `_ -(a zip file is available for Windows users): - -.. code-block:: bash - - curl -OL https://github.com/{{ cookiecutter.info.github_username }}/{{ cookiecutter.package_name }}/tarball/master - -Once you have a copy of the source code, you can install it into your current -Python environment, - -.. tab:: mamba - - .. code-block:: bash - - cd {{ cookiecutter.package_name }} - mamba install --file=requirements.txt - pip install -e . - -.. tab:: conda - - .. code-block:: bash - - cd {{ cookiecutter.package_name }} - conda install --file=requirements.txt - pip install -e . - -.. tab:: pip - - .. code-block:: bash - - cd {{ cookiecutter.package_name }} - pip install -e . diff --git a/babelizer/data/{{cookiecutter.package_name}}/docs/quickstart.rst b/babelizer/data/{{cookiecutter.package_name}}/docs/quickstart.rst deleted file mode 100644 index 8753666e..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/docs/quickstart.rst +++ /dev/null @@ -1,18 +0,0 @@ -Quickstart -========== - -.. note:: - - The following commands will install *{{ cookiecutter.package_name}}* into your current - environment. Although not necessary, we **highly recommend** you install - *{{ cookiecutter.package_name}}* into its own - :ref:`virtual environment `. - -.. include:: ../README.rst - :start-after: .. start-quickstart - :end-before: .. end-quickstart - -If you would like the very latest development version of *{{ cookiecutter.package_name}}* -or want to modify or contribute code to the *{{ cookiecutter.package_name}}* project, -you will need to do a :ref:`developer installation ` of -*{{ cookiecutter.package_name }}* from source. diff --git a/babelizer/data/{{cookiecutter.package_name}}/meson.build b/babelizer/data/{{cookiecutter.package_name}}/meson.build deleted file mode 100644 index 6edc29d5..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/meson.build +++ /dev/null @@ -1,99 +0,0 @@ -project( - '{{ cookiecutter.package_name }}', -{%- if cookiecutter.language == 'c' %} - 'c', -{%- elif cookiecutter.language == 'c++' %} - 'cpp', -{%- elif cookiecutter.language == 'fortran' %} - 'fortran', -{%- endif %} - 'cython', - version: '{{ cookiecutter.package_version }}', -) - -py = import('python').find_installation(pure: false) - -{%- if cookiecutter.language == 'c' %} -compiler = meson.get_compiler('c') -{%- elif cookiecutter.language == 'c++' %} -compiler = meson.get_compiler('cpp') -{%- elif cookiecutter.language == 'fortran' %} -compiler = meson.get_compiler('fortran') -{%- endif %} - -# python_inc = py.get_path('data') / 'include' -numpy_inc = run_command( - py, - [ - '-c', - 'import numpy; print(numpy.get_include())' - ], - check: true -).stdout().strip() -incs = include_directories( - [ - '{{ cookiecutter.package_name }}/lib', - # python_inc, - numpy_inc, - ] -) - -{% set dependency_list = cookiecutter.package_requirements.split(',') -%} -deps = [ -{%- for dependency in dependency_list if dependency != '' %} - compiler.find_library('{{ dependency }}'), -{%- endfor %} -] - -# Files get copied to /site-packages/ -install_pkg_srcs = [ - '{{ cookiecutter.package_name }}/__init__.py', - '{{ cookiecutter.package_name }}/_bmi.py', - '{{ cookiecutter.package_name }}/_version.py', -] -py.install_sources( - install_pkg_srcs, - subdir: '{{ cookiecutter.package_name }}', -) - -install_lib_srcs = [ - '{{ cookiecutter.package_name }}/lib/__init__.py', -{%- for babelized_class in cookiecutter.components|list|sort %} - '{{ cookiecutter.package_name }}/lib/{{ babelized_class|lower }}.pyx', -{%- endfor %} -] -py.install_sources( - install_lib_srcs, - subdir: '{{ cookiecutter.package_name }}/lib', -) - - -{%- for babelized_class, component in cookiecutter.components|dictsort %} -py.extension_module( - '{{ babelized_class|lower }}', - [ -{%- if cookiecutter.language == 'fortran' %} - '{{ cookiecutter.package_name }}/lib/bmi_interoperability.f90', -{%- endif %} - '{{ cookiecutter.package_name }}/lib/{{ babelized_class|lower }}.pyx', - ], - dependencies: [ - dependency('{{ component.library }}', method : 'pkg-config'), - ], - include_directories: incs, - install: true, - subdir: '{{ cookiecutter.package_name }}/lib', -{%- if cookiecutter.language == 'c++' %} - override_options : ['cython_language=cpp'], -{%- endif %} -) - -install_subdir( - 'meta/{{ babelized_class }}', - install_dir: py.get_install_dir() / '{{ cookiecutter.package_name }}/data', -) - -{%- endfor %} - -# This is a temporary fix for editable installs. -run_command('cp', '-r', '{{ cookiecutter.package_name }}/data', 'build') diff --git a/babelizer/data/{{cookiecutter.package_name}}/requirements-build.txt b/babelizer/data/{{cookiecutter.package_name}}/requirements-build.txt deleted file mode 100644 index c92dc98a..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/requirements-build.txt +++ /dev/null @@ -1,11 +0,0 @@ -# conda requirements needed to build the project -{%- if cookiecutter.language == 'c' -%} -bmi-c -c-compiler -{%- elif cookiecutter.language == 'c++' -%} -bmi-cxx -cxx-compiler -{%- elif cookiecutter.language == 'fortran' -%} -bmi-fortran -fortran-compiler -{%- endif %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/requirements-library.txt b/babelizer/data/{{cookiecutter.package_name}}/requirements-library.txt deleted file mode 100644 index 306c4bf3..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/requirements-library.txt +++ /dev/null @@ -1,3 +0,0 @@ -{%- for requirement in cookiecutter.package_requirements.split(',') %} -{{ requirement|trim }} -{%- endfor %} diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/__init__.py b/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/__init__.py deleted file mode 100644 index ae1237e9..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/__init__.py +++ /dev/null @@ -1 +0,0 @@ -{{ cookiecutter.files['__init__.py'] }} diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_bmi.py b/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_bmi.py deleted file mode 100644 index 8aeff829..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_bmi.py +++ /dev/null @@ -1 +0,0 @@ -{{cookiecutter.files['_bmi.py']}} diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_version.py b/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_version.py deleted file mode 100644 index f697ff61..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "{{cookiecutter.package_version}}" diff --git a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/__init__.py b/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/__init__.py deleted file mode 100644 index fbbeb6b6..00000000 --- a/babelizer/data/{{cookiecutter.package_name}}/{{cookiecutter.package_name}}/lib/__init__.py +++ /dev/null @@ -1 +0,0 @@ -{{ cookiecutter.files['lib/__init__.py'] }} diff --git a/babelizer/errors.py b/babelizer/errors.py index edf1abd6..d4787901 100644 --- a/babelizer/errors.py +++ b/babelizer/errors.py @@ -35,7 +35,9 @@ class ScanError(BabelizeError): class OutputDirExistsError(BabelizeError): """An exception used when the directory for babelized output exists.""" - pass + def __str__(self) -> str: + """Render a user-readable error message.""" + return f"output directory exists: {self._message}" class SetupPyError(BabelizeError): diff --git a/babelizer/render.py b/babelizer/render.py index bd2e8d51..5750f4a0 100644 --- a/babelizer/render.py +++ b/babelizer/render.py @@ -2,43 +2,24 @@ from __future__ import annotations -import contextlib +import datetime import os -import sys -from collections.abc import Generator -from typing import Any import git -from cookiecutter.exceptions import OutputDirExistsException -from cookiecutter.main import cookiecutter -from babelizer.metadata import BabelMetadata - -try: - import black as blk - import isort -except ModuleNotFoundError: - MAKE_PRETTY = False -else: - MAKE_PRETTY = True - -if sys.version_info >= (3, 11): # pragma: no cover (PY11+) - import tomllib -else: # pragma: no cover ( str: - """Render a repository for a pymt plugin. - - Parameters - ---------- - template: str - Path (or URL) to the cookiecutter template to use. - context: dict, optional - Context for the new repository. - output_dir : str, optional - Name of the directory that will be the new repository. - clobber: bool, optional - If a like-named repository already exists, overwrite it. + path = os.path.realpath(output) - Returns - ------- - path - Absolute path to the newly-created repository. - """ - context = context or {} - - try: - cookiecutter( - template, - extra_context=context, - output_dir=output_dir, - no_input=True, - overwrite_if_exists=clobber, - ) - except OutputDirExistsException as err: - raise OutputDirExistsError(", ".join(err.args)) - - name = context["package_name"] - - # path = os.path.join(output_dir, "{}".format(context["package_name"])) - # if not os.path.isdir(path): - path = os.path.join(output_dir, name) - if not os.path.isdir(path): - raise RenderError(f"error creating {path}") + with open(os.path.join(path, "babel.toml"), "w") as fp: + babel_config.dump(fp, fmt="toml") git.Repo.init(path) return path - - -@contextlib.contextmanager -def as_cwd(path: str) -> Generator[None, None, None]: - """Change directory context. - - Parameters - ---------- - path : str - Path-like object to a directory. - """ - prev_cwd = os.getcwd() - os.chdir(path) - yield - os.chdir(prev_cwd) - - -def blacken_file(filepath: str) -> None: - """Format a Python file with ``black``. - - Parameters - ---------- - filepath : str - Path-like object to a Python file. - """ - with open(filepath) as fp: - try: - new_contents = blk.format_file_contents( - fp.read(), fast=True, mode=blk.FileMode() - ) - except blk.NothingChanged: - new_contents = None - if new_contents: - with open(filepath, "w") as fp: - fp.write(new_contents) - - -def prettify_python(path_to_repo: str) -> None: - """Format files in babelized project with ``black``. - - Parameters - ---------- - path_to_repo : str - Path-like object to babelized project. - """ - with open(os.path.join(path_to_repo, "babel.toml")) as fp: - meta = tomllib.loads(fp.read()) - module_name = meta["package"]["name"] - - files_to_fix = [ - os.path.join(path_to_repo, module_name, "_bmi.py"), - os.path.join(path_to_repo, module_name, "__init__.py"), - os.path.join(path_to_repo, "docs", "conf.py"), - ] - - config = isort.Config(quiet=True) - for file_to_fix in files_to_fix: - isort.api.sort_file(file_to_fix, config=config) - blacken_file(file_to_fix) diff --git a/babelizer/utils.py b/babelizer/utils.py deleted file mode 100644 index 887c0383..00000000 --- a/babelizer/utils.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Utility functions used by the babelizer.""" - -from __future__ import annotations - -import pathlib -import subprocess -import sys -from collections.abc import Generator -from collections.abc import Iterable -from collections.abc import Sequence -from contextlib import contextmanager -from contextlib import suppress - -from babelizer.errors import SetupPyError - - -def execute(args: Sequence[str]) -> subprocess.CompletedProcess[bytes]: - """Run a command through the ``subprocess`` module. - - Parameters - ---------- - args : list - Command and arguments to command. - - Returns - ------- - ~subprocess.CompletedProcess - results from :func:`subprocess.run`. - """ - return subprocess.run(args, capture_output=True, check=True) - - -def setup_py(*args: str) -> list[str]: - """Format the command to build/install the babelized package. - - Returns - ------- - list of str - The build/install command. - """ - return [sys.executable, "setup.py"] + list(args) - - -def get_setup_py_version() -> str | None: - """Get babelized 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: - execute(setup_py("egg_info")) - except subprocess.CalledProcessError as err: - stderr = err.stderr.decode("utf-8") - if "Traceback" in stderr: - raise SetupPyError(stderr) from None - return None - result = execute(setup_py("--version")) - return result.stdout.splitlines()[0].decode("utf-8") - else: - return None - - -@contextmanager -def save_files(files: Iterable[str]) -> Generator[dict[str, str], None, None]: - """Generate repository files through a context. - - Parameters - ---------- - files : list of str - List of path-like objects. - - Yields - ------ - str - Generator for repository files. - """ - contents = {} - for file_ in files: - with suppress(FileNotFoundError), open(file_) as fp: - contents[file_] = fp.read() - yield contents - for file_ in contents: - with open(file_, "w") as fp: - fp.write(contents[file_]) diff --git a/external/requirements.txt b/external/requirements.txt index 496776c5..049a9fb8 100644 --- a/external/requirements.txt +++ b/external/requirements.txt @@ -1,12 +1,12 @@ -bmi-tester>=0.5.4 bmi-c bmi-cxx bmi-fortran +bmi-tester>=0.5.4 bmipy -cmake c-compiler +cmake cxx-compiler fortran-compiler make -pkg-config pip +pkg-config diff --git a/news/96.misc b/news/96.misc new file mode 100644 index 00000000..35872ee2 --- /dev/null +++ b/news/96.misc @@ -0,0 +1,3 @@ + +Removed dependency on *cookiecutter* by adding our own implementation +of a simplified version. diff --git a/pyproject.toml b/pyproject.toml index 47a72c40..cf527ac9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ classifiers = [ ] dependencies = [ "click", - "cookiecutter", "gitpython", "importlib-resources; python_version < '3.12'", "jinja2", @@ -66,10 +65,6 @@ dev = [ "pre-commit", "towncrier", ] -format = [ - "black", - "isort>=5", -] docs = [ "furo", "pygments>=2.4", @@ -112,7 +107,7 @@ attr = "babelizer._version.__version__" [tool.setuptools.package-data] babelizer = [ - "data/*", + "data/**/*", ] [tool.setuptools.packages.find] diff --git a/requirements-docs.txt b/requirements-docs.txt index 2f7798f6..ad7389b7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,10 +1,10 @@ # Requirements extracted from pyproject.toml # [project.optional-dependencies.docs] -sphinx>=4 +furo +pygments>=2.4 sphinx-click sphinx-copybutton sphinx-inline-tabs -sphinxcontrib.towncrier -pygments>=2.4 sphinx-inline-tabs -furo +sphinx>=4 +sphinxcontrib.towncrier diff --git a/requirements-testing.txt b/requirements-testing.txt index 0956cbaa..e11fe5fd 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,8 +1,9 @@ # Requirements extracted from pyproject.toml # [project.optional-dependencies.testing] +bmi-tester>=0.5.9 +coverage[toml] +coveralls pytest pytest-cov pytest-datadir pytest-xdist -coverage[toml] -coveralls diff --git a/requirements.txt b/requirements.txt index 8ea48b4e..29a5d308 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ # Requirements extracted from pyproject.toml # [project.dependencies] -black click gitpython +importlib-resources; python_version < '3.12' +jinja2 +logoizer@ git+https://github.com/mcflugen/logoizer pyyaml -tomlkit -isort>=5 -cookiecutter +tomli-w +tomli; python_version < '3.11' diff --git a/tests/test_cli.py b/tests/cli_test.py similarity index 95% rename from tests/test_cli.py rename to tests/cli_test.py index 83ff3d4a..7037397b 100644 --- a/tests/test_cli.py +++ b/tests/cli_test.py @@ -10,7 +10,7 @@ from click.testing import CliRunner from babelizer.cli import babelize -from babelizer.metadata import BabelMetadata +from babelizer.config import BabelConfig def test_help(): @@ -67,7 +67,7 @@ def test_generate_gives_valid_toml(): assert result.exit_code == 0 config = tomllib.loads(result.output) - BabelMetadata.validate(config) + BabelConfig.validate(config) def test_init_noargs():