Skip to content

Commit

Permalink
Overwritable templates (#9)
Browse files Browse the repository at this point in the history
* Refactor templates

README.md is now composed of overwritable snippets.

* Make template overwritable by user

* Keep trailing newlines

* Add newlines

* Fix windows path separator issue

* Use readme_content.content in default content template

* Rely on standard template override mechanism instead of using variables.

* Add tagline snippet

* List overwritable templates

---------

Co-authored-by: Ronny V <[email protected]>
  • Loading branch information
fbinz and GitRon authored Jun 20, 2024
1 parent 87cb545 commit 8f58d84
Show file tree
Hide file tree
Showing 17 changed files with 253 additions and 186 deletions.
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

# Ambient Package Update

This repository will help keep all Python packages following a certain basic structure tidy and up-to-date. It's being
maintained by [Ambient Digital](https://ambient.digital).
This repository will help keep all Python packages following a certain basic structure tidy and up-to-date. It's being
maintained by [Ambient Digital](https://ambient.digital).

This package will render all required configuration and installation files for your target package.

Expand Down Expand Up @@ -87,11 +87,49 @@ METADATA = PackageMetadata(
- Enable the readthedocs hook in your GitHub repo to update your documentation on a commit basis
- Finally, follow the steps of the section above (`How to update a package`).

### Customizing the templates

The default templates are located in the `ambient_package_update/templates` directory.
You can overwrite them by creating a `.ambient-package-update/templates` directory in your project
and create a new file with the same name as the template you want to overwrite.
The following templates are available:

```
├── docs
│ ├── conf.py.tpl
│ ├── make.bat.tpl
│ └── Makefile.tpl
├── MANIFEST.in.tpl
├── pyproject.toml.tpl
├── README.md.tpl
├── scripts
│ ├── unix
│ │ ├── install_requirements.sh.tpl
│ │ └── publish_to_pypi.sh.tpl
│ └── windows
│ ├── install_requirements.ps1.tpl
│ └── publish_to_pypi.ps1.tpl
├── setup.cfg.tpl
└── snippets
├── badges.tpl
├── content.tpl
├── contribute.tpl
├── empty.tpl
├── installation.tpl
├── licenses
│ ├── GPL.md
│ └── MIT.md
├── links.tpl
├── maintenance.tpl
├── publish.tpl
└── tagline.tpl
```

## Contribution

### Dependency updates

The dependencies of this package are being maintained with `pip-tools`.
The dependencies of this package are being maintained with `pip-tools`.

> pip install -U pip-tools
Expand Down
55 changes: 39 additions & 16 deletions ambient_package_update/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from pathlib import Path

import typer
from jinja2 import Template
from jinja2 import Environment, FileSystemLoader, select_autoescape

from ambient_package_update.metadata.constants import LICENSE_GPL
from ambient_package_update.metadata.package import PackageMetadata

BASE_PATH = Path(__file__).parent
TEMPLATE_PATH = BASE_PATH / "templates"

app = typer.Typer()

Expand All @@ -23,16 +24,14 @@ def get_metadata() -> PackageMetadata:
try:
m = import_module("metadata")
except ModuleNotFoundError as e:
raise RuntimeError('Please create a directory ".ambient-package-update" and add a "metadata.py".') from e
raise RuntimeError(
'Please create a directory ".ambient-package-update" and add a "metadata.py".'
) from e
sys.path.pop()

return m.METADATA


def get_template_path() -> Path:
return BASE_PATH / "templates"


def create_rendered_file(*, template: Path, relative_target_path: Path | str) -> None:
"""
Takes a template Path object and renders a template in the target package.
Expand All @@ -41,13 +40,28 @@ def create_rendered_file(*, template: Path, relative_target_path: Path | str) ->

# Special case: We might want to set an explicit GitHub package name
metadata_dict["github_package_name"] = (
metadata_dict["github_package_name"] if metadata_dict["github_package_name"] else metadata_dict["package_name"]
metadata_dict["github_package_name"]
if metadata_dict["github_package_name"]
else metadata_dict["package_name"]
)

env = Environment(
loader=FileSystemLoader(
[
".ambient-package-update/templates",
TEMPLATE_PATH,
]
),
autoescape=select_autoescape(),
keep_trailing_newline=True,
)

j2_template = Template(template.read_text(), keep_trailing_newline=True)
j2_template = env.get_template(str(template).replace("\\", "/"))
j2_template.globals["current_year"] = datetime.now(tz=UTC).date().year
j2_template.globals["license_label"] = (
"GNU General Public License (GPL)" if metadata_dict["license"] == LICENSE_GPL else "MIT License"
"GNU General Public License (GPL)"
if metadata_dict["license"] == LICENSE_GPL
else "MIT License"
)

print(f"> Rendering template {basename(template)!r}...")
Expand All @@ -67,23 +81,30 @@ def create_rendered_file(*, template: Path, relative_target_path: Path | str) ->

@app.command()
def render_templates():
template_list = []
# Collect all template files and add them to "template_list"
for path, subdirs, files in os.walk(get_template_path()):
print(path, subdirs, files)
[template_list.append(Path(f"{path}/{file}")) for file in files]
template_list = [
str(Path(Path(path).relative_to(TEMPLATE_PATH), file))
for path, subdirs, files in os.walk(TEMPLATE_PATH)
for file in files
if "snippets" not in path
]

for template in template_list:
print(f"> Found template {template!r}...")

print("Start rending distribution templates.")

for template in template_list:
create_rendered_file(
template=template, relative_target_path=str(Path(template).relative_to(get_template_path()))[:-4]
template=template,
relative_target_path=template.removesuffix(".tpl"),
)

# License file is conditional so we have to render it separately
metadata_dict = get_metadata().__dict__
create_rendered_file(
template=BASE_PATH / f"licenses/{metadata_dict['license']}.md", relative_target_path="LICENSE.md"
template=f"snippets/licenses/{metadata_dict['license']}.md",
relative_target_path="LICENSE.md",
)

print("Rendering finished.")
Expand All @@ -92,7 +113,9 @@ def render_templates():
@app.command()
def build_docs(package_name: str):
print(f'Building docs for package "{package_name}"')
subprocess.call(f"cd ../{package_name} && sphinx-build docs/ docs/_build/html/", shell=True)
subprocess.call(
f"cd ../{package_name} && sphinx-build docs/ docs/_build/html/", shell=True
)


@app.command()
Expand Down
11 changes: 9 additions & 2 deletions ambient_package_update/metadata/package.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import dataclasses
import datetime
from typing import Optional

from ambient_package_update.metadata.author import PackageAuthor
from ambient_package_update.metadata.constants import LICENSE_MIT
from ambient_package_update.metadata.maintainer import PackageMaintainer
from ambient_package_update.metadata.readme import ReadmeContent
from ambient_package_update.metadata.ruff_ignored_inspection import RuffIgnoredInspection
from ambient_package_update.metadata.ruff_ignored_inspection import (
RuffIgnoredInspection,
)


@dataclasses.dataclass
class PackageMetadata:
package_name: str
module_name: str
company: str
authors: list[PackageAuthor]
maintainer: PackageMaintainer
Expand All @@ -24,6 +26,11 @@ class PackageMetadata:
min_coverage: float = 100.0
license: str = LICENSE_MIT
license_year: int = datetime.datetime.now(tz=datetime.UTC).year
module_name: Optional[str] = None
github_package_name: str = None
optional_dependencies: dict[str, list[str]] = None
ruff_ignore_list: list[RuffIgnoredInspection] = None

def __post_init__(self):
if not self.module_name:
self.module_name = self.package_name.replace("-", "_")
3 changes: 2 additions & 1 deletion ambient_package_update/metadata/readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

@dataclasses.dataclass
class ReadmeContent:
tagline: str
# Variables that are used in the default templates
tagline: str = None
content: Optional[str] = None
custom_installation: Optional[str] = None
additional_installation: Optional[str] = None
Expand Down
171 changes: 9 additions & 162 deletions ambient_package_update/templates/README.md.tpl
Original file line number Diff line number Diff line change
@@ -1,162 +1,9 @@
[![PyPI release](https://img.shields.io/pypi/v/{{ package_name|replace("_", "-") }}.svg)](https://pypi.org/project/{{ package_name|replace("_", "-") }}/)
[![Downloads](https://static.pepy.tech/badge/{{ package_name|replace("_", "-") }})](https://pepy.tech/project/{{ package_name|replace("_", "-") }})
[![Coverage](https://img.shields.io/badge/Coverage-{{ min_coverage }}%25-success)](https://github.com/ambient-innovation/{{ github_package_name|replace("_", "-") }}/actions?workflow=CI)
[![Linting](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Coding Style](https://img.shields.io/badge/code%20style-Ruff-000000.svg)](https://github.com/astral-sh/ruff)
[![Documentation Status](https://readthedocs.org/projects/{{ package_name|replace("_", "-") }}/badge/?version=latest)](https://{{ package_name|replace("_", "-") }}.readthedocs.io/en/latest/?badge=latest)

{{ readme_content.tagline }}

* [PyPI](https://pypi.org/project/{{ package_name|replace("_", "-") }}/)
* [GitHub](https://github.com/ambient-innovation/{{ github_package_name|replace("_", "-") }})
* [Full documentation](https://{{ package_name|replace("_", "-") }}.readthedocs.io/en/latest/index.html)
* Creator & Maintainer: [{{ maintainer.name }}]({{ maintainer.url }})

{{ readme_content.content }}

## Installation

{% if readme_content.custom_installation %}
{{ readme_content.custom_installation }}
{% else %}
- Install the package via pip:

`pip install {{ package_name|replace("_", "-") }}`

or via pipenv:

`pipenv install {{ package_name|replace("_", "-") }}`

- Add module to `INSTALLED_APPS` within the main django `settings.py`:

````
INSTALLED_APPS = (
...
'{{ package_name }}',
)
````

{% if has_migrations %}
- Apply migrations by running:

`python ./manage.py migrate`
{% endif %}

{% if readme_content.additional_installation %}{{ readme_content.additional_installation }}{% endif %}
{% endif %}

## Contribute

### Setup package for development

- Create a Python virtualenv and activate it
- Install "pip-tools" with `pip install -U pip-tools`
- Compile the requirements with `pip-compile --extra {% for area, dependency_list in optional_dependencies.items() %}{{ area }},{% endfor %} -o requirements.txt pyproject.toml --resolver=backtracking`
- Sync the dependencies with your virtualenv with `pip-sync`

### Add functionality

- Create a new branch for your feature
- Change the dependency in your requirements.txt to a local (editable) one that points to your local file system:
`-e /Users/workspace/{{ package_name|replace("_", "-") }}` or via pip `pip install -e /Users/workspace/{{ package_name|replace("_", "-") }}`
- Ensure the code passes the tests
- Create a pull request

### Run tests

- Run tests
````
pytest --ds settings tests
````

- Check coverage
````
coverage run -m pytest --ds settings tests
coverage report -m
````

### Git hooks (via pre-commit)

We use pre-push hooks to ensure that only linted code reaches our remote repository and pipelines aren't triggered in
vain.

To enable the configured pre-push hooks, you need to [install](https://pre-commit.com/) pre-commit and run once:

pre-commit install -t pre-push -t pre-commit --install-hooks

This will permanently install the git hooks for both, frontend and backend, in your local
[`.git/hooks`](./.git/hooks) folder.
The hooks are configured in the [`.pre-commit-config.yaml`](templates/.pre-commit-config.yaml.tpl).

You can check whether hooks work as intended using the [run](https://pre-commit.com/#pre-commit-run) command:

pre-commit run [hook-id] [options]

Example: run single hook

pre-commit run ruff --all-files --hook-stage push

Example: run all hooks of pre-push stage

pre-commit run --all-files --hook-stage push

### Update documentation

- To build the documentation run: `sphinx-build docs/ docs/_build/html/`.
- Open `docs/_build/html/index.html` to see the documentation.

{% if readme_content.uses_internationalisation %}
### Translation files

If you have added custom text, make sure to wrap it in `_()` where `_` is
gettext_lazy (`from django.utils.translation import gettext_lazy as _`).

How to create translation file:

* Navigate to `{{ package_name|replace("_", "-") }}`
* `python manage.py makemessages -l de`
* Have a look at the new/changed files within `{{ package_name }}/locale`

How to compile translation files:

* Navigate to `{{ package_name|replace("_", "-") }}`
* `python manage.py compilemessages`
* Have a look at the new/changed files within `{{ package_name }}/locale`
{% endif %}

### Publish to ReadTheDocs.io

- Fetch the latest changes in GitHub mirror and push them
- Trigger new build at ReadTheDocs.io (follow instructions in admin panel at RTD) if the GitHub webhook is not yet set
up.

### Publish to PyPi

- Update documentation about new/changed functionality

- Update the `Changelog`

- Increment version in main `__init__.py`

- Create pull request / merge to master

- This project uses the flit package to publish to PyPI. Thus publishing should be as easy as running:
```
flit publish
```

To publish to TestPyPI use the following ensure that you have set up your .pypirc as
shown [here](https://flit.readthedocs.io/en/latest/upload.html#using-pypirc) and use the following command:

```
flit publish --repository testpypi
```

### Maintenance

Please note that this package supports the [ambient-package-update](https://pypi.org/project/ambient-package-update/).
So you don't have to worry about the maintenance of this package. All important configuration and setup files are
being rendered by this updater. It works similar to well-known updaters like `pyupgrade` or `django-upgrade`.

To run an update, refer to the [documentation page](https://pypi.org/project/ambient-package-update/)
of the "ambient-package-update".
{% include "snippets/badges.tpl" %}
{% include "snippets/tagline.tpl"%}
{% include "snippets/links.tpl" %}

{% include "snippets/content.tpl" %}
{% include "snippets/installation.tpl" %}
{% include "snippets/contribute.tpl" %}
{% include "snippets/publish.tpl" %}
{% include "snippets/maintenance.tpl" %}
Loading

0 comments on commit 8f58d84

Please sign in to comment.