Contributing#

This guide covers the development workflow, testing, code style, and other requirements to contribute to icalendar-anonymizer.

icalendar-anonymizer follows the Python Calendaring Ecosystem’s Code of Conduct.

Prerequisites#

You’ll need to first install prerequisites on, and clone the icalendar-anonymizer repository to, your local computer.

Tests, code formatting, and documentation builds and checks require that you install GNU Make, uv, and Git.

Note

uv handles the installation of Python and all dependencies for icalendar-anonymizer.

Make#

Make is used to provide an interface to developers to perform repetitive tasks with a single command.

Make comes installed on most Linux distributions. On macOS, you must first install Xcode, then install its command line tools. On Windows, it’s recommended to Install Linux on Windows with WSL, which will include Make.

Finally, it’s a good idea to update your system’s version of Make, because some distributions, especially macOS, have an outdated version. Use your favorite search engine or trusted online resource for how to update Make.

uv#

uv is used for installing Python, creating a Python virtual environment, and managing dependencies for documentation.

Install uv. Read the console output for further instructions, and follow them, if needed.

curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Git#

Install Git for your operating system.

Configure Git#

Unless you’re a maintainer or administrator, you don’t have write access to push commits to GitHub repositories under the pycalendar organization or the icalendar-anonymizer repository. You can, however, push commits to your fork. Thus, a typical workflow will be circular in nature. You’ll pull code from the upstream icalendar-anonymizer repository, push your work from your local clone to your remote fork, then make a pull request from your fork to the upstream icalendar-anonymizer repository.

  1. Start by forking icalendar-anonymizer’s repository to your account through the GitHub interface.

  2. Clone your forked repository.

  3. Configure Git to sync your fork with the upstream repository.

Install icalendar-anonymizer for development#

Change your directory to your local clone.

cd icalendar-anonymizer

Install icalendar-anonymizer for development—including all of its dependencies for tests, documentation, and formatting code, as well as a Python virtual environment—with the following command.

make dev

Development workflow#

This section covers the general development workflow. Subsequent sections go into more detail.

Follow these steps from the root of your clone.

  1. Check out the main branch, update it locally, and create a branch from it.

    git checkout main
    git pull origin main
    git checkout -b feature-name
    
  2. Write your code changes, corresponding tests, docstrings, and narrative documentation. All new features must include tests and documentation.

  3. Run tests. You can pass any options that pytest supports as an environment variable or argument TESTOPTS.

    make test
    
  4. Build documentation and view a live preview in a web browser.

    make livehtml
    
  5. Check links in documentation.

    make linkcheckbroken
    
  6. Check spelling, style, and grammar in narrative documentation.

    make vale
    
  7. Lint and format code.

    make lint-check  # Check for linting errors
    make lint-fix    # Auto-fix linting errors
    make format      # Format code
    
  8. Commit your changes, using the Conventional Commits message format that follows Commit Message Format.

    git add .
    git commit -m "feat: add new feature description"
    
  9. Push your changes to GitHub.

    git push origin feature-name
    
  10. Open a pull request on GitHub.

Run tests#

This section describes how to run tests.

Run all tests#

make test

Run tests with coverage#

make coverage

The test coverage report will be in htmlcov/index.html.

Test requirements#

All tests must satisfy the following requirements.

  • A minimum of 90% coverage is required and enforced by continuous integration (CI).

  • All tests must pass.

  • All new features and bug fixes must have tests.

  • Use parametrized tests to reduce duplication, as described in Test organization.

CI test matrix#

Continuous integration runs tests on the matrix cross-product of the following parameters.

  • Python versions 3.11, 3.12, and 3.13

  • Ubuntu, Windows, and macOS operating systems

This creates nine test jobs. Tests must pass across the entire matrix.

Code quality#

Use Ruff for linting and formatting code. Ruff is configured to limit line length to one-hundred characters.

Check for lint errors#

make lint-check

