diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 00000000..7aeb6f35 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,14 @@ +# Changes here will be overwritten by Copier; see https://copier.readthedocs.io/en/stable/configuring/#the-copier-answersyml-file +_commit: v0.1.1 +_src_path: git@github.com:JaneliaSciComp/janelia-cookiecutter.git +copyright_holder: Howard Hughes Medical Institute, Janelia Research Campus +email: malinmayorc@janelia.hhmi.org +full_name: Caroline Malin-Mayor +github_owner: funkelab +host_docs_on_github_io: true +minimum_python_version: '3.11' +project_name: funtracks +project_short_description: Cell tracking data model +project_slug: funtracks +publish_to_pypi: true +year: '2026' diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d4c6f8f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Normalize line endings: LF in the repository and on checkout everywhere, so +# that contributors on Windows do not produce whole-file diffs. +* text=auto eol=lf + +# Collapse uv.lock in diffs and keep it out of GitHub's language statistics. +# merge=binary avoids pointless 3-way merge conflicts in a generated file -- +# regenerate it with `uv lock` instead of resolving conflicts by hand. +uv.lock merge=binary linguist-language=TOML linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..0cd538b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,26 @@ +* funtracks version or commit: +* Python version: +* Operating System: + +### Bug or feature? + +Are you reporting a bug or adding a new feature? Perhaps you want more documentation? +Please tell us here and tag the issue appropriately. + +### Description + +Describe the bug you want fixed, the feature you want to see, etc. +If you are requesting a new feature, please say why you think that feature is important and belongs in this library, and any guesses on how to implement it. + +### Example + +For a bug: +``` +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` + +For a feature: +``` +Provide sample code that you would hope to run and what it would output. +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..65ad0fb0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +# Proposed Change +Briefly describe the contribution. If it resolves an issue or feature request, be sure to link to that issue. +Please also tag the PR with the appropriate labels (e.g. bugfix, feature, documentation, etc.) + +# Checklist +Go through these things before merge. Actions should run automatically to test them, but for information on how to run locally, see CONTRIBUTING.md. + +- [ ] I have added tests that prove that my feature works in various situations or tests the bugfix (if applicable). +- [ ] I have checked that the tests pass and I maintained or improved test coverage (`just test-cov`). +- [ ] I have written docstrings and checked that they render correctly in the documentation build (`just docs-build`). +- [ ] I have checked that linting and formatting pass (`just lint`). +- [ ] I have checked that mypy type checking passes (`just typecheck`). + +# Further Comments +If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yaml similarity index 94% rename from .github/workflows/test.yml rename to .github/workflows/ci.yaml index 8e6594f9..f179684f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yaml @@ -27,7 +27,7 @@ jobs: # ceiling, since that's where a dependency-resolution or OS interaction is # most likely to first appear. The intermediate versions get a single-OS # sanity check instead of full cross-OS coverage -- an OS-specific bug that - # only reproduces on 3.12 (and not 3.11 or 3.13) is possible but + # only reproduces on 3.12 or 3.13 (and not 3.11 or 3.14) is possible but # unlikely enough not to justify the extra CI cost of testing for it. include: - platform: ubuntu-latest @@ -45,11 +45,14 @@ jobs: - platform: ubuntu-latest python-version: "3.13" resolution: highest + - platform: ubuntu-latest + python-version: "3.14" + resolution: highest - platform: macos-latest - python-version: "3.13" + python-version: "3.14" resolution: highest - platform: windows-latest - python-version: "3.13" + python-version: "3.14" resolution: highest steps: @@ -94,7 +97,7 @@ jobs: - name: Set up Python uses: astral-sh/setup-uv@v7 with: - python-version: "3.13" + python-version: "3.14" enable-cache: true - name: Install dependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0b152cc6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,210 @@ +# Contributing to funtracks + +## Setting up a development environment + +This project uses [uv](https://docs.astral.sh/uv/) for dependency management and +[just](https://just.systems) as a task runner. Install both first: + +- [uv installation](https://docs.astral.sh/uv/getting-started/installation/) +- [just installation](https://just.systems/man/en/packages.html) + +`just` is only a convenience wrapper — every recipe is a one-line `uv run ...` +command you can copy out of the `justfile` and run directly. + +```bash +git clone https://github.com/funkelab/funtracks +cd funtracks +just install +``` + +`just install` creates the virtual environment (`uv sync`) and installs the +pre-commit hooks. Run `just` to see all available recipes. + +## Dependencies + +Dependencies are declared in `pyproject.toml` and have the following conventions. + +### Every dependency needs a lower and an upper bound + +Give every requirement — runtime, `test`, `docs` and `dev` alike — both bounds, +with the upper bound at the **next major version**: + +```toml +dependencies = [ + "numpy>=1.26,<3", # yes + "numpy>=1.26", # no: unbounded above + "numpy", # no: unbounded in both directions + "numpy==2.3.1", # no: that is the lock file's job +] +``` + +Note that a bound spans majors whenever the older ones still work — `<3` is the +next major above the *newest* release you support, not a companion to the floor. + +The upper bound excludes the next major release, since under semantic versioning +that is where the project is permitted to break you. + +- **Without an upper bound**, a major release upstream reaches users through a + fresh resolve of `pyproject.toml` — a new contributor's `uv sync`, or anyone + installing this package as a dependency — and breaks them before anyone here + has run the tests against it. +- **Without a lower bound**, the resolver is free to pick something ancient to + satisfy an unrelated conflict, and the failure surfaces as a missing attribute + rather than a resolution error. +- **Pinning exactly** (`==`) belongs in `uv.lock`, not here. Exact pins in + `pyproject.toml` make this package unusable alongside anything with a slightly + different pin. + +#### Choosing the lower bound + +The lower bound is the **oldest version that still works**, and how hard you +should work to keep it low depends on who has to live with it. + +For **runtime `dependencies`**, keep the floor as low as you can stand. These are +installed into someone else's environment, next to other packages with their own +constraints on the same libraries. A floor of "whatever was current the day I +typed `uv add`" is an arbitrary co-installation hazard: it excludes anyone whose +environment is held slightly behind by an unrelated package, and the resolver +reports that as an unsatisfiable conflict rather than anything actionable. Set it +to the oldest release providing the APIs you actually use — typically the version +that introduced the newest function or argument you call — not to today's +release. + +For **`test`, `docs` and `dev` groups**, the floor costs nothing. Nothing else +shares that environment; it is created from this project's own lock file. Use the +current release when you add the tool and move on. + +The upper bound rule is the same everywhere. + +For pre-1.0 dependencies (`0.x`), a minor bump plays the role of a major, so the +upper bound goes at the next minor: `ruff>=0.16,<0.17`. + +Widening an upper bound is a deliberate, reviewed change: edit the constraint, +re-lock, and let CI tell you whether the new major actually breaks anything. The +`Outdated dependencies` workflow described below exists to surface the releases +that these bounds are holding back. + +Because versions are pinned, new upstream releases are *not* picked up +automatically. Upgrading is a deliberate step: + +```bash +just upgrade # upgrade everything within the declared constraints +just upgrade-package numpy # upgrade a single dependency +``` + +Commit the resulting `uv.lock` alongside the change. Outside of these commands +the lock file stays put — routine `just test` / `uv run` only read it. + +These commands only move versions *within* the constraints in `pyproject.toml`; +they never rewrite the constraints themselves. A dependency declared as +`foo>=1,<2` will stay on 1.x however often you upgrade. Moving to 2.x means +editing the bound in `pyproject.toml` by hand, then re-locking — which is the +point at which you find out whether the new major breaks anything. + +Dependabot opens a monthly pull request bumping `uv.lock`, and CI runs the full +test matrix against the new versions, so most upgrades arrive as a PR you only +need to review and merge. + +Releases that fall *outside* the declared bounds never reach those PRs, so a +monthly `Outdated dependencies` workflow lists them in a GitHub issue instead. +It only reports; acting on it is the manual bound-widening step above. + +## Module layout + +Implementation modules are **underscore-prefixed** (`_example.py`, `_io.py`), and +everything users are meant to touch is re-exported from `__init__.py` and listed +in `__all__`: + +``` +src/funtracks/ + __init__.py # from ._example import greet; __all__ = [..., "greet"] + _example.py # implementation +``` + +```python +from funtracks import greet # yes +from funtracks._example import greet # works, but not the supported path +``` + +This is mainly about the generated API docs. `mkdocs-api-autonav` builds a page +per public module, and `mkdocstrings` is configured with `filters: ["!^_"]`, so +a public `example.py` alongside a re-export in `__init__.py` documents `greet` +twice — once on its own module page, once on the package page — and modules with +nothing public left to show still get an empty nav entry. Underscore-prefixing +the module removes it from the nav, leaving one page generated from `__all__`. + +The convention pays off twice over: + +- **The public API is a decision, not a side effect of the file layout.** Adding + a name to `__all__` is deliberate; splitting `_run.py` into `_run.py` and + `_dtype.py` is then a refactor users never see. +- **Docs match the import path you actually support.** Anything documented is + importable from the top-level package. + +Nothing enforces this mechanically — if a package genuinely warrants a public +submodule (a `cli`, or a heavy optional subpackage users import directly), name +it without the underscore and let it have its own docs page. + +## Tests, linting and type checking + +```bash +just test # run the test suite +just test-cov # run the test suite with a coverage report +just lint # run ruff (lint + format) and the other pre-commit hooks +just typecheck # run mypy via pre-commit +``` + +Linting, formatting and type checking are all handled via +[pre-commit](https://pre-commit.com/) hooks (ruff for lint/format, mypy for type +checking), so most style issues are fixed automatically when you commit, and +mypy always runs with the same stub/type-dependency setup declared in +`.pre-commit-config.yaml` whether you run it locally or it runs in CI. + +## Documentation + +Documentation is built with [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) +and API pages are generated from docstrings (Google style). + +```bash +just docs-serve # preview the docs locally with live reload +just docs-build # build the docs in strict mode, as CI does +``` + +We use mike to get versioned documentation. Mike pushes changes to the +`gh-pages` branch, which is served from the GitHub Pages settings. This +happens automatically via CI on push to `main` (alias `dev`) and on a newly +tagged version (alias `latest`), but is documented here in case it needs to be +done manually: + +```bash +just docs-deploy # or: uv run mike deploy +just docs-serve # preview pushed changes: uv run mike serve +``` + +Remember not to push your local `gh-pages` branch by accident after previewing +changes. To reset your local branch to match the remote: + +```bash +git fetch +git checkout gh-pages +git reset --hard origin/gh-pages +``` + +## Pull requests + +CI runs the test suite across Linux, macOS and Windows +and builds the docs. Type checking and linting run via pre-commit +(locally on commit, and in CI through pre-commit.ci) rather than as a separate +CI job. Please make sure `just test`, `just lint` and `just typecheck` pass +locally before opening a pull request. + +## Releasing + +Releases are triggered by pushing a version tag. `setuptools-scm` derives the +version from the tag, and CI builds and publishes to PyPI. + +```bash +just release v0.1.0 +``` +Publishing uses [trusted publishing](https://docs.pypi.org/trusted-publishers/), +which must be configured once on PyPI for this project. diff --git a/DEVELOPER.md b/DEVELOPER.md deleted file mode 100644 index fbd75fd0..00000000 --- a/DEVELOPER.md +++ /dev/null @@ -1,51 +0,0 @@ -# Developer Guide - -## Managing dependencies with `uv` -This repo has been set up to use `uv` for developer dependency management. -Install `uv` according to [their instructions](https://docs.astral.sh/uv/getting-started/installation/). - -Dev dependencies are specified in the `dependency-groups` section of the `pyproject.toml`. -The `dev` group is installed by default with `uv`, so running `uv run ...` should allow -you to use all the developer tools specified in this section. - -Dependency groups are different from the extra dependencies specified in -`project.optional-dependencies`, so they cannot be installed with `pip install .[dev]` -and are not packaged and distributed with the library. - -## Running tasks with `just` -For convenience, common development tasks like building docs or running test with coverage -can be run with `just`. Install `just` according to [their instructions](https://just.systems/man/en/packages.html). - -Tasks are defined in the `justfile`. These tasks are just shorthand for common commands that -you would otherwise type into your shell over and over. - -## Updating and previewing documentation -We are using mkdocs-material and mike to get versioned documentation. Mike will -push changes to the gh-pages branch, which we can serve from the Github Pages settings. -This should happen automatically from the github action upon push to 'main' (with alias 'dev') and newly tagged version (with alias 'latest'), -but we are documenting it here in case we need to do it manually at some point. - -To publish a commit to the gh-pages branch manually: -```bash -uv run mike deploy -``` -or with just tasks: -```bash -just docs-deploy -``` - -To preview pushed changes locally: -```bash -uv run mike serve -``` -or with just tasks: -```bash -just docs-serve -``` -Remember not to push your local gh-pages branch on accident after previewing changes. -To reset your local branch to the remote gh-pages, you can run: -```bash -git fetch -git checkout gh-pages -git reset --hard origin/gh-pages -``` diff --git a/pyproject.toml b/pyproject.toml index 4a940455..832813e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Image Processing", ]