Skip to content

API Documentation

slap.application.Application

The application object is the main hub for command-line interactions. It is responsible for managing the project that is the main subject of the command-line invokation (or multiple of such), provide the #cleo command-line application that #ApplicationPlugin#s can register commands to, etc.

Source code in slap/application.py
class Application:
    """The application object is the main hub for command-line interactions. It is responsible for managing the project
    that is the main subject of the command-line invokation (or multiple of such), provide the #cleo command-line
    application that #ApplicationPlugin#s can register commands to, etc."""

    main_project: Once[Project | None]

    #: The application configuration loaded once via #get_application_configuration().
    config: Once[ApplicationConfig]

    #: The cleo application to which new commands can be registered via #ApplicationPlugin#s.
    cleo: CleoApplication

    def __init__(self, directory: Path | None = None, name: str = "slap", version: str = __version__) -> None:
        from slap.util.once import Once

        self._directory = directory or Path.cwd()
        self._repository: t.Optional[Repository] = None
        self._plugins_loaded = False
        self.config = Once(self._get_application_configuration)
        self.cleo = CleoApplication(self._cleo_init, name, version)
        self.main_project = Once(self._get_main_project)

    @property
    def repository(self) -> Repository:
        """Return the Slap repository that is the subject of the current application. There may be command plugins
        that do not require the repository to function, so this property creates the repository lazily."""

        if self._repository is None:
            self._repository = find_repository(self._directory)

        return self._repository

    def _get_application_configuration(self) -> ApplicationConfig:
        """Loads the application-level configuration."""

        from databind.core.settings import ExtraKeys
        from databind.json import load

        raw_config = self.repository.raw_config().get("application", {})
        return load(raw_config, ApplicationConfig, settings=[ExtraKeys(True)])

    def _get_main_project(self) -> Project | None:
        """Returns the main project, which is the one that the current working directory is pointing to."""

        cwd = Path.cwd()

        for project in self.repository.projects():
            path = project.directory.resolve()
            if path == cwd:
                return project

        return None

    def configurations(self, targets_only: bool = False) -> list[Configuration]:
        """Return a list of all configuration objects, i.e. all projects and eventually the #Repository, unless one
        project is from the same directory as the repository."""

        result: list[Configuration] = list(self.get_target_projects() if targets_only else self.repository.projects())
        if self.repository.directory not in tuple(p.directory for p in self.repository.projects()):
            result.insert(0, self.repository)
        return result

    def load_plugins(self) -> None:
        """Loads all application plugins (see #ApplicationPlugin) and activates them.

        By default, all plugins available in the `slap.application.ApplicationPlugin` entry point group are loaded. This
        behaviour can be modified by setting either the `[tool.slap.plugins.disable]` or `[tool.slap.plugins.enable]`
        configuration option (without the `tool.slap` prefix in case of a `slap.toml` configuration file). The default
        plugins delivered immediately with Slap are enabled by default unless disabled explicitly with the `disable`
        option."""

        from slap.plugins import ApplicationPlugin
        from slap.util.plugins import iter_entrypoints

        assert not self._plugins_loaded
        self._plugins_loaded = True

        config = self.config()
        disable = config.disable or []

        logger.debug("Loading application plugins")

        for plugin_name, loader in iter_entrypoints(ApplicationPlugin):  # type: ignore[type-abstract]
            if plugin_name in disable:
                continue
            try:
                plugin = loader()(self)
            except Exception:
                logger.exception("Could not load plugin <subj>%s</subj> due to an exception", plugin_name)
            else:
                plugin_config = plugin.load_configuration(self)
                plugin.activate(self, plugin_config)

    def _cleo_init(self, io: IO) -> None:
        self.load_plugins()

    def run(self) -> None:
        """Loads and activates application plugins and then invokes the CLI."""

        self.cleo.run()

    def get_target_projects(
        self, only_projects: str | t.Sequence[str] | None = None, cwd: Path | None = None
    ) -> list[Project]:
        """
        Returns the list of projects that should be dealt with when executing a command. When there is a main project,
        only the main project will be returned. When in the repository root, all projects will be returned.
        """

        cwd = cwd or self._directory
        if isinstance(only_projects, str):
            only_projects = split_by_commata(only_projects)

        if only_projects is not None:
            projects: list[Project] = []
            for only_project in only_projects:
                project_path = (cwd / only_project).resolve()
                matching_projects = [p for p in self.repository.projects() if p.directory.resolve() == project_path]
                if not matching_projects:
                    raise ValueError(f'error: "{only_project}" does not point to a project')
                projects += matching_projects
            return projects

        main = self.main_project()
        if main:
            return [main]
        if cwd == self.repository.directory:
            return self.repository.get_projects_ordered()
        return []

