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
24 changes: 4 additions & 20 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
# Copilot Instructions for Analysis Template
# Copilot Instructions

## Project context
Canonical repo guidance lives in `AGENTS.md` — layout, paths, environments and commands.
`README.md` is the user-facing overview.

See the project README for details about the project goal, datasets, and structure.

## Quick reference

| Task | Command |
|------|---------|
| Run Python | `pixi run python script.py` |
| Run tests | `pixi run test` |
| Add conda package | `pixi add <package>` |
| Add PyPI package | `pixi add --pypi <package>` |

## Project structure
- **Notebooks**: `analysis/[INITIALS]-[YYYY]-[MM]-[DD]_description.ipynb`
- **Data**: `data/<dataset>/{raw,processed,resources,results}/`
- **Paths**: Use `from <package> import FilePaths` (edit `_constants.py` for datasets)
- **Deps**: All in `pixi.toml` (not pyproject.toml)
- pyproject.toml exists mainly for package metadata and testing
- Run `pixi install` after pulling changes that update `pixi.toml`
If this file conflicts with them, they win.
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# AGENTS.md — working conventions

This file owns the working conventions and is the canonical guidance for any coding agent.
`README.md` is the user-facing overview; anything documented there is referenced from here,
never restated.

## Layout

- **Notebooks**: `analysis/[INITIALS]-[YYYY]-[MM]-[DD]_description.ipynb`
- **Data**: `data/<dataset>/{raw,processed,resources,results}/`, gitignored
- **Package**: `src/<package>/`, installed editable from the checkout

## Paths

Never hardcode a path into `data/` or `figures/` — every path hangs off `FilePaths`:

```python
from myanalysis import FilePaths

FilePaths.DATA # data/
FilePaths.FIGURES # figures/ — curated output: talk and paper figures
FilePaths.EXAMPLE_DATASET / "processed" / "adata.h5ad"
```

`FilePaths.ROOT` is resolved from git, so it names the *main* checkout even when called from a
worktree and shared data does not follow your branch. Add a dataset as a constant in
`_constants.py`; each one keeps the `{raw,processed,resources,results}` layout by convention.

## Environments

Dependencies live in `pixi.toml`, not `pyproject.toml` — the latter carries package metadata and
the test config. Run `pixi install` after pulling a change to `pixi.toml`, in the main checkout.

| Task | Command |
| --- | --- |
| Run Python | `pixi run python script.py` |
| Run tests | `pixi run test` |
| Add conda package | `pixi add <package>` |
| Add PyPI package | `pixi add --pypi <package>` |
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Analysis Template Agent Entry Point

@AGENTS.md
4 changes: 2 additions & 2 deletions src/myanalysis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from importlib.metadata import version

from ._constants import DatasetPaths, FilePaths
from ._constants import FilePaths
from .plotting import qc_violin

__all__ = ["DatasetPaths", "FilePaths", "qc_violin"]
__all__ = ["FilePaths", "qc_violin"]
__version__ = version("myanalysis")
84 changes: 24 additions & 60 deletions src/myanalysis/_constants.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,41 @@
"""Project-wide path constants for notebooks and scripts."""
"""Project-wide paths for notebooks and scripts."""

from __future__ import annotations

from dataclasses import dataclass
import subprocess
from functools import lru_cache
from pathlib import Path

# Files that mark the repository root, searched for upward from this module.
_ROOT_MARKERS = ("pixi.toml", ".git")

@lru_cache(maxsize=1)
def _repo_root() -> Path:
"""The repository root, from git so a worktree still names the main checkout.

def _find_root(start: Path) -> Path:
"""Locate the repo root by walking upward until a marker file is found.

Falls back to the fixed ``src/<package>/`` layout (three levels up) when no
marker is present, e.g. for a non-editable installed copy.
Falls back to walking up for ``pixi.toml``/``.git`` outside a repository.
"""
for parent in (start, *start.parents):
if any((parent / marker).exists() for marker in _ROOT_MARKERS):
return parent
return start.parents[2]


@dataclass(frozen=True)
class DatasetPaths:
"""Standard subfolders for a single dataset (``data/<name>/``)."""

root: Path

@property
def raw(self) -> Path:
"""Original, unmodified input data."""
return self.root / "raw"

@property
def processed(self) -> Path:
"""Preprocessed / intermediate data."""
return self.root / "processed"

@property
def resources(self) -> Path:
"""Reference data, gene sets, annotations."""
return self.root / "resources"

@property
def results(self) -> Path:
"""Analysis outputs (tables, exported objects)."""
return self.root / "results"

def create(self) -> DatasetPaths:
"""Create all standard subfolders (idempotent). Returns ``self``."""
for path in (self.raw, self.processed, self.resources, self.results):
path.mkdir(parents=True, exist_ok=True)
return self
try:
git_dir = subprocess.run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
cwd=Path(__file__).resolve().parent,
capture_output=True,
text=True,
check=True,
).stdout.strip()
return Path(git_dir).parent
except (subprocess.CalledProcessError, OSError):
here = Path(__file__).resolve()
for parent in (here, *here.parents):
if (parent / "pixi.toml").exists() or (parent / ".git").exists():
return parent
return here.parents[2]


class FilePaths:
"""Project-wide paths for notebooks and scripts."""
"""Project-wide paths. Add a dataset as a constant here; never hardcode one."""

ROOT = _find_root(Path(__file__).resolve())
ROOT = _repo_root()

DATA = ROOT / "data"
FIGURES = ROOT / "figures"

# The bundled example dataset; customize / add your own via `dataset()`.
EXAMPLE_DATASET = DATA / "example_dataset"

@classmethod
def dataset(cls, name: str) -> DatasetPaths:
"""Return the standard raw/processed/resources/results paths for a dataset.

Examples
--------
>>> paths = FilePaths.dataset("pbmc3k").create()
>>> paths.processed / "adata.h5ad" # doctest: +SKIP
"""
return DatasetPaths(cls.DATA / name)
6 changes: 3 additions & 3 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ def test_package_has_version():


def test_filepaths():
"""Project root resolves, and the dataset accessor builds standard subpaths."""
"""The root resolves to the checkout, and dataset constants hang off it."""
assert (FilePaths.ROOT / "pixi.toml").exists()
ds = FilePaths.dataset("example_dataset")
assert ds.raw == FilePaths.DATA / "example_dataset" / "raw"
assert FilePaths.DATA == FilePaths.ROOT / "data"
assert FilePaths.EXAMPLE_DATASET == FilePaths.DATA / "example_dataset"


def test_qc_violin():
Expand Down
Loading