Skip to content

Commit c29bf7d

Browse files
authored
feat: add uv support (#7)
1 parent 508f8e3 commit c29bf7d

24 files changed

Lines changed: 1010 additions & 182 deletions

.github/workflows/deploy.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ jobs:
1515
- uses: actions/checkout@v4
1616
with:
1717
fetch-depth: 0
18-
- uses: pdm-project/setup-pdm@v4
18+
- uses: astral-sh/setup-uv@v7
1919
with:
20-
python-version: 3.11
21-
cache: true
22-
- run: pdm sync # Installs packages from pdm.lock (should be exactly the same every time this is run)
20+
version: "0.10.8"
21+
enable-cache: true
22+
- run: uv tool install commitizen
2323
- name: Create release notes
24-
run: pdm run cz changelog $(pdm run cz version --project) --dry-run > body.md
24+
run: uv tool run --from commitizen cz changelog $(uv tool run --from commitizen cz version --project) --dry-run > body.md
2525
- uses: softprops/action-gh-release@v2
2626
with:
2727
body_path: body.md

README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Numpy project template
22
[![Python 3.11](https://img.shields.io/badge/python-3.11+-blue.svg?logo=python&logoColor=cccccc)](https://www.python.org/downloads/)
3-
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm-project.org)
43
[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://squidfunk.github.io/mkdocs-material/)
54
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
5+
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
66
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
77
![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?logo=codecov)
88
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
@@ -13,7 +13,7 @@ Construct a `numpy`-based Python project from scratch for scientific computing a
1313
## :wrench: Features
1414

1515
- [Numpy](https://numpy.org/) a basic installation for the holy trifecta :dove: of `numpy, matplotlib, scipy`.
16-
- [PDM](https://pdm-project.org) for dependency, virtualenv, and package management.
16+
- [pdm](https://pdm-project.org) or [uv](https://docs.astral.sh/uv/) for dependency, virtualenv, and package management.
1717
- [Mkdocs material](https://squidfunk.github.io/mkdocs-material/) for simple, clean, automated, online code documentation.
1818
- [pre-commit](https://github.com/pre-commit/pre-commit) with [ruff](https://github.com/astral-sh/ruff) integration for code linting and formatting.
1919
- [pytest](https://docs.pytest.org/en/stable/index.html#) with [coverage](https://pytest-cov.readthedocs.io/en/latest/) for regression testing and code coverage.
@@ -22,24 +22,23 @@ Construct a `numpy`-based Python project from scratch for scientific computing a
2222
- [Github actions](https://docs.github.com/en/actions) for automated, build, test, and deployment.
2323

2424
## :round_pushpin: Quickstart
25-
Have python and git installed, then:
25+
Install copier with extensions-
2626
```shell
27-
pip install --user pdm
28-
pdm self add copier copier-templates-extensions
29-
30-
cd path/to/project
31-
pdm init --copier gh:eckelsjd/copier-numpy --trust
27+
pdm self add copier copier-templates-extensions # pdm, or
28+
uv tool install copier --with copier-templates-extensions # uv
3229
```
33-
That's it! Follow the questionnaire and then your `numpy`-based scientific computing project is ready to go.
34-
35-
All extra arguments are passed to `copier copy` (you're also welcome to just use `copier` directly).
30+
Then copy the template-
31+
```shell
32+
copier copy gh:eckelsjd/copier-numpy <dst_path> --trust
33+
```
34+
That's it! Follow the questionnaire and then your `numpy`-based scientific computing project is ready to go.
3635

3736
***Note:*** The `--trust` flag enables extensions used in this template. Please see [extensions.py](extensions.py) and [setup_github.py](setup_github.py) to make sure you trust this template (*spoiler:* these just add some global template variables and some basic `git` scripting).
3837

3938
## :snake: Publishing on PyPI
4039
Follow [this tutorial](https://docs.pypi.org/trusted-publishers/) to enable trusted publishing with Github actions. Then, do:
4140
```shell
42-
pdm bump
41+
cz bump
4342
```
4443
That's it! Your package will automatically deploy to PyPI and GitHub with a correctly-versioned `vX.X.X` tag.
4544

TODO.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@
2525
- [x] Newly copied repo should install pre-commit and setup
2626
- [x] Add license file automatically
2727
- [ ] Option in `setup_github.py` to detect and change who is logged in to gh CLI (or exit)
28-
- [ ] Migrate amisc/uqtils to this template and check the usage of `copier update`.
28+
- [x] Migrate amisc/uqtils to this template and check the usage of `copier update`.
2929
- [x] Make sure this all works in linux too (works on ubuntu at least)
3030

31+
# Agents
32+
- [ ] Look into templates for agent skills and configuration.
33+
3134
# General workflow
32-
1. User should be running `pdm test` and `pdm lint` to make sure their code works and is up to snuff.
35+
1. User should be running `dev.py test` and `dev.py lint` to make sure their code works and is up to snuff.
3336
1. `pre-commit` will lint, check for sensitive data, check for an up to date `pytest` run, etc. and check that commit message is formatted correctly. Will block if lint or tests failed. README coverage badge automatically updated after `pytest`.
3437
1. Then the user manually fixes lint/test errors. Can also use `ruff check --fix` to automatically fix lint errors. No formatting is forced at this time but would be easy with `ruff format`.
3538
1. On pull request to main, a GHA will run all the `pytest` and `ruff checks` again on multiple versions/platforms.
3639
1. On commit to main, another test-coverage is generated and read into automatic gh-pages deploy for coverage report.
37-
1. With a manual `pdm bump`, a new tag/version/release/changelog/build are generated from commit history and released. Uses `commitizen` under the hood to manage this.
40+
1. With a manual `cz bump`, a new tag/version/release/changelog/build are generated from commit history and released.

copier.yml

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ _message_before_copy: |
1212
tailored project for you.
1313
_message_after_copy: |
1414
Your project "{{ project_name }}" has been created successfully!
15-
16-
Run `pdm setup_dev` to initialize a dev environment. Run `pdm install` to add all dependencies.
1715
_message_before_update: |
1816
Thanks for updating your project using the numpy template.
1917
@@ -26,60 +24,65 @@ _message_after_update: |
2624
2725
# POST COPY TASKS (only on init)-------------------------
2826
_tasks:
29-
- command: "pdm lock"
30-
when: "{{ not is_copier_update }}"
3127
- command:
3228
- 'powershell'
3329
- '-command'
3430
- >
3531
$tempFilePath = [System.IO.Path]::GetTempFileName() + ".py";
3632
Invoke-WebRequest -Uri https://raw.githubusercontent.com/eckelsjd/copier-numpy/{{ _copier_conf.vcs_ref_hash }}/setup_github.py -OutFile $tempFilePath;
37-
pdm run python $tempFilePath;
33+
{{ python_manager }} run {% if python_manager == 'uv' %}--no-project{% endif %} python $tempFilePath;
3834
Remove-Item $tempFilePath;
39-
when: "{{ _copier_conf.os == 'windows' and init_github and not is_copier_update }}"
35+
when: "{{ _copier_conf.os == 'windows' and init_github and _copier_operation == 'copy' and (python_manager | manager_exists) }}"
4036
- command: >
4137
export temp_file=$(mktemp).py;
4238
curl -sSL https://raw.githubusercontent.com/eckelsjd/copier-numpy/{{ _copier_conf.vcs_ref_hash }}/setup_github.py --output "$temp_file";
43-
pdm run python "$temp_file";
39+
{{ python_manager }} {% if python_manager == 'uv' %}--no-project{% endif %} run python "$temp_file";
4440
rm "$temp_file";
45-
when: "{{ _copier_conf.os in ['linux', 'macos'] and init_github and not is_copier_update }}"
41+
when: "{{ _copier_conf.os in ['linux', 'macos'] and init_github and _copier_operation == 'copy' and (python_manager | manager_exists) }}"
4642
4743
# PROMPT ---------------------------------
4844
project_name:
4945
type: str
5046
help: "Enter your project name:"
51-
default: "{{ project_dir }}"
47+
default: "{{ _folder_name }}"
5248

5349
project_description:
5450
type: str
5551
help: "Enter a project description:"
5652

57-
author_name:
53+
python_manager:
54+
type: str
55+
help: "Which Python project manager do you prefer?"
56+
default: uv
57+
choices:
58+
- uv
59+
- pdm
60+
61+
user_name:
5862
type: str
5963
help: "Enter your full name:"
6064
default: "{{ git_user_name }}"
6165

62-
author_email:
66+
user_email:
6367
type: str
6468
help: "Enter your email:"
6569
default: "{{ git_user_email }}"
6670

6771
repository_namespace:
6872
type: str
6973
help: "Enter your repository namespace (e.g. your username/org on GitHub):"
70-
default: "{{ git_username }}"
74+
default: "{{ github_user }}"
7175

7276
is_distributable:
7377
type: bool
74-
help: "Do you want to build this project as a package for distribution (e.g. pip install)?"
78+
help: "Is this a Python library?"
7579
default: false
7680

7781
distribution_name:
7882
type: str
7983
help: "Your Python package distribution name (for `pip install NAME`):"
8084
default: "{{ project_name | slugify }}"
8185
when: "{{ is_distributable }}"
82-
# validator: "{% if distribution_name | pypi_name_exists %}`{{ distribution_name }}` already exists on https://pypi.org. Please choose a unique package name.{% endif %}"
8386

8487
import_name:
8588
type: str

extensions.py

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import subprocess
44
import unicodedata
55
from datetime import date
6-
from pathlib import Path
7-
import os
86
import urllib.request
9-
import urllib
7+
import urllib.error
108

119
from jinja2.ext import Extension
1210

@@ -39,23 +37,6 @@ def slugify(value, separator="-"):
3937
return re.sub(r"[-_\s]+", separator, value).strip("-_")
4038

4139

42-
def pypi_name_exists(package_name: str) -> bool:
43-
"""Check if a package name exists in PyPI."""
44-
url = f'https://pypi.org/project/{package_name}'
45-
try:
46-
urllib.request.urlopen(url)
47-
return True
48-
except urllib.error.HTTPError as e:
49-
if e.code == 404:
50-
return False
51-
else:
52-
raise
53-
54-
55-
def path_exists(path: str) -> bool:
56-
return Path(path).exists()
57-
58-
5940
def format_python_version(version: str) -> str:
6041
"""Just return the first x.x version number of a Python PEP508 version specifier"""
6142
first_version = version.split(",")[0]
@@ -69,19 +50,21 @@ def format_python_version(version: str) -> str:
6950
f"{'+' if '>' in first_version else ''}")
7051

7152

53+
def manager_exists(manager_name: str) -> bool:
54+
"""Check if a Python project manager is installed."""
55+
return shutil.which(manager_name) is not None
56+
57+
7258
class TemplateDefaultExtension(Extension):
7359
"""Provide some default answers as global variables to speed up template initialization"""
7460
def __init__(self, environment):
7561
super().__init__(environment)
7662
email = git_user_email('')
7763
environment.globals["git_user_name"] = git_user_name('')
7864
environment.globals["git_user_email"] = email
79-
environment.globals["git_username"] = email.split('@')[0]
65+
environment.globals["github_user"] = email.split('@')[0]
8066
environment.globals["current_year"] = date.today().year
81-
environment.globals["project_dir"] = Path(os.getcwd()).resolve().name
82-
environment.globals["is_copier_update"] = path_exists(".copier-answers.yml") or path_exists(".copier-answers.yaml")
83-
# runs *before* update/copy, so answers.yml will only exist when we are doing copier update
8467

85-
environment.filters["pypi_name_exists"] = pypi_name_exists
8668
environment.filters["slugify"] = slugify
8769
environment.filters["format_python_version"] = format_python_version
70+
environment.filters["manager_exists"] = manager_exists

pyproject.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,7 @@ changelog_file = "CHANGELOG.md"
2222
annotated_tag = true
2323
post_bump_hooks = ["git push --follow-tags"]
2424

25-
[tool.pdm]
26-
distribution = false
27-
28-
[tool.pdm.install]
29-
cache = true
30-
31-
[tool.pdm.scripts]
32-
bump = "cz bump {args}"
33-
34-
[tool.pdm.dev-dependencies]
25+
[dependency-groups]
3526
dev = [
3627
"commitizen>=3.29.0",
3728
"pre-commit>=3.8.0",

setup_github.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def run_command(command, capture_output=True, text=None, shell=False):
5959
else:
6060
if not shell:
6161
command = shlex.split(command)
62-
return subprocess.run(command, capture_output=capture_output, check=True, text=text, shell=shell)
62+
return subprocess.run(command, capture_output=capture_output, check=True, text=text, shell=shell, env=os.environ.copy())
6363
except subprocess.CalledProcessError as e:
6464
raise RuntimeError(f"Command `{command}` failed with code {e.returncode}: {e}\nError message: {e.stderr}") from e
6565
except subprocess.SubprocessError as e:
@@ -235,14 +235,14 @@ def initialize_git_repo_settings():
235235
# Create the gh-pages branch and link to Github pages (might fail if it already exists)
236236
run_command("git branch gh-pages")
237237
run_command("git push --set-upstream origin gh-pages") # automatically creates the GitHub pages site
238-
# run_command('gh api --method POST "/repos/{owner}/{repo}/pages" -f "source[branch]=gh-pages"')
238+
# run_command('gh api --method POST "/repos/{owner}/{repo}/pages" -f "source`[branch`]=gh-pages"')
239239
except Exception as e:
240240
print(f'Problem setting up GitHub Pages: {e}\nSkipping...')
241241

242242
# Add Github basic settings
243243
try:
244244
pyproj_values = parse_pyproject_toml(keys=["description"])
245-
topics = [f"--add-topic {topic}" for topic in ["pdm", "python", "numpy"]]
245+
topics = [f"--add-topic {topic}" for topic in ["python", "numpy"]]
246246
extra_options = ["--delete-branch-on-merge", "--enable-discussions", "--enable-issues", "--enable-wiki=false",
247247
"--enable-projects=false", "--enable-merge-commit=false", "--enable-rebase-merge",
248248
"--enable-squash-merge", "--allow-update-branch",

template/README.md.jinja

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{%- if include_docs -%}![Logo](https://raw.githubusercontent.com/{{repository_namespace}}/{{ project_name }}/main/docs/assets/logo.svg){%- else -%}
22
# {{ project_name }}{%- endif %}
3-
4-
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm-project.org)
3+
{%- if python_manager == 'pdm' %}[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm-project.org){%- endif %}
4+
{%- if python_manager == 'uv' %}[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv){%- endif %}
55
[![Python version](https://img.shields.io/badge/python-{{ python_version }}-blue.svg?logo=python&logoColor=cccccc)](https://www.python.org/downloads/)
66
[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/eckelsjd/copier-numpy)
77
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -22,17 +22,12 @@
2222

2323
{{ project_description }}
2424

25-
{% if is_distributable %} ## ⚙️ Installation
25+
{% if is_distributable %}
26+
## ⚙️ Installation
2627
```shell
2728
pip install {{ distribution_name }}
2829
```
29-
If you are using [pdm](https://github.com/pdm-project/pdm) in your own project, then you can use:
30-
```shell
31-
pdm add {{ distribution_name }}
32-
33-
# Or in editable mode from a local clone...
34-
pdm add -e ./{{ distribution_name }} --dev
35-
```{%- endif %}
30+
{% endif %}
3631

3732
## 📍 Quickstart
3833
```python
@@ -43,8 +38,10 @@ import {{ import_name }}
4338
print('Wow!')
4439
```
4540

46-
{% if is_distributable %} ## 🏗️ Contributing
47-
See the [contribution](https://github.com/{{ repository_namespace }}/{{ project_name }}/blob/main/CONTRIBUTING.md) guidelines.{%- endif %}
41+
{% if is_distributable %}
42+
## 🏗️ Contributing
43+
See the [contribution](https://github.com/{{ repository_namespace }}/{{ project_name }}/blob/main/CONTRIBUTING.md) guidelines.
44+
{% endif %}
4845

4946
## Citations
5047
Include any additional references for your project.

template/dev.py.jinja

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""A simple development script to run common tasks.
2+
3+
Available: test, bump, docs, lint, setup.
4+
Usage: `python dev.py <function_name> [args...]`
5+
"""
6+
import sys
7+
import subprocess
8+
9+
10+
def test(*args):
11+
subprocess.run(["pytest", "--cov={{ import_name }}", "--cov-report", "html:htmlcov", "tests"] + list(args))
12+
13+
14+
def bump(*args):
15+
subprocess.run(["cz", "bump"] + list(args))
16+
17+
18+
def docs(*args):
19+
subprocess.run(["mkdocs", "serve"] + list(args))
20+
21+
22+
def lint(*args):
23+
subprocess.run(["ruff", "check", "src", "tests"] + list(args))
24+
25+
26+
def setup(*args):
27+
subprocess.run(["{{ python_manager }}", "sync", "--group", "dev"])
28+
{% if init_github %}subprocess.run(["pre-commit", "install", "--allow-missing-config"]){% endif %}
29+
{% if install_jupyter %}subprocess.run(["nbstripout", "--install"]){% endif %}
30+
31+
32+
FUNCTIONS = {
33+
"test": test,
34+
"bump": bump,
35+
"docs": docs,
36+
"lint": lint,
37+
"setup": setup
38+
}
39+
40+
41+
if __name__ == "__main__":
42+
if len(sys.argv) < 2:
43+
print("Usage: python dev.py <function_name> [args...]")
44+
sys.exit(1)
45+
46+
func_name = sys.argv[1]
47+
args = sys.argv[2:]
48+
49+
if func_name not in FUNCTIONS:
50+
print(f"Error: Function '{func_name}' not found")
51+
sys.exit(1)
52+
53+
func = FUNCTIONS[func_name]
54+
func(*args)

0 commit comments

Comments
 (0)