Skip to main content

aspcompose

Define, group, select, and validate ASP (Answer Set Programming) rule collections. Solver-agnostic: rule text is opaque to the library, so any ASP dialect (clingo, DLV, ...) works. Zero runtime dependencies.

Install

pip install -e .[dev]

Quickstart

A single registry exercising all three composition axes — slots, profiles, and variants — plus a plain depends_on chain:

Group identifiers, slot-filler identifiers, and rule identifiers all use colon-delimited namespaces (base, slot_x:a, consumer:variant_1:r_1) so every string says where it lives:

from aspcompose import Rule, RuleGroup, RuleRegistry, CollectionPlan

# Plain base group: no slot, no profile, no variants.
base = RuleGroup(
    identifier="base",
    rules=(Rule(identifier="base:r_1", text="p(X) :- q(X)."),),
)

# Two interchangeable fillers of slot_x. Each carries a different profile
# so a bulk `add_profile` pulls in exactly one of them.
slot_x_a = RuleGroup(
    identifier="slot_x:a",
    rules=(Rule(identifier="slot_x:a:r_1", text="q(X) :- a(X)."),),
    depends_on=frozenset({"base"}),
    slot="slot_x",
    profiles=frozenset({"profile_a"}),
)
slot_x_b = RuleGroup(
    identifier="slot_x:b",
    rules=(Rule(identifier="slot_x:b:r_1", text="q(X) :- b(X)."),),
    depends_on=frozenset({"base"}),
    slot="slot_x",
    profiles=frozenset({"profile_b"}),
)

# Polymorphic consumer: depends on slot_x (any filler will do), carries
# both profiles so it's included under either, and declares two variants
# that both derive from the shared predicate `r(X)` — so they work the
# same regardless of which slot_x filler is active.
consumer = RuleGroup(
    identifier="consumer",
    rules=(Rule(identifier="consumer:r_1", text="r(X) :- p(X)."),),
    depends_on=frozenset({"base", "slot_x"}),
    profiles=frozenset({"profile_a", "profile_b"}),
    variants={
        "variant_1": (
            Rule(identifier="consumer:variant_1:r_1", text="u(X) :- r(X)."),
        ),
        "variant_2": (
            Rule(identifier="consumer:variant_2:r_1", text="v(X) :- r(X)."),
        ),
    },
)

# Opt-in extension (plain group, no slot or profile). Two rules show the
# `r_1`/`r_2` numbering inside a single group's namespace.
extension = RuleGroup(
    identifier="extension",
    rules=(
        Rule(identifier="extension:r_1", text="t(X) :- r(X)."),
        Rule(identifier="extension:r_2", text="s(X) :- t(X)."),
    ),
    depends_on=frozenset({"consumer"}),
)

registry = RuleRegistry()
registry.register([base, slot_x_a, slot_x_b, consumer, extension])
assert registry.validate() == []

# One plan, two outputs — variants are decided at resolve() time, so the
# same selection produces different rule sets without re-planning.
plan_a = CollectionPlan(registry)
plan_a.add_profile("profile_a")      # includes: slot_x:a, consumer
plan_a.auto_include_deps()           # pulls in concrete deps: base

for rule in plan_a.resolve(variant="variant_1"):
    print(rule.text)
# p(X) :- q(X).          (base)
# q(X) :- a(X).          (slot_x:a, filling slot_x)
# r(X) :- p(X).          (consumer, shared)
# u(X) :- r(X).          (consumer, variant_1 payload)

for rule in plan_a.resolve(variant="variant_2"):
    print(rule.text)
# p(X) :- q(X).
# q(X) :- a(X).
# r(X) :- p(X).
# v(X) :- r(X).          (consumer, variant_2 payload — same plan, new flavor)

# A different profile picks a different slot_x filler, and the extension
# is pulled in explicitly. The same variant_1 still works — variants
# don't care which filler is active, because they only depend on `r(X)`.
plan_b = CollectionPlan(registry)
plan_b.add_profile("profile_b")      # includes: slot_x:b, consumer
plan_b.add_group("extension")        # opt-in, no profile carries it
plan_b.auto_include_deps()

for rule in plan_b.resolve(variant="variant_1"):
    print(rule.text)
# p(X) :- q(X).
# q(X) :- b(X).          (slot_x:b, filling slot_x)
# r(X) :- p(X).
# u(X) :- r(X).          (consumer, variant_1 payload — unchanged)
# t(X) :- r(X).          (extension, rule r_1)
# s(X) :- t(X).          (extension, rule r_2)

