Skip to content

Commit 08c3667

Browse files
committed
feat: give new projects a typed, testable core from day one
A template only helps if the first commit already behaves like a real package. This adds a modern pyproject-based package layout, small robotics/ML-oriented primitives, example entrypoints, and regression tests so downstream work starts from executable structure instead of an empty repo. Constraint: The base template must stay light enough for generic Python use while still feeling robotics/ML-ready Rejected: Start with a notebook-first scaffold | weak packaging and test discipline for long-lived projects Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep the core package importable without optional extras; push heavyweight stacks to extras and adapters Tested: uv lock; uv sync --group dev; make check; make build; make run-example-ml; make run-example-robotics; make audit Not-tested: Real ROS 2 integration and GPU framework installation paths
1 parent 64fb25a commit 08c3667

16 files changed

Lines changed: 349 additions & 0 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Yujeong
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

docs/ml.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ML profile
2+
3+
Use the ML profile when you want to keep the repository package-first while still supporting
4+
training, evaluation, and experiment artifacts.
5+
6+
## Suggested conventions
7+
8+
- Keep notebooks exploratory; keep source of truth in `src/`, `examples/`, and `scripts/`.
9+
- Store datasets outside the repo and track references/configs in code.
10+
- Log experiment outputs into `artifacts/` or your external experiment tracker.
11+
- Install ML extras with `uv sync --group dev --extra ml`.

docs/robotics.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Robotics profile
2+
3+
Use the robotics profile when you want the core package to stay pure Python while enabling ROS 2,
4+
simulation, or hardware-specific integrations as optional layers.
5+
6+
## Suggested conventions
7+
8+
- Treat `src/` as the portable core and place ROS 2 bindings/adapters behind optional modules.
9+
- Keep control-safe interfaces typed and small.
10+
- Use `.devcontainer/` when teammates need a reproducible Linux environment.
11+
- Install robotics extras with `uv sync --group dev --extra robotics`.

examples/ml/train_stub.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
from python_template.ml import seed_everything, summarize_metrics
4+
5+
6+
def main() -> None:
7+
seed_everything(42)
8+
losses = [0.91, 0.63, 0.42, 0.28]
9+
summary = summarize_metrics(losses)
10+
print("ML training stub summary")
11+
for name, value in summary.items():
12+
print(f"- {name}: {value:.4f}")
13+
14+
15+
if __name__ == "__main__":
16+
main()

examples/robotics/pipeline_stub.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from python_template.robotics import CommandFrame, SensorFrame, run_control_loop
4+
5+
6+
def proportional_controller(frame: SensorFrame) -> CommandFrame:
7+
heading_error = frame.values["heading_error"]
8+
return CommandFrame(linear_x=0.3, angular_z=-0.8 * heading_error)
9+
10+
11+
def main() -> None:
12+
frames = [
13+
SensorFrame(timestamp_s=0.0, values={"heading_error": 0.4}),
14+
SensorFrame(timestamp_s=0.1, values={"heading_error": 0.1}),
15+
SensorFrame(timestamp_s=0.2, values={"heading_error": -0.2}),
16+
]
17+
commands = run_control_loop(frames, proportional_controller)
18+
print("Robotics pipeline stub")
19+
for index, command in enumerate(commands, start=1):
20+
print(f"- tick {index}: vx={command.linear_x:.2f}, wz={command.angular_z:.2f}")
21+
22+
23+
if __name__ == "__main__":
24+
main()

