Skip to content

# Contributing

loly is open source and welcomes contributions! Bug fixes, new policies, documentation improvements—we'll take them all. The logging apocalypse requires all hands on deck.

## Quick Start

Get the code running locally in a few commands:

    # Clone the repo (fork it first if you're new)
    git clone https://github.com/YOUR_USERNAME/ly.git
    cd ly

    # Install everything loly needs to breathe
    uv sync

    # Run the test suite (please don't break them)
    uv run pytest

    # Run loly on itself (dogfooding is good medicine)
    uv run loly src/

## Development Setup

### Prerequisites

You'll need these things to exist on your machine:

- Python 3.9+ (or 3.12+, we're not picky)
- uv (recommended, it's actually great)
- Or `pip` (if you're feeling nostalgic)

### Installation

Get your development environment ready:

    # Install in development mode (editable installs are your friend)
    uv pip install -e .

    # Install dev dependencies (linting, testing, docs, etc.)
    uv pip install -e ".[dev]"

### Running Tests

Make sure everything works as intended:

    # Run all tests (the comprehensive check)
    uv run pytest

    # Run with coverage (see what you're actually testing)
    uv run pytest --cov=loly

    # Run a specific test (for surgical debugging)
    uv run pytest tests/unit/test_exception_exc_info.py

## Project Structure

Here's what lives where:

    ly/
    ├── src/loly/
    │   ├── policies/          # Where the magic happens
    │   │   ├── base.py        # Base policy class (extend this)
    │   │   ├── exception_exc_info.py  # The LY001 detector
    │   │   └── log_loop.py    # The LY002 detector
    │   ├── file_collector.py  # Finds Python files (boring but necessary)
    │   ├── linter.py          # Orchestrates everything
    │   └── cli.py             # The command-line face
    ├── tests/
    │   ├── unit/              # Tests for individual components
    │   └── integration/       # Tests for the whole system
    ├── docs_site/             # This documentation (MkDocs)
    └── battle-test/           # Real-world codebases we tested against

## Adding a New Policy

So you want to add a policy? Excellent. Here's the process:

### 1. Create the Policy Class

Create `src/loly/policies/your_policy.py`. It's AST visitor time:

    from pathlib import Path
    from typing import List, Dict
    import libcst as cst
    from loly.policies.base import BasePolicy

    class _YourPolicyVisitor(cst.CSTVisitor):
        """Internal visitor. The name with underscore is a convention."""

        def __init__(self, levels: List[str], logger_names: List[str],
                     file_path: Path, wrapper: cst.MetadataWrapper):
            self.levels = levels
            self.logger_names = logger_names
            self.file_path = file_path
            self.violations = []
            self._wrapper = wrapper

        def visit_Call(self, node: cst.Call) -> None:
            # Your detection logic here - traverse the AST
            if self._should_flag(node):
                self.violations.append({
                    "file": str(self.file_path),
                    "line": self._get_line_number(node),
                    "code": "LY00X",
                    "message": "Your violation message goes here",
                    "category": "your_policy",
                    "severity": "fail"
                })

        def _get_line_number(self, node: cst.CSTNode) -> int:
            try:
                pos = self._wrapper.resolve(cst.metadata.PositionProvider)[node]
                return pos.start.line
            except:
                return 0

    class YourPolicy(BasePolicy):
        """What does this policy do? Be explicit."""

        @classmethod
        def check(cls, code: str, file_path: Path, levels: List[str],
                  logger_names: List[str]) -> List[Dict]:
            module = cst.parse_module(code)
            wrapper = cst.MetadataWrapper(module)
            visitor = _YourPolicyVisitor(levels, logger_names, file_path, wrapper)
            wrapper.visit(visitor)
            return visitor.violations

### 2. Add Comprehensive Tests

Create `tests/unit/src/loly/policies/test_your_policy.py`. Tests are not optional:

    from pathlib import Path
    from loly.policies.your_policy import YourPolicy

    def test_should_pass():
        """Code that follows the rule."""
        code = """
    logger.info("Valid logging pattern")
    """
        violations = YourPolicy.check(code, Path("test.py"), ["info"], ["logger"])
        assert len(violations) == 0

    def test_should_fail():
        """Code that violates the rule."""
        code = """
    logger.info("Invalid pattern")
    """
        violations = YourPolicy.check(code, Path("test.py"), ["info"], ["logger"])
        assert len(violations) == 1
        assert violations[0]["code"] == "LY00X"

    # Add edge cases, weird scenarios, everything you can think of

### 3. Register the Policy

Add it to `src/loly/linter.py` so it actually runs:

    from loly.policies.your_policy import YourPolicy

    # In the Linter class
    POLICIES = {
        "exception_exc_info": ExceptionExcInfoPolicy,
        "log_loop": LogLoopPolicy,
        "your_policy": YourPolicy,  # Add your new one here
    }

### 4. Document the Policy

Create `docs_site/docs/policies/ly00x-your-policy.md`. Make it entertaining. Make it clear. Follow the format of LY001 and LY002.

## Code of Conduct

Be kind. Be respectful. Be constructive. We're all trying to make logging better and save each other from 3 AM incidents. That's a shared mission.

Trolling is not welcome. Helping is.

Thank you for contributing to loly! Together, we're making the logging world a better place. 🐕