Skip to main content

Synchronize Ruff linter configuration across projects

Project description

ruff-sync banner
PyPI version codecov pre-commit.ci status Ruff Wily

ruff-sync

Keep your Ruff config consistent across multiple projects.

ruff-sync is a CLI tool that pulls a canonical Ruff configuration from an upstream pyproject.toml (hosted anywhere — GitHub, GitLab, or any raw URL) and merges it into your local project, preserving your comments, formatting, and project-specific overrides.


Table of Contents

The Problem

If you maintain more than one Python project, you've probably copy-pasted your [tool.ruff] config between repos more than once. When you decide to enable a new rule or bump your target Python version, you get to do it again — in every repo. Configs drift, standards diverge, and your "shared" style guide becomes a polite suggestion.

How Other Ecosystems Solve This

Ecosystem Mechanism Limitation for Ruff users
ESLint 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 — same npm-package pattern, referenced via "prettier": "@my-org/prettier-config" in package.json Same — tightly coupled to npm.
Ruff extend — extend from a local file path (great for monorepos) Only supports local paths. No native remote URL support (requested in astral-sh/ruff#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.

That's what ruff-sync does.

How It Works

┌─────────────────────────────┐
│  Upstream repo              │
│  (your "source of truth")   │
│                             │
│  pyproject.toml             │
│    [tool.ruff]              │
│    target-version = "py310" │
│    lint.select = [...]      │
└──────────┬──────────────────┘
           │  ruff-sync downloads
           │  & extracts [tool.ruff]
           ▼
┌─────────────────────────────┐
│  Your local project         │
│                             │
│  pyproject.toml             │
│    [tool.ruff]  ◄── merged  │
│    # your comments kept ✓   │
│    # formatting kept ✓      │
│    # per-file-ignores kept ✓│
└─────────────────────────────┘
  1. You point ruff-sync at the URL of your canonical pyproject.toml.
  2. It downloads the file, extracts the [tool.ruff] section.
  3. It merges the upstream config into your local pyproject.toml — updating values that changed, adding new rules, but preserving your local comments, whitespace, and any sections you've chosen to exclude (like per-file-ignores).

No package registry. No publishing step. Just a URL.

Quick Start

Install

With uv (recommended):

uv tool install ruff-sync

With pipx:

pipx install ruff-sync

With pip:

pip install ruff-sync

From Source (Bleeding Edge)

If you want the latest development version:

uv tool install git+https://github.com/Kilo59/ruff-sync

Usage

# Sync from a GitHub repository (defaults to main/pyproject.toml)
ruff-sync https://github.com/my-org/standards

# Or a direct blob/file URL (auto-converts to raw)
ruff-sync https://github.com/my-org/standards/blob/main/pyproject.toml

# GitLab support (including nested projects)
ruff-sync https://gitlab.com/my-org/my-group/nested/standards

# Once configured in pyproject.toml (see Configuration), simply run:
ruff-sync

# Sync into a specific project directory
ruff-sync --source ./my-project

# Exclude specific sections from being overwritten using dotted paths
ruff-sync --exclude lint.per-file-ignores lint.ignore

# Check if your local config is in sync (useful in CI)
ruff-sync check https://github.com/my-org/standards

# Semantic check  ignore cosmetic differences like comments and whitespace
ruff-sync check --semantic

Run ruff-sync --help for full details on all available options.

Key Features

  • Format-preserving merges — Uses tomlkit under the hood, so your comments, whitespace, and TOML structure are preserved. No reformatting surprises.
  • GitHub & GitLab URL support — Automatically converts GitHub/GitLab repository URLs or blob URLs to raw content URLs.
  • Selective exclusions — Keep project-specific overrides (like per-file-ignores or target-version) from being clobbered by the upstream config.
  • Works with any host — GitHub, GitLab, Bitbucket, or any raw URL that serves a pyproject.toml.
  • CI-ready check command — Verify that your local config is in sync without modifying anything. Exits 1 if out of sync, making it perfect for pre-merge gates. (See detailed logic)
  • Semantic mode — Use --semantic to ignore cosmetic differences (comments, whitespace) and only fail on real value changes.

Configuration

You can configure ruff-sync itself in your pyproject.toml:

[tool.ruff-sync]
# The source of truth for your Ruff configuration
upstream = "https://github.com/my-org/standards"

# Use simple names for top-level keys, and dotted paths for nested keys
exclude = [
    "target-version",                      # Top-level [tool.ruff] key — projects target different Python versions
    "lint.per-file-ignores",                # Project-specific file overrides
    "lint.ignore",                         # Project-specific rule suppressions
    "lint.isort.known-first-party",         # Every project has different first-party packages
    "lint.flake8-tidy-imports.banned-api",  # Entire plugin section — project-specific banned APIs
    "lint.pydocstyle.convention",          # Teams may disagree on google vs numpy vs pep257
]

This sets the default upstream and exclusions so you don't need to pass them on the command line every time. Note: Any explicitly provided CLI arguments will override the values in pyproject.toml.

Advanced Configuration

For more complex setups, you can also configure the default branch and parent directory used when resolving repository URLs (e.g. https://github.com/my-org/standards):

[tool.ruff-sync]
upstream = "https://github.com/my-org/standards"

# Use a specific branch or tag (default: "main")
branch = "develop"

# Specify a parent directory if pyproject.toml is not at the repo root
path = "config/ruff"

CI Integration

The check command is designed for use in CI pipelines. Add it as a step to catch config drift before it merges:

# .github/workflows/ci.yaml
- name: Check ruff config is in sync
  run: |
    ruff-sync check --semantic

With --semantic, minor reformatting of your local file won't cause a false positive — only actual rule or value differences will fail the check.

To see exactly what's drifted, omit --no-diff (the default) and the output will include a unified diff:

$ ruff-sync check --semantic
🔍 Checking Ruff sync status...
❌ Ruff configuration at pyproject.toml is out of sync!
--- local (semantic)
+++ upstream (semantic)
@@ -5,6 +5,7 @@
   "select": [
+    "PERF",
     "RUF",
     ...
   ]

Example Workflow

A typical setup for an organization:

  1. Create a "standards" repo with your canonical pyproject.toml containing your shared [tool.ruff] config.
  2. In each project, run ruff-sync pointing at that repo — either manually, in a Makefile, or as a CI step.
  3. When you update the standard, re-run ruff-sync in each project to pull the changes. Your local comments and per-file-ignores stay intact.
# In each project repo:
ruff-sync https://github.com/my-org/python-standards
git diff pyproject.toml  # review the changes
git commit -am "sync ruff config from upstream"

Detailed Check Logic

When you run ruff-sync check, it follows this process to determine if your project has drifted from the upstream source:

flowchart TD
    Start([Start]) --> Local[Read Local pyproject.toml]
    Local --> Upstream[Download Upstream pyproject.toml]
    Upstream --> Extract[Extract tool.ruff section]
    Extract --> Exclude[Apply Exclusions]
    Exclude --> Merge[Perform in-memory Merge]

    subgraph Comparison [Comparison Logic]
        direction TB
        SemanticNode{--semantic?}
        SemanticNode -- Yes --> Unwrap[Unwrap TOML objects to Python Dicts]
        Unwrap --> CompareVal[Compare Key/Value Pairs]
        SemanticNode -- No --> CompareFull[Compare Full File Strings]
    end

    Merge --> Comparison

    CompareVal --> ResultNode{Match?}
    CompareFull --> ResultNode

    ResultNode -- Yes --> Success([Exit 0: In Sync])
    ResultNode -- No --> Diff[Generate Diff]
    Diff --> Fail([Exit 1: Out of Sync])

    %% Styling
    style Start fill:#4a90e2,color:#fff,stroke:#357abd
    style Success fill:#48c774,color:#fff,stroke:#36975a
    style Fail fill:#f14668,color:#fff,stroke:#b2334b
    style ResultNode fill:#ffdd57,color:#4a4a4a,stroke:#d4b106
    style Comparison fill:none,stroke:#9e9e9e,stroke-dasharray: 5 5,stroke-width:2px
    style SemanticNode fill:#f4f4f4,color:#363636,stroke:#dbdbdb

Contributing

This project uses:

  • uv for dependency management
  • Ruff for linting and formatting
  • mypy for type checking (strict mode)
  • pytest for testing
# Setup
uv sync --group dev

# Run checks
uv run ruff check . --fix   # lint
uv run ruff format .        # format
uv run mypy .               # type check
uv run pytest -vv           # test

Dogfooding

To see ruff-sync in action, you can "dogfood" it on this project's own config.

Check if this project is in sync with its upstream:

./scripts/dogfood_check.sh

Or sync from a large upstream like Pydantic's config:

./scripts/dogfood.sh

This will download Pydantic's Ruff configuration and merge it into the local pyproject.toml. You can then use git diff to see how it merged the keys while preserving the existing structure and comments.

To revert the changes after testing:

git checkout pyproject.toml

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ruff_sync-0.0.3.dev1.tar.gz (355.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ruff_sync-0.0.3.dev1-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file ruff_sync-0.0.3.dev1.tar.gz.

File metadata

  • Download URL: ruff_sync-0.0.3.dev1.tar.gz
  • Upload date:
  • Size: 355.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ruff_sync-0.0.3.dev1.tar.gz
Algorithm Hash digest
SHA256 f128991a5632dacd13fe20c54521c601fe81243056ec58cdcbafbada3c9e7af2
MD5 8aab53646fae77fca7c62e63f4d6fc83
BLAKE2b-256 502ae8205599e29135b9bdb57bd14719a4678dd372871ad152e77ddf2fb273a3

See more details on using hashes here.

File details

Details for the file ruff_sync-0.0.3.dev1-py3-none-any.whl.

File metadata

  • Download URL: ruff_sync-0.0.3.dev1-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ruff_sync-0.0.3.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 4d3e1419f639351811b0559736bc877c39d2fc36f017a3db5af286b6d5090057
MD5 a8d52f07261b48665464736a28e0e47d
BLAKE2b-256 09ed507fa49ac654904ca1e77ba8a71d45d1e0bfa5b3d08f38088160afa474ba

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page