pyproject.toml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
[build-system]
2+
requires = ["hatchling>=1.27.0"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "python-template"
7+
dynamic = ["version"]
8+
description = "A modern Python template for robotics and ML projects."
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = { text = "MIT" }
12+
authors = [
13+
{ name = "Yujeong" },
14+
]
15+
keywords = ["python", "template", "ml", "robotics", "uv", "github-actions"]
16+
classifiers = [
17+
"Development Status :: 3 - Alpha",
18+
"Intended Audience :: Developers",
19+
"License :: OSI Approved :: MIT License",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
24+
"Topic :: Software Development :: Build Tools",
25+
]
26+
dependencies = []
27+
28+
[project.optional-dependencies]
29+
ml = [
30+
"numpy>=2.3.0",
31+
"pandas>=2.3.0",
32+
"scikit-learn>=1.7.0",
33+
]
34+
robotics = [
35+
"numpy>=2.3.0",
36+
"pyyaml>=6.0.2",
37+
"typing-extensions>=4.14.0; python_version < '3.13'",
38+
]
39+
viz = [
40+
"matplotlib>=3.10.0",
41+
"rich>=14.0.0",
42+
]
43+
44+
[project.urls]
45+
Homepage = "https://github.com/techhouse-looper/python-template"
46+
Repository = "https://github.com/techhouse-looper/python-template"
47+
Issues = "https://github.com/techhouse-looper/python-template/issues"
48+
49+
[dependency-groups]
50+
dev = [
51+
"build>=1.3.0",
52+
"pip-audit>=2.9.0",
53+
"pre-commit>=4.3.0",
54+
"pyright>=1.1.405",
55+
"pytest>=8.4.0",
56+
"pytest-cov>=7.0.0",
57+
"ruff>=0.12.0",
58+
]
59+
60+
[tool.hatch.version]
61+
path = "src/python_template/__about__.py"
62+
63+
[tool.hatch.build.targets.wheel]
64+
packages = ["src/python_template"]
65+
66+
[tool.pytest.ini_options]
67+
addopts = "-ra --strict-config --strict-markers"
68+
testpaths = ["tests"]
69+
70+
[tool.ruff]
71+
line-length = 100
72+
target-version = "py311"
73+
74+
[tool.ruff.lint]
75+
select = ["E", "F", "I", "B", "UP", "SIM", "C4", "PIE"]
76+
77+
[tool.ruff.format]
78+
docstring-code-format = true
79+
80+
[tool.pyright]
81+
pythonVersion = "3.11"
82+
include = ["src", "tests", "examples"]
83+
typeCheckingMode = "strict"
84+
venvPath = "."
85+
venv = ".venv"
86+
87+
[tool.coverage.run]
88+
source = ["python_template"]
89+
branch = true
90+
91+
[tool.coverage.report]
92+
show_missing = true
93+
skip_covered = true

src/python_template/__about__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

src/python_template/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Reusable primitives for the python-template project scaffold."""
2+
3+
from python_template.__about__ import __version__
4+
from python_template.config import ProjectProfile
5+
from python_template.ml import seed_everything, summarize_metrics
6+
from python_template.robotics import CommandFrame, SensorFrame, run_control_loop
7+
8+
__all__ = [
9+
"CommandFrame",
10+
"ProjectProfile",
11+
"SensorFrame",
12+
"__version__",
13+
"run_control_loop",
14+
"seed_everything",
15+
"summarize_metrics",
16+
]

src/python_template/__main__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from python_template import __version__
2+
from python_template.config import ProjectProfile
3+
4+
5+
def main() -> None:
6+
profile = ProjectProfile(name="python-template")
7+
print(f"python-template {__version__}")
8+
print("Default tasks:")
9+
for task in profile.default_tasks:
10+
print(f"- {task}")
11+
12+
13+
if __name__ == "__main__":
14+
main()

src/python_template/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
5+
6+
@dataclass(frozen=True, slots=True)
7+
class ProjectProfile:
8+
"""Minimal typed metadata for a robotics/ML project scaffold."""
9+
10+
name: str
11+
python: str = "3.11+"
12+
default_tasks: tuple[str, ...] = field(
13+
default_factory=lambda: ("make sync-dev", "make check", "make build"),
14+
)
15+
optional_extras: tuple[str, ...] = field(
16+
default_factory=lambda: ("ml", "robotics", "viz"),
17+
)

0 commit comments

Comments
 (0)