-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rename project to pyprefab and add cli to create a boilerplate python…
… 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
Showing
19 changed files
with
616 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Empty file.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.