repository property

repository: Repository

Return the Slap repository that is the subject of the current application. There may be command plugins that do not require the repository to function, so this property creates the repository lazily.

configurations

configurations(targets_only: bool = False) -> list[Configuration]

Return a list of all configuration objects, i.e. all projects and eventually the #Repository, unless one project is from the same directory as the repository.

Source code in slap/application.py
def configurations(self, targets_only: bool = False) -> list[Configuration]:
    """Return a list of all configuration objects, i.e. all projects and eventually the #Repository, unless one
    project is from the same directory as the repository."""

    result: list[Configuration] = list(self.get_target_projects() if targets_only else self.repository.projects())
    if self.repository.directory not in tuple(p.directory for p in self.repository.projects()):
        result.insert(0, self.repository)
    return result

get_target_projects

get_target_projects(only_projects: str | Sequence[str] | None = None, cwd: Path | None = None) -> list[Project]

Returns the list of projects that should be dealt with when executing a command. When there is a main project, only the main project will be returned. When in the repository root, all projects will be returned.

Source code in slap/application.py
def get_target_projects(
    self, only_projects: str | t.Sequence[str] | None = None, cwd: Path | None = None
) -> list[Project]:
    """
    Returns the list of projects that should be dealt with when executing a command. When there is a main project,
    only the main project will be returned. When in the repository root, all projects will be returned.
    """

    cwd = cwd or self._directory
    if isinstance(only_projects, str):
        only_projects = split_by_commata(only_projects)

    if only_projects is not None:
        projects: list[Project] = []
        for only_project in only_projects:
            project_path = (cwd / only_project).resolve()
            matching_projects = [p for p in self.repository.projects() if p.directory.resolve() == project_path]
            if not matching_projects:
                raise ValueError(f'error: "{only_project}" does not point to a project')
            projects += matching_projects
        return projects

    main = self.main_project()
    if main:
        return [main]
    if cwd == self.repository.directory:
        return self.repository.get_projects_ordered()
    return []

load_plugins

load_plugins() -> None

