docs/source/usage/specs.rst
.. _mamba_usage_specs:
.. |CondaURL| replace:: :cpp:type:CondaURL <mamba::specs::CondaURL>
.. |UnresolvedChannel| replace:: :cpp:type:UnresolvedChannel <mamba::specs::UnresolvedChannel>
.. |Channel| replace:: :cpp:type:Channel <mamba::specs::Channel>
.. |Version| replace:: :cpp:type:Version <mamba::specs::Version>
.. |VersionSpec| replace:: :cpp:type:VersionSpec <mamba::specs::VersionSpec>
.. |BuildNumberSpec| replace:: :cpp:type:BuildNumberSpec <mamba::specs::BuildNumberSpec>
.. |GlobSpec| replace:: :cpp:type:GlobSpec <mamba::specs::GlobSpec>
.. |MatchSpec| replace:: :cpp:type:MatchSpec <mamba::specs::MatchSpec>
The :any:libmambapy.specs <mamba::specs> submodule contains objects 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.
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 :cpp:func:CondaURL.parse <mamba::specs::CondaURL::parse>
as follows:
.. code:: python
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 :cpp:func:CondaURL.parse <mamba::specs::CondaURL::parse> method assumes that the URL is
properly percent encoded <https://en.wikipedia.org/wiki/Percent-encoding>_.
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 :cpp:func:CondaURL.package <mamba::specs::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 below.
.. code:: python
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
:cpp:func:CondaURL.append_path <mamba::specs::CondaURL::append_path> or the / operator.
.. code:: python
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 :cpp:func:CondaURL.str <mamba::specs::CondaURL::str> can be used to get a raw
representation of the string. By default, it will hide all credentials
.. code:: python
import libmambapy.specs as specs
url = specs.CondaURL.parse("mamba.pm/conda-forge") url.set_user("[email protected]") 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:[email protected]/t/xy-12345678-1234" ) assert url.str(credentials="Remove") == "https://mamba.pm/"
Similarly the :cpp:func:CondaURL.pretty_str <mamba::specs::CondaURL::pretty_str> returns a more
user-friendly string, but that may not be parsed back.
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 resource (an unresolved name, a URL, a file) and the platform filters.
.. code:: python
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.
.. code:: python
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
.. _libmamba_usage_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 identification 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.
.. code:: python
import libmambapy.specs as specs import libmambapy.specs.CondaURL as CondaURL
uc = specs.UnresolvedChannel.parse("conda-forge[prius-avx42]") chan, *_ = specs.Channel.resolve( uc, channel_alias=CondaURL.parse("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:
.. code:: python
import libmambapy.specs as specs import libmambapy.specs.CondaURL as CondaURL
uc = specs.UnresolvedChannel.parse("defaults") chan, *_ = specs.Channel.resolve( uc, channel_alias=CondaURL.parse("https://repo.mamba.pm"), # ... )
assert chan.url.str() == "https://repo.mamba.pm/defaults"
You may have noticed that :cpp:func:Channel.resolve <mamba::specs::Channel::resolve> returns
multiple channels.
This is because of custom multichannels, a single name can return multiple channels.
.. code:: python
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.
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 <https://peps.python.org/pep-0440/>_, to
accommodate 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:
7,[[(1, "")], [(2, "a"), (3, "")], [(5, "b"), (4, "dev")]][[(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 accommodate differences
in various ecosystems.
Library authors should stick to well defined version schemes such as
semantic versioning <https://semver.org/>,
calendar versioning <https://calver.org/>, or
PEP440 <https://peps.python.org/pep-0440/>_.
A |Version| can be created by parsing a string with
:cpp:func:Version.parse <mamba::specs::Version::parse>.
.. code:: python
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:
.. code:: python
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")
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 matches 1.2.4 only, and not 1.2.4.1 or 1.2.
Note that since 1.2.4.0 is the same as 1.2.4, this is also matched.!= for not equal is the opposite, it matches all but the given version.
For instance =!1.2.4 matches 1.2.5 and 1!1.2.4 but not 1.2.4.> for greater matches versions strictly greater than the current one, for instance
>1.2.4 matches 2.0.0, 1!1.0.0, but not 1.1.0 or 1.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 matches 1.7.8, and 1.7.0alpha1 (beware since this is smaller
than 1.7.0).
This spec can equivalently be written 1.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.* matches 1.8.3 but not 1.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 matches 2.0.0, 2.1.3, but not 3.0.1 or 2.0.0alpha.All version specs can be combined using a boolean grammar where | means or and , means
and.
For instance, (>2.1.0,<3.0)|==2.0.1 means:
2.0.1,2.1.03.0.To create a |VersionSpec| from a string, we parse it with
:cpp:func:VersionSpec.parse <mamba::specs::VersionSpec::parse>.
To check if a given version matches a version spec, we use
:cpp:func:VersionSpec.contains <mamba::specs::VersionSpec::contains>.
.. code:: python
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.
Similarly, 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 those 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 strictly 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 :cpp:func:BuildNumberSpec.parse <mamba::specs::BuildNumberSpec::parse>.
To check if a given build number matches a build number spec, we use
:cpp:func:BuildNumberSpec.contains <mamba::specs::BuildNumberSpec::contains>.
.. code:: python
import libmambapy.specs as specs
bs = specs.BuildNumberSpec.parse(">2")
assert bs.contains(3) assert not bs.contains(2)
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.
.. code:: python
import libmambapy.specs as specs
glob = specs.GlobSpec.parse("py*")
assert glob.contains("python") assert glob.contains("pypy") assert not vs.contains("rust-python")
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>, here conda-forge, describes an |UnresolvedChannel| where the package
should come from.
It accepts all values from an unresolved channel, such as conda-forge/label/micromamba_dev,
URL, local file path, and platforms filters in between brackets.
<namespace>, here ns is a future, not implemented, feature.
It is nonetheless parsed, and retrievable.
<name>, here python 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 and fn.
Attribute values support quoting with " or '.
As such, they can be useful to set previously mentioned fields 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 the |BuildNumberSpec|.subdir to select the channel subdirectory platform from which the package must come.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 of track_features specified at the package build time.optional to add the package as a constraint rather than a strict dependency... warning::
Specifying some value multiple times, 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
:cpp:func:MatchSpec.contains_except_channel <mamba::specs::MatchSpec::contains_except_channel>
can be used to check if a package is contained (matched) by the current |MatchSpec|.
The somewhat verbose name serves to indicate that the channel is ignored in this function.
As mentioned in the :ref:Channel section<libmamba_usage_channel>, resolving and matching channels
is a delicate operation.
In addition, the channel is a part that describes the provenance of a package and not its content,
so various applications may want to handle it in different ways.
The :cpp:func:MatchSpec.channel <mamba::specs::MatchSpec::channel> attribute can be used to
reason about the possible channels contained in the |MatchSpec|.
.. code:: python
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"