Skip to content

Commit

Permalink
Merge pull request #46 from csdms/mcflugen/remove-jinja-black
Browse files Browse the repository at this point in the history
Remove dependency on jinja2 and black
  • Loading branch information
mcflugen authored Jan 8, 2024
2 parents 123c1df + 4ad1ff1 commit 3447c93
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 124 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@v4
- uses: wntrblm/nox@2023.04.22
with:
python-versions: "3.9"
python-versions: "3.12"
- name: Lint
run: nox --non-interactive --error-on-missing-interpreter --session "lint"

Expand Down
16 changes: 8 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.12.1
hooks:
- id: black
name: black
Expand All @@ -23,7 +23,7 @@ repos:
additional_dependencies: [".[jupyter]"]

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
Expand All @@ -42,19 +42,19 @@ repos:
language: python

- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py39-plus]

- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
files: \.py$

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-builtin-literals
- id: check-added-large-files
Expand All @@ -68,7 +68,7 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/regebro/pyroma
rev: "4.1"
rev: "4.2"
hooks:
- id: pyroma
args: ["-d", "--min=10", "."]
Expand All @@ -86,7 +86,7 @@ repos:
- cython

- repo: https://github.com/PyCQA/pydocstyle
rev: 6.1.1
rev: 6.3.0
hooks:
- id: pydocstyle
files: bmipy/.*\.py$
Expand All @@ -96,7 +96,7 @@ repos:
additional_dependencies: [".[toml]"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-all]
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ classifiers = [
]
requires-python = ">=3.9"
dependencies = [
"black",
"click",
"jinja2",
"numpy",
]
dynamic = ["version"]
Expand Down
2 changes: 0 additions & 2 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
black
click
jinja2
numpy
2 changes: 0 additions & 2 deletions requirements/requires.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
black==23.10.1
click==8.1.7
jinja2==3.1.2
numpy==1.26.3
1 change: 1 addition & 0 deletions src/bmipy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The Basic Model Interface (BMI) for Python."""
from __future__ import annotations

from ._version import __version__
from .bmi import Bmi
Expand Down
133 changes: 133 additions & 0 deletions src/bmipy/_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from __future__ import annotations

import inspect
import os
import textwrap

from bmipy.bmi import Bmi


class Template:
"""Create template BMI implementations."""

def __init__(self, name: str):
self._name = name
self._funcs = dict(inspect.getmembers(Bmi, inspect.isfunction))

def render(self) -> str:
"""Render a module that defines a class implementing a Bmi."""
prefix = f"""\
from __future__ import annotations
import numpy as np
from bmipy.bmi import Bmi
class {self._name}(Bmi):
"""
return prefix + (os.linesep * 2).join(
[self._render_func(name) for name in sorted(self._funcs)]
)

def _render_func(self, name: str) -> str:
annotations = inspect.get_annotations(self._funcs[name])
signature = inspect.signature(self._funcs[name], eval_str=False)

docstring = textwrap.indent(
'"""' + dedent_docstring(self._funcs[name].__doc__) + '"""', " "
)

parts = [
render_function_signature(
name,
tuple(signature.parameters),
annotations,
width=84,
),
docstring,
f" raise NotImplementedError({name!r})".replace("'", '"'),
]

return textwrap.indent(os.linesep.join(parts), " ")


def dedent_docstring(text: str | None, tabsize=4) -> str:
"""Dedent a docstring, ignoring indentation of the first line.
Parameters
----------
text : str
The text to dedent.
tabsize : int, optional
Specify the number of spaces to replace tabs with.
Returns
-------
str
The dendented string.
"""
if not text:
return ""

lines = text.expandtabs(tabsize).splitlines(keepends=True)
first = lines[0].lstrip()
try:
body = lines[1:]
except IndexError:
body = [""]
return first + textwrap.dedent("".join(body))


def render_function_signature(
name: str,
params: tuple[str, ...] | None = None,
annotations: dict[str, str] | None = None,
tabsize: int = 4,
width: int = 88,
) -> str:
"""Render a function signature, wrapping if the generated signature is too long.
Parameters
----------
name : str
The name of the function.
params : tuple of str, optional
Names of each of the parameters.
annotations : dict, optional
Annotations for each parameters as well as the return type.
tabsize : int, optional
The number of spacses represented by a tab.
width : int, optional
The maximum width of a line.
Returns
-------
str
The function signature appropriately wrapped.
"""
params = () if params is None else params
annotations = {} if annotations is None else annotations

prefix = f"def {name}("
if "return" in annotations:
suffix = f") -> {annotations['return']}:"
else:
suffix = "):"
body = []
for param in params:
if param in annotations:
param += f": {annotations[param]}"
body.append(param)

signature = prefix + ", ".join(body) + suffix
if len(signature) <= width:
return signature

indent = " " * tabsize

lines = [prefix, indent + ", ".join(body), suffix]
if max(len(line) for line in lines) <= width:
return os.linesep.join(lines)

return os.linesep.join([prefix] + [f"{indent}{line}," for line in body] + [suffix])
2 changes: 2 additions & 0 deletions src/bmipy/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from __future__ import annotations

__version__ = "2.0.2.dev0"
1 change: 1 addition & 0 deletions src/bmipy/bmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This language specification is derived from the Scientific Interface
Definition Language (SIDL) file `bmi.sidl <https://github.com/csdms/bmi>`_.
"""
from __future__ import annotations

