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
|
||||
```
|
||||
|
||||
## 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/`.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -48,15 +87,13 @@ Added poetry-slam defaults to pyproject.toml
|
|||
% 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.
|
||||
|
||||
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 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:
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
@ -6,12 +8,15 @@ import typer
|
|||
from rich.logging import RichHandler
|
||||
|
||||
from poetry_slam.build_tool import BuildTool
|
||||
from poetry_slam.templates import TEMPLATE_ROOT, templates
|
||||
|
||||
app = typer.Typer()
|
||||
app_state = dict()
|
||||
|
||||
logger = logging.getLogger("slam.cli")
|
||||
|
||||
Template = Enum("Template", ((name, name) for name in templates()))
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(
|
||||
|
@ -39,7 +44,7 @@ def init():
|
|||
"""
|
||||
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"
|
||||
if not target.exists():
|
||||
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.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()
|
||||
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