Describing Conda Objects#
The libmambapy.specs
submodule contains object to describe abstraction in the Conda ecosystem.
They are purely functional and do not have any observable impact on the user system.
For instance Channel
is used to describe a channel but does not download any file.
CondaURL#
The CondaURL
is a rich URL object that has additional capabilities for dealing with tokens,
platforms, and packages.
To parse a string into a CondaURL
use CondaURL.parse
as follows:
import libmambapy.specs as specs
url = specs.CondaURL.parse(
"https://conda.anaconda.org/t/someprivatetoken/conda-forge/linux-64/x264-1%21164.3095-h166bdaf_2.tar.bz2"
)
assert url.host() == "conda.anaconda.org"
assert url.package() == "x264-1!164.3095-h166bdaf_2.tar.bz2"
assert url.package(decode=False) == "x264-1%21164.3095-h166bdaf_2.tar.bz2"
The CondaURL.parse
method assumes that the URL is
properly percent encoded.
For instance, here the character !
in the file name x264-1!164.3095-h166bdaf_2.tar.bz2
had
to be replaced with %21
.
The getter functions, such as CondaURL.package
automatically decoded it for us, but we can specify decode=False
to keep the raw representation.
The setters follow the same logic, as described bellow.
import libmambapy.specs as specs
url = specs.CondaURL()
url.set_host("mamba.pm")
url.set_user("my%20name", encode=False)
url.set_password("nd!3gfsd")
assert url.user() == "my name"
assert url.user(decode=False) == "my%20name"
assert url.password() == "nd!3gfsd"
assert url.password(decode=False) == "n%26%234d%213gfsd"
Path manipulation is handled automatically, either with
CondaURL.append_path
or the /
operator.
import libmambapy.specs as specs
url1 = specs.CondaURL.parse("mamba.pm")
url2 = url / "/t/xy-12345678-1234/conda-forge/linux-64"
assert url1.path() == "/"
assert url2.path() == "/t/xy-12345678-1234/conda-forge/linux-64"
assert url2.path_without_token() == "/conda-forge/linux-64"
You can always assume that the paths returned will start with a leading /
.
As always, encoding and decoding options are available.
Note
Contrary to pathlib.Path
, the /
operator will always append, even when the sub-path
starts with a /
.
The function CondaURL.str
can be used to get a raw
representation of the string. By default, it will hide all credentials
import libmambapy.specs as specs
url = specs.CondaURL.parse("mamba.pm/conda-forge")
url.set_user("user@mail.com")
url.set_password("private")
url.set_token("xy-12345678-1234")
assert url.str() == "https://user%40mail.com:*****@mamba.pm/t/*****"
assert (
url.str(credentials="Show")
== "https://user%40mail.com:private@mamba.pm/t/xy-12345678-1234"
)
assert url.str(credentials="Remove") == "https://mamba.pm/"
Similarily the CondaURL.pretty_str
returns a more
user-friendly string, but that may not be parsed back.
UnresolvedChannel#
A UnresolvedChannel
is a lightweight object to represent a channel string, as in passed in
the CLI or configuration.
Since channels rely heavily on configuration options, this type can be used as a placeholder for a
channel that has not been fully “resolved” to a specific location.
It does minimal parsing and can detect the type of ressource (an unresolved name, a URL, a file)
and the platform filters.
import libmambapy.specs as specs
uc = specs.UnresolvedChannel.parse("https://conda.anaconda.org/conda-forge/linux-64")
assert uc.location == "https://conda.anaconda.org/conda-forge"
assert uc.platform_filters == {"linux-64"}
assert uc.type == specs.UnresolvedChannel.Type.URL
Dynamic platforms (as in not known by Mamba) can only be detected with the []
syntax.
import libmambapy.specs as specs
uc = specs.UnresolvedChannel.parse("conda-forge[prius-avx42]")
assert uc.location == "conda-forge"
assert uc.platform_filters == {"prius-avx42"}
assert uc.type == specs.UnresolvedChannel.Type.Name
Channel#
The Channel
are represented by a CondaURL
and a set of platform filters.
A display name is also available, but is not considered a stable identifiaction form of the
channel, since it depends on the many configuration parameters, such as the channel alias.
We construct a Channel
by resolving a UnresolvedChannel
.
All parameters that influence this resolution must be provided explicitly.
import libmambapy.specs as specs
uc = specs.UnresolvedChannel.parse("conda-forge[prius-avx42]")
chan, *_ = specs.Channel.resolve(
uc,
channel_alias="https://repo.mamba.pm"
# ...
)
assert chan.url.str() == "https://repo.mamba.pm/conda-forge"
assert chan.platforms == {"prius-avx42"}
assert chan.display_name == "conda-forge[prius-avx42]"
There are no hard-coded names:
import libmambapy.specs as specs
uc = specs.UnresolvedChannel.parse("defaults")
chan, *_ = specs.Channel.resolve(
uc,
channel_alias="https://repo.mamba.pm"
# ...
)
assert chan.url.str() == "https://repo.mamba.pm/defaults"
You may have noticed that Channel.resolve
returns
multiple channels.
This is because of custom multichannel, a single name can return mutliple channels.
import libmambapy.specs as specs
chan_main, *_ = specs.Channel.resolve(
specs.UnresolvedChannel.parse("pkgs/main"),
# ...
)
chan_r, *_ = specs.Channel.resolve(
specs.UnresolvedChannel.parse("pkgs/r"),
# ...
)
defaults = specs.Channel.resolve(
specs.UnresolvedChannel.parse("defaults"),
custom_multichannels=specs.Channel.MultiChannelMap(
{"defaults": [chan_main, chan_r]}
),
# ...
)
assert defaults == [chan_main, chan_r]
Note
Creating Channel
objects this way, while highly customizable, can be very verbose.
In practice, one can create a ChannelContext
with ChannelContext.make_simple
or
ChannelContext.make_conda_compatible
to compute and hold all these parameters from a
Context
(itself getting its values from all the configuration sources).
ChannelContext.make_channel
can then directly construct a
Channel
from a string.
Version#
In the conda ecosystem, a version is an epoch and a pair of arbitrary length sequences of arbitrary
length sequences of string and integer pairs.
Let’s unpack this with an example.
The version 1.2.3
is the outer sequence, it can actually contain as many elements as needed
so 1.2.3.4.5.6.7
is also a valid version.
For alpha version, we sometimes see something like 1.0.0alpha1
.
That’s the inner sequence of pairs, for the last part [(0, "alpha"), (1, "")]
.
There can also be any number, such as in 1.0.0alpha1dev3
.
We can specify another “local” version, that we can separate with a +
, as in 1.9.0+2.0.0
,
but that is not widely used.
Finally, there is also an epoch, similar to PEP440, to
accomodate for change in the versioning scheme.
For instance, in 1!2.0.3
, the epoch is 1
.
To sum up, a version like 7!1.2a3.5b4dev+1.3.0
, can be parsed as:
epoch:
7
,version:
[[(1, "")], [(2, "a"), (3, "")], [(5, "b"), (4, "dev")]]
local version:
[[(1, "")], [(3, "")], [(0, "")]]
Finally, all versions are considered equal to the same version with any number of trailing zeros,
so 1.2
, 1.2.0
, and 1.2.0.0
are all considered equal.
Warning
The flexibility of conda versions (arguably too flexible) is meant to accomodate differences in various ecosystems. Library authors should stick to well defined version schemes such as semantic versioning, calendar versioning, or PEP440.
A Version
can be created by parsing a string with
Version.parse
.
import libmambapy.specs as specs
v = specs.Version.parse("7!1.2a3.5b4dev+1.3.0")
The most useful operations on versions is to compare them. All comparison operators are available:
import libmambapy.specs as specs
assert specs.Version.parse("1.2.0") == specs.Version.parse("1.2.0.0")
assert specs.Version.parse("1.2.0") < specs.Version.parse("1.3")
assert specs.Version.parse("2!4.0.0") >= specs.Version.parse("1.8")
VersionSpec#
A version spec is a way to describe a set of versions. We have the following primitives:
*
matches all versions (unrestricted).==
for equal matches versions equal to the given one (a singleton). For instance==1.2.4
matches1.2.4
only, and not1.2.4.1
or1.2
. Note that since1.2.4.0
is the same as1.2.4
, this is also matched.!=
for not equal is the opposite, it matches all but the given version. For instance=!1.2.4
matches1.2.5
and1!1.2.4
but not1.2.4
.>
for greater matches versions stricly greater than the current one, for instance>1.2.4
matches2.0.0
,1!1.0.0
, but not1.1.0
or1.2.4
.>=
for greater or equal.<
for less.<=
for less or equal.=
for starts with matches versions that start with the same non zero parts of the version. For instance=1.7
matches1.7.8
, and1.7.0alpha1
(beware since this is smaller than1.7.0
). This spec can equivalently be written1.7
(bare),1.7.*
, or=1.7.*
.=!
with.*
for not starts with matches all versions but the one that starts with the non zero parts specified. For instance!=1.7.*
matches1.8.3
but not1.7.2
.~=
for compatible with matches versions that are greater or equal and starting with the all but the last parts specified, including zeros. For instance~=2.0
matches2.0.0
,2.1.3
, but not3.0.1
or2.0.0alpha
.
All version spec can be combine using a boolean grammar where |
means or and ,
means
and.
For instance, (>2.1.0,<3.0)|==2.0.1
means:
- Either
equal to
2.0.1
,or, both - greater that
2.1.0
- and less than3.0
.
To create a VersionSpec
from a string, we parse it with
VersionSpec.parse
.
To check if a given version matches a version spec, we use
VersionSpec.contains
.
import libmambapy.specs as specs
vs = specs.VersionSpec.parse("(>2.1.0,<3.0)|==2.0.1")
assert vs.contains(specs.Version.parse("2.4.0"))
assert vs.contains(specs.Version.parse("2.0.1"))
assert not vs.contains(specs.Version.parse("3.0.1"))
Warning
Single versions such as 3.7
are parsed by Conda and Mamba as ==3.7
, which can seem
unintuitive.
As such, it is recommended to always specify an operator.
This mistake is especially likely when writing a match spec such as python 3.7
.
BuildNumberSpec#
Similarily, a build number spec is a way to describe a set of build numbers.
It’s much simpler than the VersionSpec
in that it does not contain any boolean grammar
(the ,
and |
operators).
BuildNumberSpec
only contain primitives similar to that used in VersionSpec
:
*
or=*
matches all build numbers (unrestricted).=
for equal matches build numbers equal to the given one (a singleton).!=
for not equal.>
for greater matches versions stricly greater than the current one.>=
for greater or equal.<
for less.<=
for less or equal.
To create a BuildNumberSpec
from a string, we parse it
with BuildNumberSpec.parse
.
To check if a given build number matches a build number spec, we use
BuildNumberSpec.contains
.
import libmambapy.specs as specs
bs = specs.BuildNumberSpec.parse(">2")
assert bs.contains(3)
assert not bs.contains(2)
Other Specs#
The GlobSpec
is used to match glob expressions on strings.
The only wildcard currently supported is *
which stands for any string (0 or more characters).
The glob spec is used as the basis for the MatchSpec
package name and build string.
import libmambapy.specs as specs
glob = specs.GlobSpec.parse("py*")
assert glob.contains("python")
assert glob.contains("pypy")
assert not vs.contains("rust-python")
MatchSpec#
Ultimately, the MatchSpec
is the way to match on conda packages, that is a way to describe a
set of packages.
This is what is passed in a command line argument such as mamba install <match_spec>
.
Match specs have a complex string representation, which we can informally write as
[[<channel>:]<namespace>:]<name>[<version>[=<build_string>]][[<attribute>=<value>, [...]]]
, or
with an example
conda-forge:ns:python>=3.7=*cypthon[subdir="linux-64",fn=pkg.conda]
.
<channel>
, hereconda-forge
describes anUnresolvedChannel
of where the channel the package should come from. It accepts all values from an unresolved channel, such asconda-forge/label/micromamba_dev
, URLs, local file path, and platforms filters in between brackets.<namespace>
, herens
is a future, not implemented, feature. It is nonetheless parsed, and retrievable.<name>
, herepython
is the package name or glob expression and is the only mandatory field.Following is the
VersionSpec
<version>
or>=3.7
here.When the version specification is written (but it could also be set to
=*
), it can be followed by a<build_string>
glob specification, here*cpython
.Last, a bracket section of comma separated
<attribute>
=<value>
. In the example, we have two attributes,subdir
andfn
. Attribute values support quaoting with"
or'
. As such, they can be useful to set previously mentioned field without ambiguity. Valid attribute names are:channel
, similar to<channel>
.name
, similar to<name>
.version
, similar to<version>
(can be useful to set version expression containing parentheses and,
and|
operators).build
, similar to<build_string>
.build_number
to set theBuildNumberSpec
.subdir
to select the channel subdirectory platform from which the package must come from.fn
to select the filename the package must match.md5
to specify the MD5 hash the package archive must have.sha256
to specify the SHA256 hash the package archive must have.license
to specify the license the package must have.track_features
to specify a list oftrack_features
specified at the package build time.optional
to add the package as a constraint rather than a strict dependency.
Warning
Specifying some value mulitple time, such as in python>=3.7[version="(=3.9|>3.11)"]
, or
python[build="foo"][build="bar"]
is undefined and subject to change in the future.
Warning
When specifying a version in the attribute section, the first =
is parsed as the attribute
assignment.
That is python[version=3.7]
is equivalent to python 3.7
, which is equivalent to
python==3.7
(strong equality).
This is intuitively different from how we write python=3.7
, which we must write with
attributes as python[version="=3.7"]
.
The method
MatchSpec.contains_except_channel
can be used to check if a package is contained (matched) by the current MatchSpec
.
The somewhat verbose name serve to indicate that the channel is ignored in this function.
As mentionned in the Channel section resolving and matching channels
is a delicate operation.
In addition, the channel is a part that describe the provenance of a package and not is content
so various application ay want to handle it in different ways.
The MatchSpec.channel
attribute can be used to
reason about the possible channel contained in the MatchSpec
.
import libmambapy.specs as specs
ms = specs.MatchSpec.parse("conda-forge::py*[build_number='>4']")
assert ms.contains(name="python", build_number=5)
assert not ms.contains(name="numpy", build_number=8)
assert ms.channel.location == "conda-forge"