Skip to content

Commit

Permalink
Add "uv-python" template
Browse files Browse the repository at this point in the history
  • Loading branch information
Mr. Test committed Oct 30, 2024
1 parent e2b20e5 commit ecb3e82
Show file tree
Hide file tree
Showing 59 changed files with 7,222 additions and 2 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ jobs:
strategy:
fail-fast: false
matrix:
package: ['managetemplates', 'pipenv-python', 'piptools-python', 'yunohost_django_package']
python-version: ["3.12", "3.11"]
package: [
'managetemplates',
'pipenv-python',
'piptools-python',
'uv-python',
'yunohost_django_package'
]
python-version: ["3.12", "3.11"] # TODO: Add 3.13
steps:
- name: Checkout
run: |
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ Use with [manageprojects](https://github.com/jedie/manageprojects), e.g.:
./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory piptools-python ~/foobar/
```

## uv based python package

* Requirement management with [uv](https://github.com/astral-sh/uv)
* [click](https://click.palletsprojects.com) based CLI for app and dev mode
* Auto virtualenv bootstrap, just by calling the `cli.py` / `dev-cli.py`
* used [pyproject.toml](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) for everything
* Has basic unittest


Cookiecutter template tests are here: [uv-python/tests.py](https://github.com/jedie/cookiecutter_templates/blob/main/uv-python/tests.py)


Use with vanilla [cookiecutter](https://github.com/cookiecutter/cookiecutter), e.g.:
```shell
cookiecutter https://github.com/jedie/cookiecutter_templates/ --directory uv-python
```

Use with [manageprojects](https://github.com/jedie/manageprojects), e.g.:
```shell
./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory uv-python ~/foobar/
```

## Base Django YunoHost app

[YunoHost](https://yunohost.org) is a Open-Source, Debian based self-hosting solution.
Expand Down Expand Up @@ -185,6 +207,7 @@ Usage: ./dev-cli.py [OPTIONS] COMMAND [ARGS]...
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)

* [v0.5.0](https://github.com/jedie/cookiecutter_templates/compare/v0.3.0...v0.5.0)
* 2024-10-29 - Add "uv-python" template
* 2024-09-26 - Move pip-compile settings into pyproject.toml + create pywheel hashes
* 2024-09-26 - Add "setup Python" to YunoHost template
* 2024-09-25 - Simplify: Use update_readme_history() from cli-base-utilities
Expand Down
20 changes: 20 additions & 0 deletions generated_templates/uv-python/your_cool_package/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# see https://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.py]
max_line_length = 119

[{Makefile,**.mk}]
indent_style = tab
insert_final_newline = false

[{*.yaml,*.yml}]
indent_size = 2
7 changes: 7 additions & 0 deletions generated_templates/uv-python/your_cool_package/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# Move to pyproject.toml after: https://github.com/PyCQA/flake8/issues/234
#
[flake8]
exclude = .*, dist, htmlcov
#ignore = E402
max-line-length = 119
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

name: tests

on:
push:
branches:
- main
pull_request:
schedule:
- cron: '0 8 * * *'

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.11"]
steps:
- name: Checkout
run: |
echo $GITHUB_REF $GITHUB_SHA
git clone https://github.com/$GITHUB_REPOSITORY.git .
git fetch origin $GITHUB_SHA:temporary-ci-branch
git checkout $GITHUB_SHA || (git fetch && git checkout $GITHUB_SHA)
- name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v5
# https://github.com/marketplace/actions/setup-python
with:
python-version: '${{ matrix.python-version }}'
cache: 'pip' # caching pip dependencies
cache-dependency-path: '**/requirements.*.txt'

- name: 'Bootstrap app venv'
# The first CLI call will create the .venv
run: |
./cli.py version
- name: 'app CLI help'
run: |
./cli.py --help
- name: 'Bootstrap dev venv'
# The first CLI call will create the .venv
run: |
./dev-cli.py version
- name: 'dev CLI help'
run: |
./dev-cli.py --help
- name: 'Run pip-audit'
run: |
./dev-cli.py pip-audit
- name: 'Run tests with Python v${{ matrix.python-version }}'
env:
PYTHONUNBUFFERED: 1
PYTHONWARNINGS: always
run: |
./dev-cli.py coverage
- name: 'Upload coverage report'
uses: codecov/codecov-action@v4
# https://github.com/marketplace/actions/codecov
with:
fail_ci_if_error: false
verbose: true

15 changes: 15 additions & 0 deletions generated_templates/uv-python/your_cool_package/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.*
*.egg-info
__pycache__
/dist/
/build/
/coverage.*
*.orig

!.github
!.editorconfig
!.flake8
!.gitignore
!.pre-commit-config.yaml
!.pre-commit-hooks.yaml
!.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# pre-commit plugin configuration
# See https://pre-commit.com for more information
default_install_hook_types:
- pre-commit
- post-rewrite
- pre-push

repos:
- repo: https://github.com/jedie/cli-base-utilities
rev: v0.13.1
hooks:
- id: update-readme-history
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# https://pre-commit.com/#creating-new-hooks
- id: update-readme-history
name: cli-base-utilities
description: >-
Update history in README.md from git log.
entry: "python -m cli_base update-readme-history -v"
language: python
language_version: python3
require_serial: true
pass_filenames: false
always_run: true
verbose: true
stages: [pre-commit, post-rewrite, pre-push]
62 changes: 62 additions & 0 deletions generated_templates/uv-python/your_cool_package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# your_cool_package

[![tests](https://github.com/john-doh/your_cool_package/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/john-doh/your_cool_package/actions/workflows/tests.yml)
[![codecov](https://codecov.io/github/john-doh/your_cool_package/branch/main/graph/badge.svg)](https://app.codecov.io/github/john-doh/your_cool_package)
[![your_cool_package @ PyPi](https://img.shields.io/pypi/v/your_cool_package?label=your_cool_package%20%40%20PyPi)](https://pypi.org/project/your_cool_package/)
[![Python Versions](https://img.shields.io/pypi/pyversions/your_cool_package)](https://github.com/john-doh/your_cool_package/blob/main/pyproject.toml)
[![License GPL-3.0-or-later](https://img.shields.io/pypi/l/your_cool_package)](https://github.com/john-doh/your_cool_package/blob/main/LICENSE)

A minimal Python package

## CLI

[comment]: <> (✂✂✂ auto generated main help start ✂✂✂)
```
Usage: ./cli.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
│ update-readme-history Update project history base on git commits/tags in README.md │
│ version Print version and exit │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
```
[comment]: <> (✂✂✂ auto generated main help end ✂✂✂)


## dev CLI

[comment]: <> (✂✂✂ auto generated dev help start ✂✂✂)
```
Usage: ./dev-cli.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
│ check-code-style Check code style by calling darker + flake8 │
│ coverage Run tests and show coverage report. │
│ fix-code-style Fix code style of all your_cool_package source code files via darker │
│ install Install requirements and 'your_cool_package' via pip as editable. │
│ mypy Run Mypy (configured in pyproject.toml) │
│ pip-audit Run pip-audit check against current requirements files │
│ publish Build and upload this project to PyPi │
│ test Run unittests │
│ tox Run tox │
│ update Update "requirements*.txt" dependencies files │
│ update-test-snapshot-files Update all test snapshot files (by remove and recreate all snapshot │
│ files) │
│ version Print version and exit │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
```
[comment]: <> (✂✂✂ auto generated dev help end ✂✂✂)


## History

[comment]: <> (✂✂✂ auto generated history start ✂✂✂)



[comment]: <> (✂✂✂ auto generated history end ✂✂✂)
123 changes: 123 additions & 0 deletions generated_templates/uv-python/your_cool_package/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3

"""
bootstrap CLI
~~~~~~~~~~~~~
Just call this file, and the magic happens ;)
"""

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


def print_no_pip_error():
print('Error: Pip not available!')
print('Hint: "apt-get install python3-venv"\n')


try:
from ensurepip import version
except ModuleNotFoundError as err:
print(err)
print('-' * 100)
print_no_pip_error()
raise
else:
if not version():
print_no_pip_error()
sys.exit(-1)


assert sys.version_info >= (3, 11), f'Python version {sys.version_info} is too old!'


if sys.platform == 'win32': # wtf
# Files under Windows, e.g.: .../.venv/Scripts/python.exe
BIN_NAME = 'Scripts'
FILE_EXT = '.exe'
else:
# Files under Linux/Mac and all other than Windows, e.g.: .../.venv/bin/python3
BIN_NAME = 'bin'
FILE_EXT = ''

BASE_PATH = Path(__file__).parent
VENV_PATH = BASE_PATH / '.venv-app'
BIN_PATH = VENV_PATH / BIN_NAME
PYTHON_PATH = BIN_PATH / f'python3{FILE_EXT}'
PIP_PATH = BIN_PATH / f'pip{FILE_EXT}'
UV_PATH = BIN_PATH / f'uv{FILE_EXT}'

DEP_LOCK_PATH = BASE_PATH / 'uv.lock'
DEP_HASH_PATH = VENV_PATH / '.dep_hash'

# script file defined in pyproject.toml as [console_scripts]
# (Under Windows: ".exe" not added!)
PROJECT_SHELL_SCRIPT = BIN_PATH / 'your_cool_package_app'


def get_dep_hash():
"""Get SHA512 hash from lock file content."""
return hashlib.sha512(DEP_LOCK_PATH.read_bytes()).hexdigest()


def store_dep_hash():
"""Generate .venv/.dep_hash"""
DEP_HASH_PATH.write_text(get_dep_hash())


def venv_up2date():
"""Is existing .venv is up-to-date?"""
if DEP_HASH_PATH.is_file():
return DEP_HASH_PATH.read_text() == get_dep_hash()
return False


def verbose_check_call(*popen_args):
print(f'\n+ {shlex.join(str(arg) for arg in popen_args)}\n')
return subprocess.check_call(popen_args)


def main(argv):
assert DEP_LOCK_PATH.is_file(), f'File not found: "{DEP_LOCK_PATH}" !'

# Create virtual env in ".venv/":
if not PYTHON_PATH.is_file():
print(f'Create virtual env here: {VENV_PATH.absolute()}')
builder = venv.EnvBuilder(symlinks=True, upgrade=True, with_pip=True)
builder.create(env_dir=VENV_PATH)

# Set environment variable for uv to use '.venv-app' as project environment:
os.environ['UV_PROJECT_ENVIRONMENT'] = str(VENV_PATH.absolute())

if not PROJECT_SHELL_SCRIPT.is_file() or not venv_up2date():
# Update pip
verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'pip')

# Install uv
verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'uv')

# install requirements
verbose_check_call(UV_PATH, 'sync', '--frozen', '--no-dev')

# install project
verbose_check_call(PIP_PATH, 'install', '--no-deps', '-e', '.')
store_dep_hash()

# Call our entry point CLI:
try:
verbose_check_call(PROJECT_SHELL_SCRIPT, *argv[1:])
except subprocess.CalledProcessError as err:
sys.exit(err.returncode)
except KeyboardInterrupt:
print('Bye!')
sys.exit(130)


if __name__ == '__main__':
main(sys.argv)
Loading

0 comments on commit ecb3e82

Please sign in to comment.