Loads all application plugins (see #ApplicationPlugin) and activates them.

By default, all plugins available in the slap.application.ApplicationPlugin entry point group are loaded. This behaviour can be modified by setting either the [tool.slap.plugins.disable] or [tool.slap.plugins.enable] configuration option (without the tool.slap prefix in case of a slap.toml configuration file). The default plugins delivered immediately with Slap are enabled by default unless disabled explicitly with the disable option.

Source code in slap/application.py
def load_plugins(self) -> None:
    """Loads all application plugins (see #ApplicationPlugin) and activates them.

    By default, all plugins available in the `slap.application.ApplicationPlugin` entry point group are loaded. This
    behaviour can be modified by setting either the `[tool.slap.plugins.disable]` or `[tool.slap.plugins.enable]`
    configuration option (without the `tool.slap` prefix in case of a `slap.toml` configuration file). The default
    plugins delivered immediately with Slap are enabled by default unless disabled explicitly with the `disable`
    option."""

    from slap.plugins import ApplicationPlugin
    from slap.util.plugins import iter_entrypoints

    assert not self._plugins_loaded
    self._plugins_loaded = True

    config = self.config()
    disable = config.disable or []

    logger.debug("Loading application plugins")

    for plugin_name, loader in iter_entrypoints(ApplicationPlugin):  # type: ignore[type-abstract]
        if plugin_name in disable:
            continue
        try:
            plugin = loader()(self)
        except Exception:
            logger.exception("Could not load plugin <subj>%s</subj> due to an exception", plugin_name)
        else:
            plugin_config = plugin.load_configuration(self)
            plugin.activate(self, plugin_config)

run

run() -> None

Loads and activates application plugins and then invokes the CLI.

Source code in slap/application.py
def run(self) -> None:
    """Loads and activates application plugins and then invokes the CLI."""

    self.cleo.run()

slap.plugins

ApplicationPlugin

Bases: Generic[T], ABC

A plugin that is activated on application load, usually used to register additional CLI commands.

Source code in slap/plugins.py
class ApplicationPlugin(t.Generic[T], abc.ABC):
    """A plugin that is activated on application load, usually used to register additional CLI commands."""

    ENTRYPOINT = "slap.plugins.application"

    def __init__(self, app: Application) -> None:
        pass

    @abc.abstractmethod
    def load_configuration(self, app: Application) -> T:
        """Load the configuration of the plugin. Usually, plugins will want to read the configuration from the Slap
        configuration, which is either loaded from `pyproject.toml` or `slap.toml`. Use #Application.raw_config
        to access the Slap configuration."""

    @abc.abstractmethod
    def activate(self, app: Application, config: T) -> None:
        """Activate the plugin. Register a #Command to #Application.cleo or another type of plugin to
        the #Application.plugins registry."""
activate abstractmethod
activate(app: Application, config: T) -> None

Activate the plugin. Register a #Command to #Application.cleo or another type of plugin to the #Application.plugins registry.

Source code in slap/plugins.py
@abc.abstractmethod
def activate(self, app: Application, config: T) -> None:
    """Activate the plugin. Register a #Command to #Application.cleo or another type of plugin to
    the #Application.plugins registry."""
load_configuration abstractmethod
load_configuration(app: Application) -> T

Load the configuration of the plugin. Usually, plugins will want to read the configuration from the Slap configuration, which is either loaded from pyproject.toml or slap.toml. Use #Application.raw_config to access the Slap configuration.

Source code in slap/plugins.py
@abc.abstractmethod
def load_configuration(self, app: Application) -> T:
    """Load the configuration of the plugin. Usually, plugins will want to read the configuration from the Slap
    configuration, which is either loaded from `pyproject.toml` or `slap.toml`. Use #Application.raw_config
    to access the Slap configuration."""

CheckPlugin

Bases: ABC

This plugin type can be implemented to add custom checks to the slap check command. Note that checks will be grouped and their names prefixed with the plugin name, so that name does not need to be included in the name of the returned checks.

Source code in slap/plugins.py
class CheckPlugin(abc.ABC):
    """This plugin type can be implemented to add custom checks to the `slap check` command. Note that checks will
    be grouped and their names prefixed with the plugin name, so that name does not need to be included in the name
    of the returned checks."""

    ENTRYPOINT = "slap.plugins.check"

    def get_project_checks(self, project: Project) -> t.Iterable[Check]:
        return []

    def get_application_checks(self, app: Application) -> t.Iterable[Check]:
        return []

ProjectHandlerPlugin

Bases: ABC

A plugin that implements the core functionality of a project. Project handlers are intermediate layers between the Slap tooling and the actual project configuration, allowing different types of configurations to be adapted and used with Slap.

Source code in slap/plugins.py
class ProjectHandlerPlugin(abc.ABC):
    """A plugin that implements the core functionality of a project. Project handlers are intermediate layers between
    the Slap tooling and the actual project configuration, allowing different types of configurations to be adapted and
    used with Slap."""

    ENTRYPOINT = "slap.plugins.project"

    @abc.abstractmethod
    def matches_project(self, project: Project) -> bool:
        """Return `True` if the handler is able to provide data for the given project."""

    @abc.abstractmethod
    def get_dist_name(self, project: Project) -> str | None:
        """Return the distribution name for the project."""

    @abc.abstractmethod
    def get_readme(self, project: Project) -> str | None:
        """Return the readme file configured for the project."""

    @abc.abstractmethod
    def get_packages(self, project: Project) -> list[Package] | None:
        """Return a list of packages for the project. Return `None` to indicate that the project is expected to
        not contain any packages."""

    @abc.abstractmethod
    def get_dependencies(self, project: Project) -> Dependencies:
        """Return the dependencies of the project."""

    def get_version(self, project: Project) -> str | None:
        """Return the main project version string."""

        ref = next(iter(self.get_version_refs(project)), None)
        return ref.value if ref else None

    def get_version_refs(self, project: Project) -> list[VersionRef]:
        """Allows the project handler to return additional version refs. Usually returns the version reference in
        `pyproject.toml`."""

        return []

    def add_dependency(self, project: Project, dependency: Dependency, where: str) -> None:
        """Add a dependency to the project configuration.

        Arguments:
          project: The project to update.
          dependency: The dependency to add.
          where: The location of where to add the dependency. This is either `'run'`, `'dev'`, or otherwise
            refers to the name of an extra requirement.
        Raises:
            NotImplementedError: If the operation is not supported by the project handler.
        """

        raise NotImplementedError
add_dependency
add_dependency(project: Project, dependency: Dependency, where: str) -> None

Add a dependency to the project configuration.

Parameters:

Name Type Description Default
project Project

The project to update.

required
dependency Dependency

The dependency to add.

required
where str

The location of where to add the dependency. This is either 'run', 'dev', or otherwise refers to the name of an extra requirement.

required

Raises: NotImplementedError: If the operation is not supported by the project handler.

Source code in slap/plugins.py
def add_dependency(self, project: Project, dependency: Dependency, where: str) -> None:
    """Add a dependency to the project configuration.

    Arguments:
      project: The project to update.
      dependency: The dependency to add.
      where: The location of where to add the dependency. This is either `'run'`, `'dev'`, or otherwise
        refers to the name of an extra requirement.
    Raises:
        NotImplementedError: If the operation is not supported by the project handler.
    """

    raise NotImplementedError
get_dependencies abstractmethod
get_dependencies(project: Project) -> Dependencies

Return the dependencies of the project.

Source code in slap/plugins.py
@abc.abstractmethod
def get_dependencies(self, project: Project) -> Dependencies:
    """Return the dependencies of the project."""
get_dist_name abstractmethod
get_dist_name(project: Project) -> str | None

Return the distribution name for the project.

Source code in slap/plugins.py
@abc.abstractmethod
def get_dist_name(self, project: Project) -> str | None:
    """Return the distribution name for the project."""
get_packages abstractmethod
get_packages(project: Project) -> list[Package] | None

Return a list of packages for the project. Return None to indicate that the project is expected to not contain any packages.

Source code in slap/plugins.py
@abc.abstractmethod
def get_packages(self, project: Project) -> list[Package] | None:
    """Return a list of packages for the project. Return `None` to indicate that the project is expected to
    not contain any packages."""
get_readme abstractmethod
get_readme(project: Project) -> str | None

Return the readme file configured for the project.

Source code in slap/plugins.py
@abc.abstractmethod
def get_readme(self, project: Project) -> str | None:
    """Return the readme file configured for the project."""
get_version
get_version(project: Project) -> str | None

Return the main project version string.

Source code in slap/plugins.py
def get_version(self, project: Project) -> str | None:
    """Return the main project version string."""

    ref = next(iter(self.get_version_refs(project)), None)
    return ref.value if ref else None
get_version_refs
get_version_refs(project: Project) -> list[VersionRef]

Allows the project handler to return additional version refs. Usually returns the version reference in pyproject.toml.

Source code in slap/plugins.py
def get_version_refs(self, project: Project) -> list[VersionRef]:
    """Allows the project handler to return additional version refs. Usually returns the version reference in
    `pyproject.toml`."""

    return []
matches_project abstractmethod
matches_project(project: Project) -> bool

Return True if the handler is able to provide data for the given project.

Source code in slap/plugins.py
@abc.abstractmethod
def matches_project(self, project: Project) -> bool:
    """Return `True` if the handler is able to provide data for the given project."""

ReleasePlugin

Bases: ABC

This plugin type provides additional references to the project's version number allowing slap release to update these references to a new version number.

Source code in slap/plugins.py
class ReleasePlugin(abc.ABC):
    """This plugin type provides additional references to the project's version number allowing `slap release` to
    update these references to a new version number.
    """

    ENTRYPOINT = "slap.plugins.release"

    app: Application
    io: IO

    def get_version_refs(self, project: Project) -> list[VersionRef]:
        """Return a list of occurrences of the project version."""

        return []

    def create_release(
        self, repository: Repository, project: Project | None, target_version: str, dry: bool
    ) -> t.Sequence[Path]:
        """Gives the plugin a chance to perform an arbitrary action after all version references have been bumped,
        being informed of the target version. If *dry* is `True`, the plugin should only act as if it was performing
        its usual actions but not commit the changes to disk. It should return the list of files that it modifies
        or would have modified."""

        return []
create_release
create_release(repository: Repository, project: Project | None, target_version: str, dry: bool) -> Sequence[Path]

Gives the plugin a chance to perform an arbitrary action after all version references have been bumped, being informed of the target version. If dry is True, the plugin should only act as if it was performing its usual actions but not commit the changes to disk. It should return the list of files that it modifies or would have modified.

Source code in slap/plugins.py
def create_release(
    self, repository: Repository, project: Project | None, target_version: str, dry: bool
) -> t.Sequence[Path]:
    """Gives the plugin a chance to perform an arbitrary action after all version references have been bumped,
    being informed of the target version. If *dry* is `True`, the plugin should only act as if it was performing
    its usual actions but not commit the changes to disk. It should return the list of files that it modifies
    or would have modified."""

    return []
get_version_refs
get_version_refs(project: Project) -> list[VersionRef]

Return a list of occurrences of the project version.

Source code in slap/plugins.py
def get_version_refs(self, project: Project) -> list[VersionRef]:
    """Return a list of occurrences of the project version."""

    return []

RepositoryCIPlugin

Bases: ABC

This plugin type can be used with the slap changelog update-pr -use <plugin_name> option. It provides all the details derivable from the environment (e.g. environment variables available from CI builds) that can be used to detect which changelog entries have been added in a pull request, the pull request URL and the means to publish the changes back to the original repository.

Source code in slap/plugins.py
class RepositoryCIPlugin(abc.ABC):
    """This plugin type can be used with the `slap changelog update-pr -use <plugin_name>` option. It provides all the
    details derivable from the environment (e.g. environment variables available from CI builds) that can be used to
    detect which changelog entries have been added in a pull request, the pull request URL and the means to publish
    the changes back to the original repository.
    """

    ENTRYPOINT = "slap.plugins.repository_ci"

    io: IO

    @abc.abstractmethod
    def initialize(self) -> None: ...

    @abc.abstractmethod
    def get_base_ref(self) -> str: ...

    def get_head_ref(self) -> str | None:
        return None

    @abc.abstractmethod
    def get_pr(self) -> str: ...

    @abc.abstractmethod
    def publish_changes(self, changed_files: list[Path], commit_message: str) -> None: ...

    @staticmethod
    def all() -> dict[str, t.Callable[[], RepositoryCIPlugin]]:
        """Iterates over all registered automation plugins and returns a dictionary that maps
        the plugin name to a factory function."""

        from slap.util.plugins import iter_entrypoints

        result: dict[str, t.Callable[[], RepositoryCIPlugin]] = {}
        for ep in iter_entrypoints(RepositoryCIPlugin.ENTRYPOINT):
            result[ep.name] = partial(lambda ep: ep.load()(), ep)

        return result

    @staticmethod
    def get(plugin_name: str, io: IO) -> RepositoryCIPlugin:
        """Returns an instance of the plugin with given name, fully initialized.

        Raises a #ValueError if the plugin does not exist."""

        plugins = RepositoryCIPlugin.all()
        if plugin_name not in plugins:
            raise ValueError(f"plugin {RepositoryCIPlugin.ENTRYPOINT}:{plugin_name} does not exist")

        plugin = plugins[plugin_name]()
        plugin.io = io
        plugin.initialize()
        return plugin
all staticmethod
all() -> dict[str, Callable[[], RepositoryCIPlugin]]

Iterates over all registered automation plugins and returns a dictionary that maps the plugin name to a factory function.

Source code in slap/plugins.py
@staticmethod
def all() -> dict[str, t.Callable[[], RepositoryCIPlugin]]:
    """Iterates over all registered automation plugins and returns a dictionary that maps
    the plugin name to a factory function."""

    from slap.util.plugins import iter_entrypoints

    result: dict[str, t.Callable[[], RepositoryCIPlugin]] = {}
    for ep in iter_entrypoints(RepositoryCIPlugin.ENTRYPOINT):
        result[ep.name] = partial(lambda ep: ep.load()(), ep)

    return result
get staticmethod
get(plugin_name: str, io: IO) -> RepositoryCIPlugin

Returns an instance of the plugin with given name, fully initialized.

Raises a #ValueError if the plugin does not exist.

Source code in slap/plugins.py
@staticmethod
def get(plugin_name: str, io: IO) -> RepositoryCIPlugin:
    """Returns an instance of the plugin with given name, fully initialized.

    Raises a #ValueError if the plugin does not exist."""

    plugins = RepositoryCIPlugin.all()
    if plugin_name not in plugins:
        raise ValueError(f"plugin {RepositoryCIPlugin.ENTRYPOINT}:{plugin_name} does not exist")

    plugin = plugins[plugin_name]()
    plugin.io = io
    plugin.initialize()
    return plugin

RepositoryHandlerPlugin

Bases: ABC

A plugin to provide data and operations on a repository level.

Source code in slap/plugins.py
class RepositoryHandlerPlugin(abc.ABC):
    """A plugin to provide data and operations on a repository level."""

    ENTRYPOINT = "slap.plugins.repository"

    @abc.abstractmethod
    def matches_repository(self, repository: Repository) -> bool:
        """Return `True` if the handler is able to provide data for the given project."""

    @abc.abstractmethod
    def get_vcs(self, repository: Repository) -> Vcs | None:
        """Return the version control system that the repository is managed with."""

    @abc.abstractmethod
    def get_repository_host(self, repository: Repository) -> RepositoryHost | None:
        """Return the interface for interacting with the VCS hosting service."""

    @abc.abstractmethod
    def get_projects(self, repository: Repository) -> list[Project]:
        """Return the projects of this repository."""
get_projects abstractmethod
get_projects(repository: Repository) -> list[Project]

Return the projects of this repository.

Source code in slap/plugins.py
@abc.abstractmethod
def get_projects(self, repository: Repository) -> list[Project]:
    """Return the projects of this repository."""
get_repository_host abstractmethod
get_repository_host(repository: Repository) -> RepositoryHost | None

Return the interface for interacting with the VCS hosting service.

Source code in slap/plugins.py
@abc.abstractmethod
def get_repository_host(self, repository: Repository) -> RepositoryHost | None:
    """Return the interface for interacting with the VCS hosting service."""
get_vcs abstractmethod
get_vcs(repository: Repository) -> Vcs | None

Return the version control system that the repository is managed with.

Source code in slap/plugins.py
@abc.abstractmethod
def get_vcs(self, repository: Repository) -> Vcs | None:
    """Return the version control system that the repository is managed with."""
matches_repository abstractmethod
matches_repository(repository: Repository) -> bool

Return True if the handler is able to provide data for the given project.

Source code in slap/plugins.py
@abc.abstractmethod
def matches_repository(self, repository: Repository) -> bool:
    """Return `True` if the handler is able to provide data for the given project."""

VersionIncrementingRulePlugin

Bases: ABC

This plugin type can be implemented to provide rules accepted by the slap release <rule> command to "bump" an existing version number to another. The builtin rules implemented in #slap.ext.version_incrementing_rules.

Source code in slap/plugins.py
class VersionIncrementingRulePlugin(abc.ABC):
    """This plugin type can be implemented to provide rules accepted by the `slap release <rule>` command to "bump" an
    existing version number to another. The builtin rules implemented in #slap.ext.version_incrementing_rules.
    """

    ENTRYPOINT = "slap.plugins.version_incrementing_rule"

    @abc.abstractmethod
    def increment_version(self, version: Version) -> Version: ...