Skip to content

cli

ruff_sync.cli

Synchronize Ruff linter configuration across Python projects.

This module provides a CLI tool and library for downloading, parsing, and merging Ruff configuration from upstream sources (like GitHub/GitLab) into local projects.

__all__ module-attribute

__all__ = [
    "Arguments",
    "ColoredFormatter",
    "get_config",
    "main",
]

__version__ module-attribute

__version__ = '0.1.0.dev0'

LOGGER module-attribute

LOGGER = getLogger(__name__)

PARSER module-attribute

PARSER = _get_cli_parser()

ColoredFormatter

Bases: Formatter

Logging Formatter to add colors.

Source code in src/ruff_sync/cli.py
class ColoredFormatter(logging.Formatter):
    """Logging Formatter to add colors."""

    RESET: ClassVar[str] = "\x1b[0m"
    COLORS: ClassVar[Mapping[int, str]] = {
        logging.DEBUG: "\x1b[36m",  # Cyan
        logging.INFO: "\x1b[32m",  # Green
        logging.WARNING: "\x1b[33m",  # Yellow
        logging.ERROR: "\x1b[31m",  # Red
        logging.CRITICAL: "\x1b[1;31m",  # Bold Red
    }

    def __init__(self, fmt: str = "%(message)s") -> None:
        """Initialize the formatter with a format string."""
        super().__init__(fmt)

    def format(self, record: logging.LogRecord) -> str:  # type: ignore[explicit-override]
        """Format the log record with colors if the output is a TTY."""
        if sys.stderr.isatty():
            color = self.COLORS.get(record.levelno, self.RESET)
            return f"{color}{super().format(record)}{self.RESET}"
        return super().format(record)

RESET class-attribute

RESET = '\x1b[0m'

COLORS class-attribute

COLORS = {
    DEBUG: "\x1b[36m",
    INFO: "\x1b[32m",
    WARNING: "\x1b[33m",
    ERROR: "\x1b[31m",
    CRITICAL: "\x1b[1;31m",
}

__init__

__init__(fmt='%(message)s')

Initialize the formatter with a format string.

Source code in src/ruff_sync/cli.py
def __init__(self, fmt: str = "%(message)s") -> None:
    """Initialize the formatter with a format string."""
    super().__init__(fmt)

format

format(record)

Format the log record with colors if the output is a TTY.

Source code in src/ruff_sync/cli.py
def format(self, record: logging.LogRecord) -> str:  # type: ignore[explicit-override]
    """Format the log record with colors if the output is a TTY."""
    if sys.stderr.isatty():
        color = self.COLORS.get(record.levelno, self.RESET)
        return f"{color}{super().format(record)}{self.RESET}"
    return super().format(record)

Arguments

Bases: NamedTuple

CLI arguments for the ruff-sync tool.

Source code in src/ruff_sync/cli.py
class Arguments(NamedTuple):
    """CLI arguments for the ruff-sync tool."""

    command: str
    upstream: URL
    to: pathlib.Path
    exclude: Iterable[str]
    verbose: int
    branch: str = "main"
    path: str = ""
    semantic: bool = False
    diff: bool = True
    init: bool = False

    @property
    @deprecated("Use 'to' instead")
    def source(self) -> pathlib.Path:
        """Deprecated: use 'to' instead."""
        return self.to

    @classmethod
    @lru_cache(maxsize=1)
    def fields(cls) -> set[str]:
        """Return the set of all field names, including deprecated ones."""
        return set(cls._fields) | {"source"}

command instance-attribute

command

upstream instance-attribute

upstream

to instance-attribute

to

exclude instance-attribute

exclude

verbose instance-attribute

verbose

branch class-attribute instance-attribute

branch = 'main'

path class-attribute instance-attribute

path = ''

semantic class-attribute instance-attribute

semantic = False

diff class-attribute instance-attribute

diff = True

init class-attribute instance-attribute

init = False

source property

source

