Skip to content
Open
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
14 changes: 14 additions & 0 deletions .agents/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This document defines the mandatory testing standards and patterns for the `ruff
## 3. Best Practices and Patterns

### 3.1 Use Pytest Fixtures

Avoid re-defining common TOML strings or setup logic in every test function. Use fixtures to provide consistent test data.

```python
Expand All @@ -34,6 +35,7 @@ lint.select = ["F", "E"]
```

### 3.2 Parameterization

Use `@pytest.mark.parametrize` to test the same logic against multiple scenarios. Use `pytest.param(..., id="case_name")` to ensure test reports are readable.

```python
Expand All @@ -49,9 +51,11 @@ def test_merge_scenarios(source, upstream, expected_keys):
```

### 3.3 No Autouse Fixtures

`autouse=True` fixtures are **never allowed**. They hide setup logic and can cause non-obvious side effects or dependencies between tests. All fixtures used by a test must be explicitly requested in the test function's arguments.

### 3.4 Main Entry Point

Every test file **must** end with a main entry point block. This ensures each file is independently executable as a script (`python tests/test_foo.py`).

```python
Expand All @@ -60,6 +64,7 @@ if __name__ == "__main__":
```

**Why this matters:**

1. **Direct Execution**: Developers can run a single test file using standard Python without needing to remember complex `pytest` filter flags.
2. **IDE Workflow Integration**: Many IDEs (like VS Code or PyCharm) allow you to run the "Current File" with a single click or keyboard shortcut. Having a main block ensures this works out of the box with the correct verbosity and scope.
3. **Cleaner Diffs**: By terminating the file with this standard block, it prevents "no newline at end of file" warnings and ensures that new tests added above it produce clean, isolated diff segments. It also ensures that when debugging with `--icdiff` or similar tools, the output is scoped correctly to the specific file.
Expand All @@ -69,8 +74,11 @@ if __name__ == "__main__":
`tomlkit` is central to this project but its dynamic type system can be tricky for mypy.

### The "Proxy" Problem

`tomlkit` often returns "proxy" objects (like dotted keys) that don't always behave like standard dicts.

- **Assertion Pattern**: To satisfy mypy when indexing into a parsed document in tests, use the `cast(Any, ...)` pattern:

```python
from typing import Any, cast
import tomlkit
Expand All @@ -80,20 +88,25 @@ if __name__ == "__main__":
ruff_cfg = cast(Any, doc)["tool"]["ruff"]
assert ruff_cfg["target-version"] == "py310"
```

- **Comparison**: Use `list()` or `.unwrap()` if you need to compare `tomlkit` arrays/objects to standard Python types.

## 4. Lifecycle TOML Fixtures

For end-to-end (E2E) testing of the sync/merge logic, use the "Lifecycle" pattern.

### Fixture Triples

Each test case consists of three files in `tests/lifecycle_tomls/`:

1. `<case_name>_initial.toml`: The starting project state.
2. `<case_name>_upstream.toml`: The remote ruff config to sync from.
3. `<case_name>_final.toml`: The expected result after merge.

### Scaffolding New Cases

Use the provided Invoke task to create a new case from a template:

```bash
uv run invoke new-case --name <case_name> --description "Description of the edge case"
```
Expand Down Expand Up @@ -125,5 +138,6 @@ def test_my_edge_case():
## 6. Code Coverage

We target **high coverage** for `ruff_sync.py`.

- Run coverage locally: `uv run coverage run -m pytest -vv && uv run coverage report`
- New features MUST include unit tests in `tests/test_basic.py` or specialized files like `tests/test_whitespace.py` if they involve formatting logic.
4 changes: 4 additions & 0 deletions .agents/skills/mkdocs-generation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mike>=2.0
## Directory Structure

**Simple (flat)**:

```
docs/
├── index.md # Home/overview
Expand All @@ -49,6 +50,7 @@ docs/
```

**Complex (nested)**:

```
docs/
├── index.md
Expand All @@ -69,6 +71,7 @@ docs/
See `templates/mkdocs.yml` for the full configuration template.

Key sections:

1. **Site metadata**: name, description, URLs
2. **Versioning**: mike provider for multi-version docs
3. **Theme**: Material with navigation features
Expand All @@ -88,6 +91,7 @@ Create minimal markdown files that reference Python modules:
```

mkdocstrings auto-generates documentation from docstrings. Configure in `mkdocs.yml`:

- `docstring_style: google` - Use Google-style docstrings
- `show_source: false` - Hide source code
- `merge_init_into_class: true` - Combine `__init__` with class docs
Expand Down
25 changes: 17 additions & 8 deletions .agents/skills/mkdocs-generation/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ docs/
```

**mkdocs.yml nav:**

```yaml
nav:
- Home: index.md
Expand All @@ -24,6 +25,7 @@ nav:
```

**index.md:**

```markdown
# Todoist MCP Server

Expand Down Expand Up @@ -66,6 +68,7 @@ docs/
```

**mkdocs.yml nav:**

