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_xpicks which group definesq(X)—slot_x:aorslot_x:b— whileconsumerdepends onslot_xpolymorphically, without naming either filler.profile_a/profile_bare bulk toggles: one call toadd_profilepulls in a coherent set of groups (here, aslot_xfiller and theconsumer). Theextensiongroup carries no profile and is added withadd_groupwhen wanted.variant_1/variant_2switchconsumer's emitted rule payload atresolve()time. The variants derive from the sharedr(X), so the choice is independent of both the profile and the slot filler —plan_arenders cleanly under either variant, andvariant_1works the same inplan_b.auto_include_depsonly walks concretedepends_onedges; 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. Declaresdepends_on(group identifiers or slot names), may fill aslot, may carry any number ofprofiles(tags), and may define per-flavorvariants. Frozen.- Slot — an abstract role that multiple groups can fill (e.g.
"input_format", filled byformat_sbmlorformat_kgml). A plan may include at most one filler per slot, anddepends_onmay 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")emitsrulesplus 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-curatedincluded/excludedselection.validate()returns structured issues;resolve()returns a flat, ordered rule list or raisesPlanInvalidError.
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_formatslot filled byformat_sbmlorformat_kgml; asolver_hintsslot filled byclingo_hintsordlv_hints. Downstream groups writedepends_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 withplan.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
pathwaysgroup has shared logic inrulesplus a few format-specific tail rules invariants={"sbml": ..., "kgml": ...}. The same plan yields two programs viaresolve(variant="sbml")andresolve(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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.