What each axis does in this example:

  • slot_x picks which group defines q(X) — slot_x:a or slot_x:b — while consumer depends on slot_x polymorphically, without naming either filler.
  • profile_a / profile_b are bulk toggles: one call to add_profile pulls in a coherent set of groups (here, a slot_x filler and the consumer). The extension group carries no profile and is added with add_group when wanted.
  • variant_1 / variant_2 switch consumer's emitted rule payload at resolve() time. The variants derive from the shared r(X), so the choice is independent of both the profile and the slot filler — plan_a renders cleanly under either variant, and variant_1 works the same in plan_b.
  • auto_include_deps only walks concrete depends_on edges; slot deps are left to the user (or to a profile) to resolve.

Concepts

  • Rule — a single ASP rule with an identifier, source text, and optional docs.
  • RuleGroup — rules that are always added/removed together. Declares depends_on (group identifiers or slot names), may fill a slot, may carry any number of profiles (tags), and may define per-flavor variants. Frozen.
  • Slot — an abstract role that multiple groups can fill (e.g. "input_format", filled by format_sbml or format_kgml). A plan may include at most one filler per slot, and depends_on may name a slot instead of a concrete group — that's a polymorphic dependency on "some filler of this role".
  • Profile — a free-form label carried by any number of groups (e.g. "experimental", "sbml"). plan.add_profile(name) includes every group tagged with it in a single call.
  • Variant — a format/flavor key declared inside a group: variants={"key": (Rule, ...)}. plan.resolve(variant="key") emits rules plus the matching payload, so one plan can produce N programs (one per variant key).
  • RuleRegistry — stores every known group. validate() catches unknown deps and cycles.
  • CollectionPlan — user-curated included / excluded selection. validate() returns structured issues; resolve() returns a flat, ordered rule list or raises PlanInvalidError.

Slots vs profiles vs variants

The three are orthogonal: they act on different axes of the composition problem, and a single group can use all three at once without interference.

Slot Profile Variant
Answers "which group fills this role?" "which groups go together?" "which rule flavor to emit?"
Cardinality at most one filler per slot, per plan any number of tagged groups one variant key per resolve() call
Decided at plan-composition time plan-composition time resolve() time — same plan, N outputs
In depends_on yes — polymorphic dependency no no
Conflict slot_conflict if two fillers are included none missing_variant if the key isn't found

Use cases:

  • Slot — interchangeable implementations of the same interface. An input_format slot filled by format_sbml or format_kgml; a solver_hints slot filled by clingo_hints or dlv_hints. Downstream groups write depends_on={"input_format"} and don't care which filler the user picks.
  • Profile — bulk toggles for cross-cutting themes. Tag a dozen groups scattered across features with "experimental"; flip them all on at once with plan.add_profile("experimental"). Profiles never exclude anything and never conflict — they're a user-ergonomic shortcut, not a composition constraint.
  • Variant — one group, multiple output flavors. A pathways group has shared logic in rules plus a few format-specific tail rules in variants={"sbml": ..., "kgml": ...}. The same plan yields two programs via resolve(variant="sbml") and resolve(variant="kgml") — no re-selection, no duplicate groups, no N×M explosion.

A single group can fill a slot, carry profiles, and define variants all at the same time.

Validation issues

kind Meaning
missing_dependency Included group requires a group that is not included
excluded_dependency Included group requires a group that is explicitly excluded
unfilled_slot Included group needs a slot but no group filling it is in
slot_conflict Two included groups fill the same slot
unknown_dependency A depends_on target is not registered
missing_variant Included group has variants but not for the requested key
cycle Cyclic depends_on graph (registry-level)

Testing

pytest

Metadata

Release files for aspcompose 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aspcompose 0.1.0
File Size Uploaded Provenance Actions
aspcompose-0.1.0.tar.gz 14.1 kB With provenance Details for aspcompose-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for aspcompose 0.1.0
File Interpreter ABI Platform Actions
aspcompose-0.1.0-py3-none-any.whl Python 3 none any Details for aspcompose-0.1.0-py3-none-any.whl

Total release size: 23.6 kB

Release files / aspcompose-0.1.0.tar.gz

Download URL aspcompose-0.1.0.tar.gz
Size 14.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5802658074710ad69e724af0475a7ffa8925006f03fa3d2c0132f80e6071ac0b
BLAKE2b-256 checksum
How to use checksums
5df2999502affea8e5b15a9a5b56f8b8ce1e8d65d01ee190c67b058df66d6649
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 8, 2026.

Transparency log

Release files / aspcompose-0.1.0-py3-none-any.whl

Download URL aspcompose-0.1.0-py3-none-any.whl
Size 9.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7124e6d701566daeb97e5b2acc2b18e5fb4b4a694fda73d5a8202cb0ac87e71e
BLAKE2b-256 checksum
How to use checksums
1d2ee0f23e5924b76f333d27cd3be1f89a6ea66ab85aa1cd3695c712e9858c76
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 8, 2026.

Transparency log

Security for aspcompose 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for aspcompose-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page