```yaml
nav:
- Home: index.md
Expand Down Expand Up @@ -116,11 +119,11 @@ import asyncio
from pydmp import DMPPanel

async def main():
panel = DMPPanel()
await panel.connect("192.168.1.100", "00001", "YOURKEY")
await panel.update_status()
areas = await panel.get_areas()
await panel.disconnect()
panel = DMPPanel()
await panel.connect("192.168.1.100", "00001", "YOURKEY")
Comment on lines 119 to +123
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Python example inside main() is no longer valid due to lost indentation.

This makes the snippet syntactically invalid and likely to confuse readers. Please re-indent the example code so it is properly nested inside async def main().

await panel.update_status()
areas = await panel.get_areas()
await panel.disconnect()

asyncio.run(main())
\`\`\`
Expand Down Expand Up @@ -173,20 +176,22 @@ The CLI expects a YAML config file:

\`\`\`yaml
panel:
Comment on lines 176 to 178
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): YAML config under panel: lost indentation and is no longer a valid nested mapping.

host, account, and remote_key need to remain indented as children of panel: for the YAML to be valid and preserve the intended structure.

host: 192.168.1.100
account: "00001"
remote_key: "YOURKEY"
host: 192.168.1.100
account: "00001"
remote_key: "YOURKEY"
\`\`\`

## Commands

### Areas & Zones

\`\`\`bash
pydmp get-areas [--json|-j]
pydmp get-zones [--json|-j]
\`\`\`

### Arm/Disarm

\`\`\`bash
pydmp arm "1,2,3" [--bypass-faulted|-b] [--force-arm|-f]
pydmp disarm <AREA> [--json|-j]
Expand All @@ -195,10 +200,13 @@ pydmp disarm <AREA> [--json|-j]
## Examples

\`\`\`bash

# View areas with debug logs

pydmp --debug get-areas

# Arm area 1

pydmp arm "1" --bypass-faulted
\`\`\`
```
Expand All @@ -216,6 +224,7 @@ docs = [
```

Install and build:

```bash
pip install -e ".[docs]"
mkdocs serve
Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/release-notes-generation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ uv run invoke release --draft --skip-tests

> [!NOTE]
> `invoke release` will:
>
> 1. Check if you are on `main`.
> 2. Check for clean git state.
> 3. Create a GitHub Release with `--generate-notes`.
Expand All @@ -52,13 +53,15 @@ uv run invoke release --draft --skip-tests
GitHub's automatically generated notes are a good start but often lack professional categorization and narrative. Use the following structure for final refinement:

#### Categorization

- **🚀 Features**: New capabilities added to `ruff_sync`.
- **🐞 Bug Fixes**: Issues resolved in CLI, merging logic, or HTTP handling.
- **✨ Improvements**: Enhancements to existing features, performance, or logging.
- **📖 Documentation**: Updates to `README.md`, `docs/`, or docstrings.
- **🛠️ Maintenance**: Dependency updates, CI changes, or test refactoring.

#### Writing Style

- Use clear, action-oriented language (e.g., "Add support...", "Fix issue where...", "Refactor...").
- Link to PRs and contributors using their GitHub handles.
- Include a "Breaking Changes" section if applicable (use `[!WARNING]` alerts).
Expand Down
12 changes: 10 additions & 2 deletions .agents/workflows/add-test-case.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,40 @@ description: How to add a new lifecycle TOML test case for ruff-sync
Follow these steps to add a new end-to-end (E2E) test case for `ruff-sync` to test a specific sync or merge scenario.

### 1. Identify the Edge Case or Bug

- Clearly define the "Initial" state (local `pyproject.toml`) and the "Upstream" state (remote `pyproject.toml`).
- Define the "Final" expected state after the sync.

### 2. Scaffold the Fixture

// turbo

1. Run the `new-case` task to generate the file triple:
```bash
uv run invoke new-case --name <case_name> --description "Description of the test case"
```
*Replace `<case_name>` with a simple name (e.g., `dotted_keys`). This creates files in `tests/lifecycle_tomls/`.*
_Replace `<case_name>` with a simple name (e.g., `dotted_keys`). This creates files in `tests/lifecycle_tomls/`._

### 3. Edit the Fixtures

1. Edit `tests/lifecycle_tomls/<case_name>_initial.toml` with the local starting state.
2. Edit `tests/lifecycle_tomls/<case_name>_upstream.toml` with the remote config (must contain a `[tool.ruff]` section).
3. Edit `tests/lifecycle_tomls/<case_name>_final.toml` with the expected result of the merge.

### 4. Run the E2E Test Suite

// turbo

1. Execute the tests to ensure the new case is picked up:
```bash
uv run pytest -vv tests/test_e2e.py
```
*The E2E suite automatically discovers all file triples in the `lifecycle_tomls/` directory.*
_The E2E suite automatically discovers all file triples in the `lifecycle_tomls/` directory._

### 5. Validate Formatting and Types

// turbo

1. Ensure the new TOML files don't break any project rules:
```bash
uv run invoke lint --check
Expand Down
56 changes: 42 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,68 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-ast
- id: check-json
- id: check-yaml
- id: check-ast # validates Python syntax before committing
- id: check-json # validates JSON file structure
- id: check-yaml # validates YAML file structure
args: ["--unsafe"]
exclude: .agents/skills/mkdocs-generation/templates/mkdocs.yml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: no-commit-to-branch
- id: check-toml # validates pyproject.toml and other TOML files
- id: check-merge-conflict # blocks files that still have <<< merge conflict markers
- id: check-added-large-files # prevents files over 1MB from being committed
args: ["--maxkb=1024"]
- id: check-case-conflict # catches filename casing issues across Windows/Mac/Linux
- id: detect-private-key # blocks accidental commit of SSH keys and API secrets
- id: mixed-line-ending # enforces LF line endings for cross-platform consistency
args: ["--fix=lf"]
- id: end-of-file-fixer # ensures every file ends with a newline
- id: trailing-whitespace # removes trailing whitespace from all lines
- id: no-commit-to-branch # prevents direct commits to develop and main branches
args: [--branch, develop, --branch, main]

# Ruff: fast Python linter and formatter (replaces flake8, isort, black)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.5"
hooks:
- id: ruff-check
- id: ruff-check # lints Python code and auto-fixes where possible
args: ["--fix"]
- id: ruff-format
- id: ruff-format # formats Python code consistently

# Prettier: opinionated formatter for YAML, JSON, and Markdown
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [yaml, json]
types_or: [yaml, json, markdown] # formats all three file types
exclude: .agents/skills/mkdocs-generation/templates/mkdocs.yml

# Actionlint: static analysis for GitHub Actions workflow files
- repo: https://github.com/rhysd/actionlint
rev: v1.7.11
hooks:
- id: actionlint
exclude: .github/workflows/complexity.yaml

# Conventional Commits: enforces structured commit messages like feat:, fix:, chore:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.6.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg] # runs only on the commit message stage
args: [feat, fix, docs, style, refactor, test, chore, ci, build]

# Gitleaks: scans for hardcoded secrets, tokens, and credentials
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.0
hooks:
- id: gitleaks

# https://pre-commit.ci/
ci:
autofix_commit_msg: |
[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: "main"
autofix_prs: true # automatically opens a PR with fixes
autoupdate_branch: "main" # targets main branch for dependency updates
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
autoupdate_schedule: weekly
submodules: false
autoupdate_schedule: weekly # checks for hook version updates every week
submodules: false # does not run hooks inside git submodules
1 change: 0 additions & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

The MIT License (MIT)

Copyright (c) 2023 Gabriel Gore
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ If you maintain more than one Python project, you've probably copy-pasted your `

### How Other Ecosystems Solve This

| Ecosystem | Mechanism | Limitation for Ruff users |
|-----------|-----------|---------------------------|
| **ESLint** | [Shareable configs](https://eslint.org/docs/latest/extend/shareable-configs) — publish an npm package, then `extends: ["my-org-config"]` | Requires a package registry (npm). Python doesn't have an equivalent convention. |
| **Prettier** | [Shared configs](https://prettier.io/docs/sharing-configurations) — same npm-package pattern, referenced via `"prettier": "@my-org/prettier-config"` in `package.json` | Same — tightly coupled to npm. |
| **Ruff** | [`extend`](https://docs.astral.sh/ruff/configuration/#config-file-discovery) — extend from a _local_ file path (great for monorepos) | Only supports local paths. No native remote URL support ([requested in astral-sh/ruff#12352](https://github.com/astral-sh/ruff/issues/12352)). |
| Ecosystem | Mechanism | Limitation for Ruff users |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **ESLint** | [Shareable configs](https://eslint.org/docs/latest/extend/shareable-configs) — publish an npm package, then `extends: ["my-org-config"]` | Requires a package registry (npm). Python doesn't have an equivalent convention. |
| **Prettier** | [Shared configs](https://prettier.io/docs/sharing-configurations) — same npm-package pattern, referenced via `"prettier": "@my-org/prettier-config"` in `package.json` | Same — tightly coupled to npm. |
| **Ruff** | [`extend`](https://docs.astral.sh/ruff/configuration/#config-file-discovery) — extend from a _local_ file path (great for monorepos) | Only supports local paths. No native remote URL support ([requested in astral-sh/ruff#12352](https://github.com/astral-sh/ruff/issues/12352)). |

Ruff's `extend` is perfect inside a monorepo, but if your projects live in **separate repositories**, there's no built-in way to inherit config from a central source.

Expand Down Expand Up @@ -199,7 +199,7 @@ Ensure your configuration is always in sync before every commit. Add this to you

```yaml
- repo: https://github.com/Kilo59/ruff-sync
rev: v0.1.0 # Use the latest version
rev: v0.1.0 # Use the latest version
hooks:
- id: ruff-sync-check
```
Expand Down
Loading