Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Lint
on: [push, pull_request]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- run: pip install -r requirements/common.txt
- run: ruff check --output-format=github .
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
.ruff_cache/

# Distribution / packaging
.Python
Expand Down
49 changes: 0 additions & 49 deletions .pre-commit-config.yaml

This file was deleted.

34 changes: 8 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,24 @@
# Cookiecutter Python Package

A [Cookiecutter](https://github.com/cookiecutter/cookiecutter) template for a Python package with Poetry as the dependency manager.
A [Cookiecutter](https://github.com/cookiecutter/cookiecutter) blank template for a project.

**NOTE:** Only Python 3.8+ is supported.

---

## Features
- Hooks: Pre-commit
- Formatters and Linters: Black, Flake8, Flake8-bugbear, Isort, and Lizard
- Package manager: pip
- Formatters and Linters: Ruff
- Testing Frameworks (Optional): Pytest, Coverage, and CovDefaults

## Usage

- Since this template uses Poetry as the dependancy manager, install poetry from `https://python-poetry.org/docs/#installation`

- Install the `cookiecutter` library.

```python
pip install cookiecutter
```
OR
```python
python -m pip install cookiecutter
```

- Run the command:
```python
cookiecutter https://github.com/gurashish1singh/cookiecutter-python.git
```
OR
```python
python -m cookiecutter https://github.com/gurashish1singh/cookiecutter-python.git
```
- bash setup.sh

- This template uses post-project generation hooks to:
- Initialize a git repository (with default branch as main), IF the working directory is not already a git repository.

**NOTE**: You will have to create a repositry on remote if it doesn't already exist before running the cookiecutter command.
- Create a Poetry virtualenv
- Install all dependencies
- Install the pre-commit and pre-push hooks
- Create a python virtual environment and activate it
- Install ruff
- Include an initial lint github workflow
4 changes: 2 additions & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"project_name": "Boilerplate Template",
"github_repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}",
"project_short_description": "Minimal python cookiecutter template with Poetry as the dependency manager and Black, Isort, Flake8, Flake8-bugbear as the linters.",
"project_short_description": "Minimal python cookiecutter template with pip as the package manager and Ruff as the linting tool.",
"version": "1.0.0",
"pytest": ["y", "n"]
"pytest": ["n", "y"]
}
124 changes: 101 additions & 23 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
#!/usr/bin/env python
from __future__ import annotations

import os
import pathlib
import shlex
import subprocess
import sys
import venv
from pathlib import Path

ERROR_MSG = "Error occured while running command"
PRETTY_LINES = "*" * 80
PROJECT_NAME = "{{ cookiecutter.project_slug }}"
# TODO: Can allow user to pass it in through cookiecutter.json
VENV_NAME = ".venv"
VIRTUAL_ENV = "VIRTUAL_ENV"

SUBPROCESS_PARAMS = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"check": True,
}


def main() -> int:
Expand All @@ -19,7 +31,12 @@ def main() -> int:

if return_code_one == 0:
return_code_two = setup_environment()
return 0 or return_code_one or return_code_two

if return_code_two == 0:
return_code_three = _rename_pyproject_toml_file()
print(f"New project {PROJECT_NAME!r} is setup.")

return 0 or return_code_one or return_code_two or return_code_three


def initialize_git() -> int:
Expand All @@ -30,14 +47,14 @@ def initialize_git() -> int:
),
"init_git": (
shlex.split("git init"),
"Initializing an empty git repository locally. You will have to create a repo "
"Initializing an empty git repository locally. You will have to create a repository "
"on remote.\n",
),
}
for cmds, message in COMMANDS_AND_MESSAGE.values():
print(message)
try:
subprocess.run(cmds, check=True)
subprocess.run(cmds, **SUBPROCESS_PARAMS)
except subprocess.CalledProcessError as e:
print(ERROR_MSG, e)
return e.returncode
Expand All @@ -46,26 +63,87 @@ def initialize_git() -> int:


def setup_environment() -> int:
return_code = 0
try:
# Always create a new environment in the project dir
python_venv_path = _create_new_environment()
system_type, python_executable_path, python_activate_script_path = _get_python_paths(venv_path=python_venv_path)
_activate_environment(system_type, python_activate_script_path)
except Exception:
return_code = -1
return_code = _install_requirements(python_executable_path)
return 0 or return_code

COMMANDS_AND_MESSAGE = {
"install_poetry": (
shlex.split("poetry install"),
f"\n{PRETTY_LINES}\nInstalling poetry virtual environment",
),
"install_pre_commit": (
shlex.split(
"poetry run pre-commit install --hook-type pre-commit --hook-type pre-push"
),
f"\n{PRETTY_LINES}\nInstalling pre-commit hooks",
),
}
for cmds, message in COMMANDS_AND_MESSAGE.values():
print(message)
try:
subprocess.run(cmds, check=True)
except subprocess.CalledProcessError as e:
print(ERROR_MSG, e)
return e.returncode

def _create_new_environment() -> str:
parent_dir = pathlib.Path(os.getcwd()).parent.resolve()
project_name = PROJECT_NAME.strip()
python_venv_path = str(parent_dir / project_name / VENV_NAME)

print(PRETTY_LINES)
print(f"Attempting to create a new virtual env at {python_venv_path}")
try:
venv.create(env_dir=python_venv_path, with_pip=True)
except Exception:
print("An unexpected error has occured")
raise

print(f"Successfully created virtualenv at: {python_venv_path!r}")
return python_venv_path


def _get_python_paths(venv_path: str) -> tuple[str, str, str]:
sys_type = sys.platform
if sys_type == "win32":
python_executable_path = pathlib.Path(venv_path, "Scripts", "python.exe")
activate_script_path = pathlib.Path(venv_path, "Scripts", "activate")
elif sys_type in ("darwin", "linux", "linux2"):
python_executable_path = pathlib.Path(venv_path, "bin", "python")
activate_script_path = pathlib.Path(venv_path, "bin", "activate")
else:
raise OSError(f"Unsupported platform: {sys_type!r}")
return sys_type, str(python_executable_path), str(activate_script_path)


def _activate_environment(system_type: str, activate_script_path: str) -> None:
if system_type == "win32":
subprocess.call(["cmd.exe", "c", activate_script_path])
elif system_type in ("darwin", "linux", "linux2"):
subprocess.call(f"source {activate_script_path}", shell=True)

print("Successfully activated virtualenv.")
print(f"\n{PRETTY_LINES}")


def _install_requirements(python_executable_path: str) -> int:
print("Installing all dependencies from the requirements.txt file.\n")
try:
subprocess.run(
[python_executable_path, "-m", "pip", "install", "-r", "requirements.txt"],
**SUBPROCESS_PARAMS,
)
if "{{ cookiecutter.project_slug }}":
subprocess.run(
[python_executable_path, "-m", "pip", "install", "-r", "dev-requirements.txt"],
**SUBPROCESS_PARAMS,
)
except subprocess.CalledProcessError as e:
print(ERROR_MSG, e)
return e.returncode

print(PRETTY_LINES)
return 0


def _rename_pyproject_toml_file() -> int:
try:
os.rename(
"template-pyproject.toml",
"pyproject.toml",
)
except subprocess.CalledProcessError as e:
print(ERROR_MSG, e)
return e.returncode

return 0

Expand Down
Loading
Loading