Skip to content

Commit

Permalink
Rename project to pyprefab and add cli to create a boilerplate python…
Browse files Browse the repository at this point in the history
… package (#18)

* rename hello_world module to pyprefab

* Add dependencies for CLI and templating

* Add the first piece of templating: creating a pyproject.toml

* Remove pre-dynamic version artifact

* Add README.md template

* Add remaining templates for skeleton app

Also make some additions and corrections to the pyproject.toml template

* Update README

* Fix typos and ensure files generated by jinja end with newlines

* Add pre-commit clarifications

* Remove artifacts from earlier iteration of pyprefab

Move logging setup to __init__.py for consistency with the boilerplate
generated by pyprefab

* Get consistent with test vs tests
  • Loading branch information
bsweger authored Jan 13, 2025
1 parent c64c29a commit 80b4dec
Show file tree
Hide file tree
Showing 19 changed files with 616 additions and 113 deletions.
111 changes: 75 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,107 @@
# pyprefab

A Python template for personal use. The package itself doesn't do much, but I
keep it up to date as my Python workflow and tooling preferences evolve.
Creates a new Python package from an opinioned set of templates.

## Installing and running the package (no development)
## Installing pyprefab

To install this package via pip:

```bash
pip install git+https://github.com/bsweger/pyprefab.git
pip install pyprefab
```

To run it:
## Generating boilerplate for a new Python package

Use pyprefab's command line interface to create a new Python package:

```bash
hello_world
➜ pyprefab-create --help

Usage: pyprefab-create [OPTIONS] NAME

Generate a new Python project from templates.

╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
* name TEXT Name of the project [default: None] [required] │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
* --author TEXT Project author [default: None] [required] │
│ --description TEXT Project description │
│ --directory PATH Directory that will contain the project (defaults to current directory) [default: None] │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it or customize the installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

For example:

```
pyprefab-create project_test --author lassie --description "this is a pet project for lassie" --directory ~/code/lassie
```

## Setup for local development
## Setting up the package's dev environment

The instructions below outline how to set up a development environment based
on uv tooling.
Follow the steps below to create a development environment for the package.

Prerequisites:
These directions use `uv`, but you can use your preferred tooling.

- [uv](https://docs.astral.sh/uv/getting-started/installation/)
1. `cd` to the directory of the new Python package

1. Clone this repository
2. Change to the repo's root directory:
2. Create a virtual environment seeded with pip:

```bash
cd pyprefab
```script
uv venv --seed
```
3. Create a Python virtual environment and install dependencies. The command
below creates a virtual environment in the `.venv` directory, installs Python
if needed, installs project dependencies (including dev dependencies), and
installs the package in
[editable mode](https://setuptools.pypa.io/en/stable/userguide/development_mode.html):
3. Install dependencies + project as editable module
```bash
```script
uv sync
```
4. Run the test suite to confirm that everything is working:
4. Test the project setupt:
```script
uv run <your_package_name>
```
You should see a log output stating that the project has been set up correctly.
For example:
`2025-01-13 02:29:08 [info ] project_test successfully created.`
You can also run the tests:
```bash
```script
uv run pytest
```
### Updating dependencies
**Note:** `uv run` runs commands in the virtual environment created by uv
(see step 2). Alternately, you can activate the virtual environment the
old-fashioned way and then run commands without the `uv run` prefix:
Use [`uv add`](https://docs.astral.sh/uv/reference/cli/#uv-add) to include a
new dependency in the project. This command will install the new dependency
into the virtual environment, add it to `uv.lock`, and update the
`dependencies` section of [`pyproject.toml`](pyproject.toml).
```script
source .venv/bin/activate
<your package name>
pytest
```
```bash
uv add <package-name>
```
**Optional:**
To add a dependency to a specific group (adding a dev dependency, for example),
use the `--group` flag:
- Add the new project to a git repository:
```bash
uv add <package-name> --group dev
```
```script
git init
git add .
git commit -am "Initial commit"
```
- If you use [pre-commit](https://pre-commit.com/), pyprefab's boilerplate
includes a baseline `pre-commit-config.yaml` configuration. To use it, make
sure the project has been added to git (see above) and run the following
command to install the pre-commit git hook scripts:
```script
pre-commit install
```
18 changes: 13 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dynamic = ['version']
dependencies = [
'structlog',
'rich',
"typer",
"jinja2>=3.1.5",
]

[dependency-groups]
Expand All @@ -26,33 +28,39 @@ dev = [
'ruff',
]

[project.scripts]
pyprefab-create = "pyprefab.cli:app"

[project.urls]
Repository = "https://github.com/bsweger/pyprefab.git"

[project.entry-points.'console_scripts']
hello_world = 'hello_world.app:main'
pyprefab = 'pyprefab.app:main'

[build-system]
# Minimum requirements for the build system to execute.
requires = ['setuptools', 'wheel', "setuptools_scm>=8"]
requires = ['setuptools>=45', 'wheel', "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
namespaces = true
where = ["src"]

[tool.setuptools.package-data]
"pyprojgen.templates" = ["*.j2", "*.jinja2", "*"]

[tool.setuptools_scm]
local_scheme = "no-local-version"

[tool.hello_world]
[tool.pyprefab]
# to write json-formatted logs to disk, uncomment the following line specify the file location
# log_file = '/path/to/logs/files/hello_world.log'
# log_file = '/path/to/logs/files/pyprefab.log'

[tool.pytest.ini_options]
addopts = "--random-order"
tmp_path_retention_policy = "none"
testpaths = [
"tests",
"test",
]

[tool.ruff]
Expand Down
1 change: 0 additions & 1 deletion src/hello_world/__init__.py

This file was deleted.

20 changes: 0 additions & 20 deletions src/hello_world/app.py

This file was deleted.

Empty file removed src/hello_world/util/__init__.py
Empty file.
15 changes: 0 additions & 15 deletions src/hello_world/util/date.py

This file was deleted.

32 changes: 32 additions & 0 deletions src/pyprefab/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""pyprefab initialization."""

import sys

import structlog


def setup_logging():
shared_processors = [
structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S'),
structlog.processors.add_log_level,
]

if sys.stderr.isatty():
# If we're in a terminal, pretty print the logs.
processors = shared_processors + [
structlog.dev.ConsoleRenderer(),
]
else:
# Otherwise, output logs in JSON format
processors = shared_processors + [
structlog.processors.dict_tracebacks,
structlog.processors.JSONRenderer(),
]

structlog.configure(
processors=processors,
cache_logger_on_first_use=True,
)


setup_logging()
90 changes: 90 additions & 0 deletions src/pyprefab/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Command-line interface for the pyprefab package."""
import shutil
from pathlib import Path
from typing import Optional

import typer
from jinja2 import Environment, FileSystemLoader
from rich import print
from rich.panel import Panel

app = typer.Typer(help='Generate python project scaffolding based on pyprefab.')

def validate_project_name(name: str) -> bool:
"""Validate project name follows Python package naming conventions."""
return name.isidentifier() and name.islower()

@app.command()
def create(
name: str = typer.Argument(..., help='Name of the project'),
author: str = typer.Option(..., '--author', help='Project author'),
description: str = typer.Option('', '--description', help='Project description'),
project_dir: Optional[Path] = typer.Option(
None, '--directory', help='Directory that will contain the project (defaults to current directory)'
),
):
"""Generate a new Python project from templates."""
if not validate_project_name(name):
typer.secho(
f'Error: {name} is not a valid Python package name',
fg=typer.colors.RED,
)
raise typer.Exit(1)

templates_dir = Path(__file__).parent / 'templates'
target_dir = project_dir or Path.cwd() / name

try:
# Create project directory
target_dir.mkdir(parents=True, exist_ok=True)

# Template context
context = {
'project_name': name,
'author': author,
'description': description,
}

# Process templates
env = Environment(loader=FileSystemLoader(templates_dir))
path_env = Environment() # For rendering path names
#env = Environment(loader=FileSystemLoader(templates_dir))
for template_file in templates_dir.rglob('*'):
if template_file.is_file():
rel_path = template_file.relative_to(templates_dir)
template = env.get_template(str(rel_path))
output = template.render(**context)

# Process path parts through Jinja
path_parts = []
for part in rel_path.parts:
# Render each path component through Jinja
rendered_part = path_env.from_string(part).render(**context)
if rendered_part.endswith('.j2'):
rendered_part = rendered_part[:-3]
path_parts.append(rendered_part)

# Create destination path preserving structure
dest_file = target_dir.joinpath(*path_parts)
dest_file.parent.mkdir(parents=True, exist_ok=True)
dest_file.write_text(output)

print(Panel.fit(
f'✨ Created new project [bold green]{name}[/] in {target_dir}\n'
f'Author: [blue]{author}[/]\n'
f'Description: {description}',
title='Project Created Successfully',
border_style='green',
))

except Exception as e:
typer.secho(f'Error creating project: {str(e)}', fg=typer.colors.RED)
if target_dir.exists():
shutil.rmtree(target_dir)
raise typer.Exit(1)

def main():
app()

if __name__ == '__main__':
main()
Loading

0 comments on commit 80b4dec

Please sign in to comment.