Fix lint errors#

make lint-fix

This project sets unsafe-fixes = true in pyproject.toml, so make lint-fix applies Ruff’s unsafe fixes too, not only the safe ones. Review the diff before committing.

Format code#

make format

Configuration#

Ruff settings are in pyproject.toml under the [tool.ruff] table.

CI enforces the same Ruff version as that used in development.

pre-commit hooks#

pre-commit hooks catch issues before committing, providing feedback faster than waiting for CI. These are installed automatically when creating a development environment. The hooks are defined in .pre-commit-config.yaml.

What runs on every commit#

  • Code lint

  • Code format

  • REUSE compliance validates SPDX license headers

  • File integrity checks:

    • Trailing whitespace removal

    • End-of-file fixer

    • YAML, JSON, and TOML syntax validation

    • Python AST check

    • Case conflict check

    • Merge conflict detection

    • Large file prevention, configured to detect files larger than 1MB

    • Line ending normalization to LF

    • Debug statement detection

  • Commit message validation, enforcing Conventional Commits format

Performance#

All checks complete in under five seconds.

Run pre-commit manually#

You can run pre-commit manually.

Run all hooks on all files.

make pc

pre-commit run ruff --all-files  # Run specific hook

Skip hooks#

For work-in-progress commits, you can skip pre-commit hooks with the following command. Use it sparingly.

git commit --no-verify

Note

pre-commit is optional for contributors. CI enforces the same checks regardless. Core maintainers should use it.

Code style guidelines#

This section describes guidelines for writing code in icalendar-anonymizer.

Docstrings#

Use Google-style docstrings with multi-line format:

def foo(arg1: str, arg2: int) -> str:
    """Brief description on first line.

    More detailed explanation if needed. Can span multiple paragraphs.

    Args:
        arg1: Description of first argument
        arg2: Description of second argument

    Returns:
        Description of return value

    Raises:
        ValueError: When invalid input provided
    """

Include an Examples section only with real, testable doctests.

Test organization#

Use pytest.mark.parametrize for duplicate test patterns:

@pytest.mark.parametrize(
    ("property_name", "expected_value"),
    [
        ("status", "CONFIRMED"),
        ("priority", 1),
    ],
)
def test_preserves_metadata(property_name, expected_value):
    """Test implementation."""

Organize tests into logical groups with clear section comments.

Imports#

  • Standard library imports first

  • Third-party imports second

  • Local imports third

  • Sort alphabetically within each group

# Standard library
import hashlib
from datetime import datetime

# Third-party
from icalendar import Calendar

# Local
from icalendar_anonymizer import anonymize

Line length#

The maximum line length for code is 100 characters maximum. It is enforced by Ruff.

Documentation style guidelines#

This section describes the guidelines for writing documentation for icalendar-anonymizer.

API documentation#

Use autodoc for API function signatures in Sphinx documentation:

.. autofunction:: icalendar_anonymizer.anonymize

This ensures documentation stays in sync with code. Don’t manually copy function signatures.

Code examples#

Use doctest format for Python examples in documentation:

..  doctest::

    >>> from icalendar import Calendar
    >>> from icalendar_anonymizer import anonymize
    >>> # Example code here

This allows examples to be automatically tested for correctness.

Pull request process#

This section describes the guidelines for working with pull requests for icalendar-anonymizer.

Requirements#

The following list of requirements must be satisfied to merge a pull request.

  • At least one approval is required before merge

  • All tests must pass

  • Coverage must be greater than or equal to 90%

  • Pull request title must follow Commit Message Format

  • A change log entry (see Change log), unless the change doesn’t affect users

Title format#

Pull request titles must follow conventional commits because maintainers use squash merge:

feat: add preserve parameter to anonymize function
fix: correct UID uniqueness handling
docs: update installation instructions

The pull request title becomes the commit message on the main branch.

Change log#

See Change log below.

Artificial intelligence policy#

