diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..42c5394a --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../skills \ No newline at end of file diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 00000000..fa7c1d5c --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,35 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "ridgeplot" + +[setup] +script = ''' +if git remote | grep -q '^origin$'; then + echo "info: git remote 'origin' already exists" +else + git remote add origin https://github.com/tpvasconcelos/ridgeplot.git +fi + +if git rev-parse --is-shallow-repository 2>/dev/null | grep -q true; then + git fetch --unshallow +else + echo "info: git repository is already complete (not shallow)" +fi + +git fetch -v +''' + +[[actions]] +name = "source venv" +icon = "tool" +command = "source .venv/bin/activate" + +[[actions]] +name = "tox -m tests" +icon = "test" +command = "uvx tox -m tests" + +[[actions]] +name = "tox -m static-quick" +icon = "test" +command = "uvx tox -m static-quick" diff --git a/.codex/skills b/.codex/skills new file mode 120000 index 00000000..42c5394a --- /dev/null +++ b/.codex/skills @@ -0,0 +1 @@ +../skills \ No newline at end of file diff --git a/.cursor/skills b/.cursor/skills new file mode 120000 index 00000000..42c5394a --- /dev/null +++ b/.cursor/skills @@ -0,0 +1 @@ +../skills \ No newline at end of file diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json index 810d8c0d..e7ab0a01 100644 --- a/.cursor/worktrees.json +++ b/.cursor/worktrees.json @@ -1,5 +1,6 @@ { "setup-worktree": [ - "make init" + "make init", + "source .venv/bin/activate" ] } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0c5010e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,221 @@ +# ridgeplot Development Guide for AI Agents + +`ridgeplot` is a Python package for beautiful, interactive ridgeline plots built on Plotly. + +## Start Here + +- Read this file first, then the most relevant source file(s). +- Prefer local docs and tests over memory; do not guess behavior. +- If requirements are unclear or risky, ask a concrete question before changing code. + +## Stack and Style Constraints + +- Python >=3.10 +- Plotly graph objects (dependency is `plotly>=5.20`) +- Line length 100, formatting with ruff +- Docstrings are NumPy style +- Type annotations are modern and throughout with strict checking via pyright + +## Quick Commands + +### Environment Setup + +```shell +# Initialize full development environment (recommended for first-time setup) +# This creates .venv, installs dependencies, and sets up pre-commit hooks +make init + +# Activate the virtual environment +source .venv/bin/activate +``` + +### Running Tests + +```shell +# Run all test suites (unit + e2e + cicd_utils) +uvx tox -m tests + +# Run a specific test suite +uvx tox -e tests-unit # Unit tests with coverage +uvx tox -e tests-e2e # End-to-end tests +uvx tox -e tests-cicd_utils # CI/CD utilities tests + +# Run pytest directly with custom options +uvx tox -e pytest -- tests/unit/test_init.py --no-cov +uvx tox -e pytest -- -k "test_specific_function" --no-cov +``` + +### Linting and Formatting + +```shell +# Run the main static checks +uvx tox -m static-quick + +# Run the entire suite of static checks (incl. all pre-commit hooks) +# If running from the main branch, you'll need +# to skip the 'no-commit-to-branch' check with: +SKIP='no-commit-to-branch' uvx tox -m static + +# Run all pre-commit hooks on all files +uvx pre-commit run --all-files + +# Run specific pre-commit hooks +uvx pre-commit run ruff-format --all-files + +# Run type checking with pyright only +uvx tox -e typing +``` + +### Documentation + +```shell +# Build static documentation +uvx tox -e docs-static +``` + +## Project Map + +```text +src/ridgeplot/ +├── __init__.py # Public API exports +├── _ridgeplot.py # Main ridgeplot() function +├── _figure_factory.py # Plotly Figure construction +├── _kde.py # Kernel Density Estimation +├── _hist.py # Histogram binning +├── _types.py # Type aliases and type guards +├── _utils.py # Utility functions +├── _missing.py # Sentinel for missing values +├── _version.py # Version string (setuptools-scm) +├── _color/ # Color handling +│ ├── colorscale.py # Colorscale resolution +│ ├── css_colors.py # CSS color parsing +│ ├── interpolation.py # Color interpolation +│ └── utils.py # Color utilities +├── _obj/traces/ # Trace objects +│ ├── area.py # Area trace (filled curves) +│ ├── bar.py # Bar trace (histograms) +│ └── base.py # Base trace class +├── _vendor/ # Vendored dependencies +└── datasets/ # Built-in datasets + └── data/ # CSV data files + +tests/ +├── conftest.py # Shared pytest fixtures +├── unit/ # Unit tests for individual modules +├── e2e/ # End-to-end tests with expected outputs +│ └── artifacts/ # JSON artifacts for e2e comparisons +└── cicd_utils/ # Tests for CI/CD utilities + +cicd_utils/ # CI/CD helper modules +├── cicd/ # Scripts and test helpers +└── ridgeplot_examples/ # Example implementations for docs/testing +``` + +## Public API + +The public API exposes one main function: `ridgeplot.ridgeplot(...) -> go.Figure` + +The `ridgeplot()` docstring in `src/ridgeplot/_ridgeplot.py` contains the API contract. + +`ridgeplot.datasets.load_probly()` and + `ridgeplot.datasets.load_lincoln_weather()` are legacy loaders kept for + backwards compatibility only. + +## Key Data Flow + +1. Users call `ridgeplot(samples=...)` or `ridgeplot(densities=...)`. +2. If samples are provided, KDE in `_kde.py` or histogram binning in `_hist.py` + produces densities. +3. Densities are normalised if the `norm` parameter is set. +4. `create_ridgeplot()` in `_figure_factory.py` builds the Plotly Figure by + resolving colors, creating traces, and applying layout settings. +5. The function returns a `plotly.graph_objects.Figure`. + +## Key Files to Know + +| File | Purpose | +|------------------------------------|-----------------------------| +| `src/ridgeplot/_ridgeplot.py` | Main `ridgeplot()` function | +| `src/ridgeplot/_figure_factory.py` | Figure construction logic | +| `src/ridgeplot/_types.py` | All type aliases and guards | +| `tests/unit/test_ridgeplot.py` | Core function tests | +| `cicd_utils/ridgeplot_examples/` | Example scripts for docs | +| `docs/` | User and developer docs | +| `tox.ini` | CI environment definitions | +| `ruff.toml` | Linting configuration | + +Documentation: https://ridgeplot.readthedocs.io/en/stable/ + +## Workflow Expectations + +### When Adding New Features + +1. Start with `src/ridgeplot/_ridgeplot.py` to understand the entry point. +2. Add parameters following existing patterns and deprecation handling. +3. Update types in `src/ridgeplot/_types.py` when introducing new structures. +4. Add tests in `tests/unit/` with good coverage. +5. Update docstrings to match the new behavior. + +### When Fixing Bugs + +1. Write a failing test first. +2. Fix the issue with minimal changes. +3. Ensure tests and type checks pass. +4. Verify docstrings remain accurate. + +### Type Annotations + +- Uses **pyright** in strict mode (see `pyrightconfig.json`) +- All functions must be fully typed, following modern Python typing practices and existing project conventions. +- Use `TYPE_CHECKING` blocks for import-only types +- Use type guards for runtime type narrowing (see `_types.py`) +- Follow this general principle for user-facing code: be contravariant in the input type and covariant in the output type +- Add type aliases close to their usage. If widely used, place in `_types.py`. + +## Common Patterns + +**Handling deprecated parameters:** + +```python +if deprecated_param is not MISSING: + if new_param is not None: + raise ValueError("Cannot use both...") + warnings.warn("...", DeprecationWarning, stacklevel=2) + new_param = deprecated_param +``` + +**Type narrowing with guards:** + +```python +if is_shallow_samples(samples): + samples = nest_shallow_collection(samples) +samples = cast("Samples", samples) +``` + +**Lazy imports for performance:** + +```python +# Heavy imports inside functions to reduce import time +def _coerce_to_densities(...): + from ridgeplot._kde import estimate_densities # statsmodels is slow to import +``` + +## Testing Notes + +- Use `--no-cov` flag during development for faster test runs +- Run targeted tests via: `uvx tox -e pytest -- tests/unit/test_foo.py -k "test_bar" --no-cov` +- The `tests/e2e/artifacts/` directory contains expected Plotly Figure JSON for e2e tests + +## CI/CD Pipeline + +GitHub Actions runs tests on Python 3.10–3.14 across Ubuntu, macOS, and Windows. +Codecov minimums are 98% overall and 100% diff coverage for new code. + +## Notes for AI Assistants + +1. Run tests after changes when feasible, starting with the smallest relevant subset. +2. Run `uvx tox -e typing` if types are touched or errors are likely. +3. Run `uvx pre-commit run ruff-format --all-files` to format code. +4. Preserve deprecation behavior and public API stability. +5. Keep changes minimal and aligned with existing patterns. +6. Respect existing patterns - this is a mature codebase with consistent style. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 3706215c..37d01270 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -33,11 +33,14 @@ prune docs/api/autogen prune docs/api/public # Misc -recursive-include .cursor *.json +recursive-include .claude *.md +recursive-include .codex *.toml *.md +recursive-include .cursor *.json *.md recursive-include .github *.yml *.yaml recursive-include cicd_utils README.md *.py *.sh recursive-include misc *.py *.ipynb *.txt *.png recursive-include requirements *.txt +recursive-include skills *.md recursive-include tests *.py recursive-include tests/e2e/artifacts *.json diff --git a/Makefile b/Makefile index 4f867ed6..fff82b13 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ VENV_BIN := $(VENV_PATH)/bin OFFLINE ?= 0 ifeq ($(OFFLINE), 1) _UV_OFFLINE_ARG = --offline - else _UV_OFFLINE_ARG = endif @@ -58,7 +57,7 @@ _check-sys: ## Check system requirements fi -$(VENV_PATH): _check-sys ## create a virtual environment +$(VENV_PATH): | _check-sys ## create a virtual environment @echo "==> Creating local virtual environment under: $(VENV_PATH)/ ($(BASE_PYTHON))" @uv venv $(_UV_OFFLINE_ARG) --python="$(BASE_PYTHON)" --seed "$(VENV_PATH)" @@ -78,7 +77,7 @@ install: $(VENV_PATH) ## install all local development dependencies .PHONY: jupyter-init jupyter-init: install ## initialise a jupyter environment @echo "==> Setting up jupyterlab environment..." - @$(VENV_BIN)/uv pip install --upgrade ipykernel jupyter + @uv pip install --upgrade ipykernel jupyter @$(VENV_BIN)/python -m ipykernel install --user --name='ridgeplot' --display-name='ridgeplot' diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index 2bd31788..32a23ef6 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -18,6 +18,8 @@ Unreleased changes ### Developer Experience +- Add AGENTS.md, CLAUDE.md, .codex/, and .cursor/ helper files for AI-assisted development ({gh-pr}`367`) +- Small improvements to the project's Makefile ({gh-pr}`367`) - Add a basic version of Cursor's `worktrees.json` config ({gh-pr}`364`) ### CI/CD diff --git a/pyproject.toml b/pyproject.toml index 7264cd54..75c5acbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,3 +66,13 @@ namespaces = false # and we want to push X.devM to TestPyPi # on every merge to the `main` branch local_scheme = "no-local-version" + +[tool.check-manifest] +ignore = [ + ".claude/skills", + ".claude/skills/**", + ".codex/skills", + ".codex/skills/**", + ".cursor/skills", + ".cursor/skills/**", +] diff --git a/skills/dropping-and-adding-support-for-python-versions/SKILL.md b/skills/dropping-and-adding-support-for-python-versions/SKILL.md new file mode 100644 index 00000000..136402d4 --- /dev/null +++ b/skills/dropping-and-adding-support-for-python-versions/SKILL.md @@ -0,0 +1,73 @@ +--- +name: dropping-and-adding-support-for-python-versions +description: "Keeps supported Python versions aligned across CI, configs, and docs. Use when adding a new Python version or dropping an end-of-life version per the official Python support policy." +--- + +# Dropping and adding support for Python versions + +## When to use + +- The user asks to add support for a specific Python version. +- The user asks to drop or deprecate an end-of-life Python version. +- The user asks to sync supported versions with the official [Python support policy](https://devguide.python.org/versions/). + +## Required inputs + +- Change type: add or drop a specific version, or asked to sync with the official support policy. +- Exact Python version string (e.g., 3.13), if applicable. + +If any input is missing, ask before editing. + +## Workflow + +1. Confirm that you have the required inputs. +2. Apply the file updates for the change type. +3. Add an entry to the [changelog](docs/reference/changelog.md) as specified below. +4. Search for other mentions of the target version and update only those that describe supported versions. +5. Run the entire test suite to ensure no regressions. +6. Run all linters and type checks to ensure consistency. +7. Report the changes made, tests run, and any follow-ups needed. + +## Adding support for a new Python version + +- .github/workflows/ci.yml: `jobs.software-tests.strategy.matrix.python-version` +- pyproject.toml: `project.classifiers` + +Finally, update the changelog at the top of "Unreleased changes" using the following template: + +> Add support for Python 3.XX, in accordance with the official Python support policy[^1] ({gh-pr}`XXX`). + +## Dropping support for an end-of-life Python version + +- .github/workflows/ci.yml: `jobs.software-tests.strategy.matrix.python-version` and `jobs.static-checks.steps.with.python-version` +- .github/workflows/release.yml: `jobs.build.steps.with.python-version` and `jobs.github-release.steps.with.python-version` +- .pre-commit-config.yaml: `default_language_version.python` +- .readthedocs.yml: `build.tools.python` +- AGENTS.md: wherever relevant +- docs/development/contributing.md: wherever relevant +- Makefile: `BASE_PYTHON` +- mypy.ini: `python_version` +- pyproject.toml: `project.classifiers`, `project.requires-python` +- pyrightconfig.json: `pythonVersion` +- ruff.toml: `target-version` + +Finally, update the changelog under "Unreleased changes" -> "Breaking changes" using the following template: + +> Dropped support for Python 3.XX, in accordance with the official Python support policy[^1] ({gh-pr}`XXX`). + +## Validation checks + +- CI matrix, classifiers, and `requires-python` are consistent. +- Tooling configs reflect the new minimum version when dropping support. +- Docs mention the updated supported versions where appropriate. + +## Out of scope + +- Do not change dependency versions or code behavior unless the user asks. +- Do not update unrelated CI jobs or tooling settings. + +## Output + +- Summarize changes and list updated files. +- Note any tests run and follow-ups needed. +- If you encountered any discrepancies between these instructions and the locations in the codebase where supported Python versions are mentioned and need to be updated, please document them here for future reference.