Deprecated: use 'to' instead.

fields cached classmethod

fields()

Return the set of all field names, including deprecated ones.

Source code in src/ruff_sync/cli.py
@classmethod
@lru_cache(maxsize=1)
def fields(cls) -> set[str]:
    """Return the set of all field names, including deprecated ones."""
    return set(cls._fields) | {"source"}

get_config cached

get_config(source)

Read [tool.ruff-sync] configuration from pyproject.toml.

Examples:

>>> import pathlib
>>> config = get_config(pathlib.Path("."))
>>> if "upstream" in config:
...     print(f"Syncing from {config['upstream']}")
Source code in src/ruff_sync/cli.py
@lru_cache(maxsize=1)
def get_config(
    source: pathlib.Path,
) -> Config:
    """Read [tool.ruff-sync] configuration from pyproject.toml.

    Examples:
        >>> import pathlib
        >>> config = get_config(pathlib.Path("."))
        >>> if "upstream" in config:
        ...     print(f"Syncing from {config['upstream']}")
    """
    local_toml = source / "pyproject.toml"
    # TODO: use pydantic to validate the toml file
    cfg_result: dict[str, Any] = {}
    if local_toml.exists():
        toml = tomlkit.parse(local_toml.read_text())
        config = toml.get("tool", {}).get("ruff-sync")
        if config:
            allowed_keys = set(Config.__annotations__.keys())
            for arg, value in config.items():
                if arg in allowed_keys:
                    if arg == "source":
                        LOGGER.warning(
                            "DeprecationWarning: [tool.ruff-sync] 'source' is deprecated. "
                            "Use 'to' instead."
                        )
                    cfg_result[arg] = value
                else:
                    LOGGER.warning(f"Unknown ruff-sync configuration: {arg}")
            # Ensure 'to' is populated if 'source' was used
            if "source" in cfg_result and "to" not in cfg_result:
                cfg_result["to"] = cfg_result["source"]
    return cast("Config", cfg_result)

main

main()

Run the ruff-sync CLI.

Source code in src/ruff_sync/cli.py
def main() -> int:
    """Run the ruff-sync CLI."""
    # Handle backward compatibility: default to 'pull' if no command provided
    if len(sys.argv) > 1 and sys.argv[1] not in (
        "pull",
        "check",
        "-h",
        "--help",
        "--version",
    ):
        sys.argv.insert(1, "pull")
    elif len(sys.argv) == 1:
        sys.argv.append("pull")

    args = PARSER.parse_args()

    # Configure logging
    log_level = {
        0: logging.WARNING,
        1: logging.INFO,
    }.get(args.verbose, logging.DEBUG)

    LOGGER.setLevel(log_level)
    handler = logging.StreamHandler()
    handler.setFormatter(ColoredFormatter())
    LOGGER.addHandler(handler)
    LOGGER.propagate = "PYTEST_CURRENT_TEST" in os.environ  # Allow capturing in tests

    # Determine target 'to' from CLI or use default '.'
    # Defer Path conversion to avoid pyfakefs issues with captured Path class
    arg_to = args.to or args.source
    initial_to = pathlib.Path(arg_to) if arg_to else pathlib.Path()
    config: Config = get_config(initial_to)

    upstream, to_val, exclude, branch, path = _resolve_args(args, config, initial_to)

    # Convert non-raw github/gitlab upstream url to the raw equivalent
    upstream = resolve_raw_url(upstream, branch=branch, path=path)

    # Create Arguments object
    exec_args = Arguments(
        command=args.command,
        upstream=upstream,
        to=to_val,
        exclude=exclude,
        verbose=args.verbose,
        branch=branch,
        path=path,
        semantic=getattr(args, "semantic", False),
        diff=getattr(args, "diff", True),
        init=getattr(args, "init", False),
    )

    if exec_args.command == "check":
        return asyncio.run(check(exec_args))
    return asyncio.run(pull(exec_args))