Skip to content

Dependency Selection

Pyra supports three dependency scopes from pyproject.toml and provides granular control over which scopes are included during sync.

Declared in [project].dependencies:

[project]
dependencies = [
"requests>=2.31",
"rich>=13",
]

Base dependencies are always included by default. Internally, Pyra represents them through a synthetic group called pyra-default. This is an implementation detail — you never interact with pyra-default directly.

Declared in [dependency-groups]:

[dependency-groups]
dev = ["pytest>=8", "mypy>=1.11"]
docs = ["sphinx>=7"]

Groups model development, testing, documentation, or other workflow dependencies that are separate from your package’s runtime requirements.

Declared in [project.optional-dependencies]:

[project.optional-dependencies]
performance = ["uvloop>=0.19"]
socks = ["httpx[socks]"]

Extras represent optional runtime capabilities that consumers of your package can opt into.

When you run pyra sync without flags, Pyra selects:

  • Included: Base dependencies (pyra-default)
  • Included: dev group (if it exists)
  • Excluded: Apps and extras
  • Excluded: Other groups
FlagEffect
--group <name>Add a group to the default selection
--extra <name>Add an extra
--all-groupsAdd all groups
--all-extrasAdd all extras
FlagEffect
--no-group <name>Remove a group after inclusions
--no-devRemove dev after inclusions
FlagEffect
--only-group <name>Select only named groups, exclude base dependencies
--only-devSelect only dev, exclude base dependencies

Exclusions always win over inclusions.

If you write --all-groups --no-group docs, the docs group is excluded even though --all-groups nominally includes it.

--only-group and --only-dev are stronger exclusions — they remove base dependencies entirely and select only the specified groups.

Group and extra names are compared using normalized form:

  • Case-insensitive
  • -, _, and . all normalize to -
  • Original names are preserved for display

Pyra rejects duplicate group names after normalization. If your pyproject.toml declares both my-group and my_group, Pyra will refuse to proceed.

[dependency-groups] supports { include-group = "..." } directives:

[dependency-groups]
dev = ["pytest>=8", { include-group = "typing" }]
typing = ["mypy>=1.11", "pyright>=1.1"]

Rules:

  • Includes are expanded eagerly into one requirement list per group
  • Include cycles are rejected
  • Missing include targets are rejected
  • Repeated requirements from includes are preserved (no silent deduplication)

The same package can appear in multiple scopes:

  • In base dependencies and a group
  • In multiple groups
  • In extras and groups

This is valid. Resolution solves one union of all scopes, and installation uses markers to select the appropriate subset.

Pyra currently resolves all scopes (base, all groups, all extras) as one combined graph. This means:

  • Groups and extras that are independently valid may still fail if they require incompatible versions when solved together
  • This is stricter than the ideal model, but keeps the resolver and lock simple

See Sync Model for how this fits into the full pipeline.