from abc import ABC, abstractmethod

Expand Down
89 changes: 6 additions & 83 deletions src/bmipy/cmd.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,21 @@
"""Command line interface that create template BMI implementations."""
import inspect
from __future__ import annotations

import keyword
import re

import black as blk
import click
import jinja2

from bmipy import Bmi

BMI_TEMPLATE = """# -*- coding: utf-8 -*-
{% if with_hints -%}
from typing import Tuple
{%- endif %}
from bmipy import Bmi
import numpy
class {{ name }}(Bmi):
{% for func in funcs %}
def {{ func }}{{ funcs[func].sig }}:
\"\"\"{{ funcs[func].doc }}\"\"\"
raise NotImplementedError("{{ func }}")
{% endfor %}
"""


def _remove_hints_from_signature(signature):
"""Remove hint annotation from a signature."""
params = []
for _, param in signature.parameters.items():
params.append(param.replace(annotation=inspect.Parameter.empty))
return signature.replace(
parameters=params, return_annotation=inspect.Signature.empty
)


def _is_valid_class_name(name):
p = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE)
return p.match(name) and not keyword.iskeyword(name)


def render_bmi(name, black=True, hints=True):
"""Render a template BMI implementation in Python.
Parameters
----------
name : str
Name of the new BMI class to implement.
black : bool, optional
If True, reformat the source using black styling.
hints : bool, optiona
If True, include type hint annotation.
Returns
-------
str
The contents of a new Python module that contains a template for
a BMI implementation.
"""
if _is_valid_class_name(name):
env = jinja2.Environment()
template = env.from_string(BMI_TEMPLATE)

funcs = {}
for func_name, func in inspect.getmembers(Bmi, inspect.isfunction):
signature = inspect.signature(func)
if not hints:
signature = _remove_hints_from_signature(signature)
funcs[func_name] = {"sig": signature, "doc": func.__doc__}

contents = template.render(name=name, funcs=funcs, with_hints=hints)

if black:
contents = blk.format_file_contents(
contents, fast=True, mode=blk.FileMode()
)

return contents
else:
raise ValueError(f"invalid class name ({name})")
from bmipy._template import Template


@click.command()
@click.version_option()
@click.option("--black / --no-black", default=True, help="format output with black")
@click.option("--hints / --no-hints", default=True, help="include type hint annotation")
@click.argument("name")
@click.pass_context
def main(ctx, name, black, hints):
def main(ctx: click.Context, name: str):
"""Render a template BMI implementation in Python for class NAME."""
if _is_valid_class_name(name):
print(render_bmi(name, black=black, hints=hints))
if name.isidentifier() and not keyword.iskeyword(name):
print(Template(name).render())
else:
click.secho(
f"💥 💔 💥 {name!r} is not a valid class name in Python",
Expand Down
Loading

0 comments on commit 3447c93

Please sign in to comment.