icalendar-anonymizer follows the Python Calendaring Ecosystem’s AI policy. Read it before using AI to help draft a pull request.

Change log#

If your PR changes behavior, add a news fragment. CI-only and internal-refactor PRs don’t need one.

touch news/<issue-number>.<type>.rst

Where <type> is one of: breaking, removal, feature, bugfix, documentation, deps, internal, chore, security.

Write a short, user-facing description of the change inside the file, starting with a past tense verb such as “Added,” “Fixed,” “Removed,” or “Updated.” Use double backticks for inline literals (` ``PROPERTY `), the ``:py:func:/:py:class: roles for Python objects, and the :file: role for file paths. Towncrier appends the issue link automatically from the filename, so don’t add one yourself. For a change with no issue number, name the file +<short-description>.<type>.rst instead.

Fragments are collected into CHANGES.rst at release time. Don’t edit that file directly.

If you used AI to help write the change, briefly disclose it in the fragment, per the Artificial intelligence policy.

To preview what the change log will look like:

towncrier build --draft --version 0.0.0

If you’re unsure whether your PR needs a fragment, ask a maintainer rather than skipping silently.

License and REUSE compliance#

This project follows the REUSE specification for clear licensing.

License#

The project is licensed under AGPL-3.0-or-later.

SPDX headers and REUSE.toml#

All new source files must include SPDX headers. Autogenerated files that cannot have persistent headers may instead rely on entries in REUSE.toml as a fallback.

SPDX headers are required in all source files:

Python files#

# SPDX-FileCopyrightText: 2025 icalendar-anonymizer contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

reStructuredText files#

.. SPDX-FileCopyrightText: 2025 icalendar-anonymizer contributors
.. SPDX-License-Identifier: AGPL-3.0-or-later

Markdown files#

<!--- SPDX-FileCopyrightText: 2025 icalendar-anonymizer contributors -->
<!--- SPDX-License-Identifier: AGPL-3.0-or-later -->

REUSE.toml#

Use REUSE.toml only as a fallback for autogenerated files that cannot reasonably include headers. It must not be treated as a substitute for adding headers to regular source files.

Check compliance#

pre-commit hooks automatically check REUSE compliance. You can also run the check manually:

reuse lint

All files must pass REUSE compliance before merge.

Cloudflare Workers deployment#

The hosted service at https://icalendar-anonymizer.com runs on Cloudflare Workers, using Pyodide to run the same FastAPI app as the pip package. This section is for contributors working on that deployment path. It’s not required knowledge for the library, CLI, or self-hosted web service.

worker.py at the repository root is the Workers entry point (wrangler.jsonc’s main). Before importing the FastAPI app, it sets CLOUDFLARE_WORKERS=true in the environment. src/icalendar_anonymizer/webapp/main.py reads that variable at import time and at request time to:

  • Mount icalendar_anonymizer.webapp.r2.WorkersR2Client instead of the in-memory MockR2Client used everywhere else

  • Skip mounting /static via FastAPI’s StaticFiles (Cloudflare Workers serves static assets itself)

  • Report r2_enabled: true from GET /health

wrangler.jsonc configures the rest of the deployment: an R2 bucket binding (CALENDAR_SHARE_BUCKET) for shareable links, the custom domain route, and an assets.run_worker_first list of paths that must reach the Python worker instead of being served as static files.

build.sh prepares a deployable tree before wrangler deploy runs: it copies the package into python_modules/ (Workers’ Pyodide runtime imports it from there) and copies the static frontend into src/icalendar_anonymizer/webapp/assets/ (the directory wrangler.jsonc’s assets.directory points at). The cloudflare-deploy.yml GitHub Actions workflow runs this on every v* tag push, or manually via workflow_dispatch.

Get help#

  • Check the Issue Tracker

  • Open a new issue for bugs or feature requests

  • For major changes, open an issue for discussion before starting work

Reference#