Plugin system

pypi-profile uses pluggy to discover installed profile packages. This page explains how that works.

What pluggy is

pluggy is a plugin framework built by the pytest team. The central idea is:

  • The host application (here: pypi-profile) defines a set of hook specs — function signatures that plugins may implement.
  • Plugin packages implement those functions and register themselves via a Python entry point.
  • The framework discovers all registered plugins at runtime and calls them when the host asks.

You do not need to know how pluggy works internally to write a plugin. The pattern is simple: implement a function with the right name, add an @hookimpl decorator, and register it.

Hook specification

The spec lives in pypi_profile/plugin_spec.py:

import pluggy

hookspec = pluggy.HookspecMarker("pypi_profile")
hookimpl = pluggy.HookimplMarker("pypi_profile")

class PypiProfileSpec:
    @hookspec
    def get_profile_data(self) -> dict:
        """Return a dict of profile data contributed by this plugin."""

hookspec marks functions that plugins may implement. hookimpl marks the implementations in plugin modules. Both markers use the string "pypi_profile" as a namespace so they do not collide with other pluggy-based tools.

Currently there is one hook: get_profile_data(). Its return value is informational — plugins can return a dict, but the primary mechanism for profile data is the bundled pypi_profile.toml file described below.

Plugin discovery

pypi_profile/plugin_manager.py builds the plugin manager at startup:

import importlib.metadata
import pluggy
from pypi_profile.plugin_spec import PypiProfileSpec

def build_plugin_manager() -> pluggy.PluginManager:
    pm = pluggy.PluginManager("pypi_profile")
    pm.add_hookspecs(PypiProfileSpec)
    for ep in importlib.metadata.entry_points(group="pypi_profile.plugins"):
        plugin_module = ep.load()
        pm.register(plugin_module)
    return pm

importlib.metadata.entry_points is a Python standard library function that reads the dist-info metadata of every installed package in the current environment and returns all entry points that belong to the group "pypi_profile.plugins". This is how pytest discovers test plugins, how Babel discovers extension packages, and how many other Python tools work.

How profile packages are discovered for the hub

Beyond the pluggy hook, loader.py's discover_installed_profiles() function finds installed profiles for the hub (/profiles listing and serve with no source argument). It uses two methods:

Method 1: Entry point registration (preferred)

for entry_point in meta.entry_points(group="pypi_profile.plugins"):
    module_name = entry_point.value.partition(":")[0]
    candidate = Path(str(resources.files(module_name).joinpath("pypi_profile.toml")))
    if candidate.exists():
        seen_paths[candidate.resolve()] = entry_point.dist.name

resources.files(module_name) is the standard way to access package data files without caring where on disk the package is installed. It works with regular installs, editable installs, and zipimport.

Method 2: Distribution scan (fallback)

for dist in meta.distributions():
    for package_file in dist.files or []:
        if package_file.name == "pypi_profile.toml":
            candidate = Path(str(dist.locate_file(package_file)))
            if candidate.exists():
                seen_paths.setdefault(candidate.resolve(), dist.metadata.get("Name", ...))

This scans every installed package for a file literally named pypi_profile.toml. It is slower but catches packages that ship the TOML without registering an entry point.

What a minimal plugin package looks like

The john_doe package in this repo is the canonical example:

john_doe/
├── pyproject.toml
├── john_doe/
│   ├── __init__.py        ← implements get_profile_data() with @hookimpl
│   ├── __about__.py       ← version string
│   ├── py.typed           ← marker file for type checkers
│   └── pypi_profile.toml  ← bundled profile data
└── tests/
    └── test_plugin.py

john_doe/__init__.py:

from pypi_profile.plugin_spec import hookimpl

@hookimpl
def get_profile_data() -> dict:
    return {
        "author": "John Doe",
        "pypi_username": "john_doe",
        "github": "https://github.com/john_doe",
    }

john_doe/pyproject.toml (relevant sections):

[project]
name = "john-doe"
dependencies = ["pypi-profile>=0.1.0"]

[project.entry-points."pypi_profile.plugins"]
john_doe = "john_doe"

[tool.hatch.build.targets.wheel]
include = [
    "john_doe/**/*.py",
    "john_doe/py.typed",
    "john_doe/pypi_profile.toml",
    "/README.md",
    "LICENSE",
]

The critical piece is the entry point:

[project.entry-points."pypi_profile.plugins"]
john_doe = "john_doe"

This says: "when the group pypi_profile.plugins is queried, I provide a plugin called john_doe implemented by the module john_doe." When you pip install john-doe, Python records this in the package's dist-info/entry_points.txt and it becomes visible to importlib.metadata.

What the TOML file inside the package does

The pypi_profile.toml file bundled inside the wheel is what actually provides profile data to the hub and to commands like pypi-profile serve john-doe. When you run:

pypi-profile serve john-doe

loader.find_profile("john-doe") calls meta.distribution("john-doe"), then dist.locate_file("pypi_profile.toml") to get the path to the bundled TOML, and load_profile(that_path) to parse it.

For this to work, pypi_profile.toml must be included in the built wheel. With hatchling, add it to the include list in [tool.hatch.build.targets.wheel].

The py.typed marker

py.typed is an empty file that tells type checkers (mypy, pyright) that the package contains type annotations. Without it, type checkers ignore the package's type information. Include it in your wheel alongside the .py files.

Coexistence of multiple plugins

Multiple profile packages can be installed at the same time. The hub lists all of them at /profiles. Each profile is independent — they do not share data, they do not conflict, and installing one does not break another.

The serve command without a source argument starts a hub that shows all of them:

pypi-profile serve

The command with a source argument serves only one:

pypi-profile serve john-doe
pypi-profile serve matthewdeanmartin