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. 🐕