Adding templates
This commit is contained in:
parent
afbcaadd29
commit
b117994c9e
53
README.md
53
README.md
|
@ -23,9 +23,48 @@ Clone the repository and install poetry-slam locally. You need the following pre
|
||||||
% pip3 install dist/*.whl
|
% pip3 install dist/*.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Starting a New Project
|
||||||
|
|
||||||
### Configuring Your Project
|
poetry-slam can generate a boilerplate python project with a cli:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
% mkdir /tmp/test_project
|
||||||
|
% cd /tmp/test_project
|
||||||
|
% git init
|
||||||
|
Initialized empty Git repository in /tmp/test_project/.git/
|
||||||
|
|
||||||
|
% slam new test_project
|
||||||
|
Added poetry-slam defaults to pyproject.toml
|
||||||
|
Formatting...
|
||||||
|
Installing...
|
||||||
|
Testing...
|
||||||
|
========== test session starts ==========
|
||||||
|
platform linux -- Python 3.10.12, pytest-8.1.1, pluggy-1.4.0
|
||||||
|
rootdir: /tmp/test_project
|
||||||
|
configfile: pyproject.toml
|
||||||
|
plugins: cov-5.0.0
|
||||||
|
collected 1 item
|
||||||
|
|
||||||
|
test/test_test_project.py x [100%]
|
||||||
|
test/test_test_project_cli.py x [100%]
|
||||||
|
|
||||||
|
---------- coverage: platform linux, python 3.10.12-final-0 ----------
|
||||||
|
Name Stmts Miss Cover Missing
|
||||||
|
------------------------------------------------------------
|
||||||
|
src/test_project/__init__.py 0 0 100%
|
||||||
|
src/test_project/cli.py 23 9 61% 37-50, 57
|
||||||
|
------------------------------------------------------------
|
||||||
|
TOTAL 23 9 61%
|
||||||
|
|
||||||
|
|
||||||
|
========== 2 xfailed in 0.11s ==========
|
||||||
|
Building...
|
||||||
|
slam build: SUCCESS
|
||||||
|
Successfully initialized new default project test_project.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Configuring An Existing Project
|
||||||
|
|
||||||
poetry-slam expects your package python source in `src/` and your tests in `test/`.
|
poetry-slam expects your package python source in `src/` and your tests in `test/`.
|
||||||
|
|
||||||
|
@ -37,7 +76,7 @@ packages = [
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Initializing poetry-slam
|
#### Initializing poetry-slam
|
||||||
|
|
||||||
The first time you use poetry-slam in a new project, it's a good idea to run `slam init`. This will add opinionated defaults for the build tooling directly to your `pyproject.toml`. It will also add both pytest and pytest-cov as dependencies in your dev group.
|
The first time you use poetry-slam in a new project, it's a good idea to run `slam init`. This will add opinionated defaults for the build tooling directly to your `pyproject.toml`. It will also add both pytest and pytest-cov as dependencies in your dev group.
|
||||||
|
|
||||||
|
@ -48,15 +87,13 @@ Added poetry-slam defaults to pyproject.toml
|
||||||
% poetry update
|
% poetry update
|
||||||
```
|
```
|
||||||
|
|
||||||
### What You Don't Need
|
#### What You Don't Need
|
||||||
|
|
||||||
Aside from pytest and pytest-cov, which poetry-slam will add for you, You don't need other dependencies in your project's dev group. When you install poetry-slam you will also get isort and friends if they aren't already present, and these tools will automatically load configuration from the first `pyproject.toml` they find in your directory hierarchy.
|
Aside from pytest and pytest-cov, which poetry-slam will add for you, You don't need other dependencies in your project's dev group. When you install poetry-slam you will also get isort and friends if they aren't already present, and these tools will automatically load configuration from the first `pyproject.toml` they find in your directory hierarchy.
|
||||||
|
|
||||||
You also don't need tool-specific configuration files or global defaults, since the configs are added directly to your `pyproject.toml`.
|
You also don't need tool-specific configuration files or global defaults, since the configs are added directly to your `pyproject.toml`.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### The Build Loop
|
### The Build Loop
|
||||||
|
|
||||||
The most common usage and the default if no command is specified is to do a `build`, which will:
|
The most common usage and the default if no command is specified is to do a `build`, which will:
|
||||||
|
@ -108,7 +145,7 @@ You can also run individual steps; see `slam --help` for details:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing With Pytest
|
## Testing With Pytest
|
||||||
|
|
||||||
Anything passed to `slam test` will be passed directly to pytest as command-line arguments. So for example:
|
Anything passed to `slam test` will be passed directly to pytest as command-line arguments. So for example:
|
||||||
|
|
||||||
|
@ -117,7 +154,7 @@ Anything passed to `slam test` will be passed directly to pytest as command-line
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Debugging
|
## Debugging
|
||||||
|
|
||||||
Get gory details with the combination of `--verbose` and `--log-level` most suitable to your liking:
|
Get gory details with the combination of `--verbose` and `--log-level` most suitable to your liking:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -6,12 +8,15 @@ import typer
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
from poetry_slam.build_tool import BuildTool
|
from poetry_slam.build_tool import BuildTool
|
||||||
|
from poetry_slam.templates import TEMPLATE_ROOT, templates
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
app_state = dict()
|
app_state = dict()
|
||||||
|
|
||||||
logger = logging.getLogger("slam.cli")
|
logger = logging.getLogger("slam.cli")
|
||||||
|
|
||||||
|
Template = Enum("Template", ((name, name) for name in templates()))
|
||||||
|
|
||||||
|
|
||||||
@app.callback(invoke_without_command=True)
|
@app.callback(invoke_without_command=True)
|
||||||
def main(
|
def main(
|
||||||
|
@ -39,7 +44,7 @@ def init():
|
||||||
"""
|
"""
|
||||||
Add opinionated defaults to your pyproject.toml.
|
Add opinionated defaults to your pyproject.toml.
|
||||||
"""
|
"""
|
||||||
defaults = Path(__file__).parent / "defaults.toml"
|
defaults = TEMPLATE_ROOT / "slam_defaults.toml"
|
||||||
target = app_state["build_tool"].project_root / "pyproject.toml"
|
target = app_state["build_tool"].project_root / "pyproject.toml"
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
raise RuntimeError(f"Could not find pyproject.toml at '{target}'")
|
raise RuntimeError(f"Could not find pyproject.toml at '{target}'")
|
||||||
|
@ -63,6 +68,34 @@ def init():
|
||||||
app_state["build_tool"].run_with_poetry("add", "-G", "dev", "pytest", "pytest-cov")
|
app_state["build_tool"].run_with_poetry("add", "-G", "dev", "pytest", "pytest-cov")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def new(
|
||||||
|
template: Template = typer.Option("default", help="The template to use."),
|
||||||
|
project_name: str = typer.Argument(
|
||||||
|
help="The name of your new project. Defaults to the current directory name"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize a project with boilerplate code.
|
||||||
|
"""
|
||||||
|
tmpl = templates()[template.name]
|
||||||
|
tmpl.values = {
|
||||||
|
'PROJECT_NAME': project_name,
|
||||||
|
'PACKAGE_NAME': project_name,
|
||||||
|
'DESCRIPTION': f"{project_name}: automatically generated by poetry-slam.",
|
||||||
|
'AUTHOR_NAME': os.environ['USER']
|
||||||
|
}
|
||||||
|
logging.debug(f"Configuring template: {tmpl}")
|
||||||
|
tmpl.apply()
|
||||||
|
init()
|
||||||
|
backup = app_state["build_tool"].project_root / "pyproject.toml.slam-orig"
|
||||||
|
logging.debug(f"Cleaning up {backup}...")
|
||||||
|
backup.unlink()
|
||||||
|
build()
|
||||||
|
backup = app_state["build_tool"].run(project_name, "--help")
|
||||||
|
print(f"Successfully initialized new {template.name} project {project_name}.")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def format():
|
def format():
|
||||||
"""
|
"""
|
||||||
|
|
9
src/poetry_slam/templates/__init__.py
Normal file
9
src/poetry_slam/templates/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from .project_template import ProjectTemplate, TEMPLATE_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def templates():
|
||||||
|
avail = dict()
|
||||||
|
for p in TEMPLATE_ROOT.iterdir():
|
||||||
|
if p.is_dir():
|
||||||
|
avail[p.name] = ProjectTemplate(p)
|
||||||
|
return avail
|
10
src/poetry_slam/templates/default/README.md
Normal file
10
src/poetry_slam/templates/default/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# ${PROJECT_NAME}
|
||||||
|
|
||||||
|
Auto-generated by poetry-slam.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
${PACKAGE_NAME} --help
|
||||||
|
```
|
25
src/poetry_slam/templates/default/pyproject.toml
Normal file
25
src/poetry_slam/templates/default/pyproject.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "${PROJECT_NAME}"
|
||||||
|
version = "1.0"
|
||||||
|
description = "${DESCRIPTION}"
|
||||||
|
authors = ["${AUTHOR_NAME}"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [
|
||||||
|
{include = "*", from = "src"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
python-dotenv = "^0.21.0"
|
||||||
|
rich = "^13.7.0"
|
||||||
|
typer = "^0.9.0"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
${PACKAGE_NAME} = "${PACKAGE_NAME}.cli:app"
|
58
src/poetry_slam/templates/default/src/${PACKAGE_NAME}/cli.py
Normal file
58
src/poetry_slam/templates/default/src/${PACKAGE_NAME}/cli.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_DEFAULTS = """
|
||||||
|
# ${PROJECT_NAME} Defaults
|
||||||
|
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
"""
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
app_state = dict(
|
||||||
|
config_file=Path("~/.config/${PROJECT_NAME}.conf").expanduser(),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("${PACKAGE_NAME}.cli")
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(invoke_without_command=True)
|
||||||
|
def main(
|
||||||
|
context: typer.Context,
|
||||||
|
verbose: bool = typer.Option(False, help="Enable verbose output."),
|
||||||
|
log_level: str = typer.Option("error", help=" Set the log level."),
|
||||||
|
config_file: Optional[Path] = typer.Option(
|
||||||
|
app_state["config_file"],
|
||||||
|
help="Path to the ${PACKAGE_NAME} configuration file",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Configure the execution environment with global parameters.
|
||||||
|
"""
|
||||||
|
app_state["config_file"] = config_file
|
||||||
|
load_dotenv(stream=io.StringIO(CONFIG_DEFAULTS))
|
||||||
|
load_dotenv(app_state["config_file"])
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(message)s",
|
||||||
|
level=getattr(logging, log_level.upper()),
|
||||||
|
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
|
||||||
|
)
|
||||||
|
app_state["verbose"] = verbose
|
||||||
|
|
||||||
|
if context.invoked_subcommand is None:
|
||||||
|
logger.debug("No command specified; invoking default handler.")
|
||||||
|
run(context)
|
||||||
|
|
||||||
|
|
||||||
|
def run(context: typer.Context):
|
||||||
|
"""
|
||||||
|
The default CLI entrypoint is ${PACKAGE_NAME}.cli.run().
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Please define ${PACKAGE_NAME}.cli.run().")
|
|
@ -0,0 +1,8 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_tests_are_implemented():
|
||||||
|
import ${PACKAGE_NAME}
|
||||||
|
print("Yyou have not implemented any tests yet.")
|
||||||
|
assert False
|
|
@ -0,0 +1,8 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ${PACKAGE_NAME} import cli
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_tests_are_implemented():
|
||||||
|
assert cli.main()
|
66
src/poetry_slam/templates/project_template.py
Normal file
66
src/poetry_slam/templates/project_template.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_ROOT = Path(__file__).parent
|
||||||
|
|
||||||
|
logger = logging.getLogger("slam.templates")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProjectTemplate:
|
||||||
|
root: Path = TEMPLATE_ROOT / "default"
|
||||||
|
values: field(default_factory=dict) = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paths(self) -> list:
|
||||||
|
def walk(path):
|
||||||
|
files = []
|
||||||
|
for path in path.iterdir():
|
||||||
|
if path.is_dir():
|
||||||
|
files += walk(path)
|
||||||
|
else:
|
||||||
|
files.append(path)
|
||||||
|
return files
|
||||||
|
return walk(self.root)
|
||||||
|
|
||||||
|
def render(self, path) -> tuple:
|
||||||
|
"""
|
||||||
|
Return a tuple of the formatted destination path name and the file contents, if any.
|
||||||
|
"""
|
||||||
|
if path.is_dir():
|
||||||
|
body = None
|
||||||
|
else:
|
||||||
|
body = Template((TEMPLATE_ROOT / path).read_text()).substitute(self.values)
|
||||||
|
|
||||||
|
relpath = path.relative_to(self.root)
|
||||||
|
path_template = Template(str(relpath)).substitute(self.values)
|
||||||
|
return Path(path_template), body
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
"""
|
||||||
|
Apply the template to the current directory, creating any missing files and directories.
|
||||||
|
"""
|
||||||
|
if not Path(".git").exists():
|
||||||
|
raise RuntimeError("Cannot apply a template outside of a project root (no .git present here).")
|
||||||
|
if Path("pyproject.toml").exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot apply a template to an existing project. "
|
||||||
|
"Maybe you meant slam init, to apply defaults to your pyproject.toml?"
|
||||||
|
)
|
||||||
|
for path in self.paths:
|
||||||
|
logger.debug(f"Processing {path}...")
|
||||||
|
target, body = self.render(path)
|
||||||
|
if not target.parent.exists():
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not target.exists():
|
||||||
|
target.write_text(body)
|
||||||
|
logger.info(f"Created {target}")
|
||||||
|
else:
|
||||||
|
logger.warn(f"Skipping existing file {target}")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.root}: {self.values}"
|
Loading…
Reference in New Issue
Block a user