Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace cookiecutter #96

Merged
merged 38 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b295751
add _cookiecutter module to replace cookiecutter functionality
mcflugen Mar 23, 2024
34fdc2d
replace cookiecutter's now extension for the current date
mcflugen Mar 23, 2024
026de99
fix an annotation
mcflugen Mar 23, 2024
48453de
move as_cwd to utils module
mcflugen Mar 23, 2024
205c14e
adjust location of template down on folder
mcflugen Mar 23, 2024
6e23709
use replacement cookiecutter to render files
mcflugen Mar 23, 2024
4918faf
add a post-hook function to run after rendering files
mcflugen Mar 23, 2024
260ae7e
add some verbosity
mcflugen Mar 24, 2024
2502c3b
remove .jinja extension from templates
mcflugen Mar 24, 2024
5a248b2
add .jinja extension to python template files
mcflugen Mar 24, 2024
54b354d
add package_version to context
mcflugen Mar 24, 2024
3c86843
remove cookiecutter files from the data dir; rename package data dir
mcflugen Mar 31, 2024
d22da6a
remove cookiecutter context from jinja templates
mcflugen Mar 31, 2024
f061df8
remove cookiecutter context from metadata
mcflugen Mar 31, 2024
d9b6aee
remove calls to cookiecutter package
mcflugen Mar 31, 2024
9bdceef
remove cookiecutter dependency
mcflugen Mar 31, 2024
f6727e5
remove prettify_python and black, isort dependencies
mcflugen Mar 31, 2024
5c4f3b2
add get_template_dir function
mcflugen Apr 1, 2024
73fc90c
raise error for undefined variables in templates
mcflugen Apr 1, 2024
a2232ab
clean up variable names in templates
mcflugen Apr 1, 2024
57a9353
better error message for existing output dir
mcflugen Apr 1, 2024
3b10450
make utils module private
mcflugen Apr 2, 2024
1eb959a
clean up cookiecutter; add babelizer_environment function
mcflugen Apr 2, 2024
2b63d93
add news fragment
mcflugen Apr 2, 2024
768dd09
remove the manifest from the templates
mcflugen Apr 3, 2024
70347d9
add setuptools package-data and version for python projects
mcflugen Apr 3, 2024
ffa804c
remove the babelizer's manifest
mcflugen Apr 3, 2024
cfc62d7
recusively include babelizer package data
mcflugen Apr 3, 2024
36a1218
remove the docs Makefile
mcflugen Apr 3, 2024
f7f8af2
more clean up; move parse_entry_point into _utils.py
mcflugen Apr 4, 2024
c4a8100
update requirements files
mcflugen Apr 4, 2024
2824e9e
clean up .gitignore; add a couple linters
mcflugen Apr 4, 2024
b669578
fix parse_entry_point doctest
mcflugen Apr 4, 2024
4f11dd1
update the template pre-commit config file
mcflugen Apr 4, 2024
c10fa1b
remove unused _norm_os function
mcflugen Apr 4, 2024
efbcc9c
update pre-commit config file
mcflugen Apr 4, 2024
be33883
move validate_dict into _utils as validate_dict_keys
mcflugen Apr 4, 2024
cfad50e
changed BabelMetadata to BabelConfig
mcflugen Apr 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions babelizer/_cookiecutter.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions babelizer/_datadir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
8 changes: 2 additions & 6 deletions babelizer/_files/readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
155 changes: 155 additions & 0 deletions babelizer/_post_hook.py
Original file line number Diff line number Diff line change
@@ -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<fname>\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,
)
16 changes: 16 additions & 0 deletions babelizer/utils.py → babelizer/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
import pathlib
import subprocess
import sys
Expand Down Expand Up @@ -90,3 +91,18 @@ def save_files(files: Iterable[str]) -> Generator[dict[str, str], None, None]:
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)
Loading
Loading