Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions benchmarks/specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ def _make_cold(self, spec: SpecifierSet) -> None:
spec._canonicalized = False
if hasattr(spec, "_resolved_ops"):
spec._resolved_ops = None
if hasattr(spec, "_ranges"):
spec._ranges = None
if hasattr(spec, "_range_cache"):
spec._range_cache = None
if hasattr(spec, "_is_unsatisfiable"):
spec._is_unsatisfiable = None
for sp in spec._specs:
sp._spec_version = None
if hasattr(sp, "_wildcard_split"):
sp._wildcard_split = None
if hasattr(sp, "_ranges"):
sp._ranges = None
if hasattr(sp, "_range_cache"):
sp._range_cache = None

@add_attributes(pretty_name="SpecifierSet constructor")
def time_constructor(self) -> None:
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The ``packaging`` library uses calendar-based versioning (``YY.N``).

version
specifiers
ranges
markers
licenses
requirements
Expand Down
157 changes: 157 additions & 0 deletions docs/ranges.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
Ranges
======

.. versionadded:: 26.3

A :class:`~packaging.ranges.VersionRange` represents the set of
:class:`~packaging.version.Version` values matched by a
:class:`~packaging.specifiers.Specifier` or
:class:`~packaging.specifiers.SpecifierSet`. Unlike a
:class:`~packaging.specifiers.SpecifierSet`, ranges are closed under
intersection, union, and complement, so questions like "do these two
constraints overlap?" or "is this constraint a subset of that one?"
reduce to direct set operations.

Constructing a range
--------------------

Build a range from a :class:`Specifier` or :class:`SpecifierSet`
using :meth:`~Specifier.to_range`:

.. doctest::

>>> from packaging.ranges import VersionRange
>>> from packaging.specifiers import Specifier, SpecifierSet
>>> r = SpecifierSet(">=1.0,<2.0").to_range()
>>> "1.5" in r
True
>>> "2.0" in r
False

The classmethods :meth:`VersionRange.from_specifier` and
:meth:`VersionRange.from_specifier_set` produce the same results and
are useful when only a :class:`VersionRange` reference is in scope.

Three factories return common identity ranges:

.. doctest::

>>> VersionRange.empty().is_empty
True
>>> "1.5" in VersionRange.full()
True
>>> "1.0" in VersionRange.singleton("1.0")
True

Calling ``VersionRange()`` directly raises :exc:`TypeError`; use one
of the factories above.

Set algebra
-----------

:class:`VersionRange` supports intersection, union, and complement
via the :meth:`~VersionRange.intersection`,
:meth:`~VersionRange.union`, and :meth:`~VersionRange.complement`
methods, or the ``&``, ``|``, and ``~`` operator aliases. Every
operation returns a new range; operands are not mutated.

.. doctest::

>>> ge1 = SpecifierSet(">=1.0").to_range()
>>> lt2 = SpecifierSet("<2.0").to_range()
>>> "1.5" in (ge1 & lt2)
True
>>> "2.5" in (ge1 | lt2)
True
>>> # Double-complement is the original range.
>>> ~~ge1 == ge1
True
>>> # A range and its complement are always disjoint.
>>> bool(ge1 & ~ge1)
False

Set operations answer overlap and subset questions directly:

.. doctest::

>>> a = SpecifierSet(">=1.0,<2.0").to_range()
>>> b = SpecifierSet(">=1.5,<3.0").to_range()
>>> # Do these constraints overlap?
>>> bool(a & b)
True
>>> # Is *a* entirely contained in *b*?
>>> (a & b) == a
False
>>> narrow = SpecifierSet(">=1.0,<1.5").to_range()
>>> wide = SpecifierSet(">=1.0,<2.0").to_range()
>>> (narrow & wide) == narrow
True

Membership and filtering
------------------------

``in`` and :meth:`~VersionRange.filter` mirror :class:`SpecifierSet`'s
:meth:`~SpecifierSet.__contains__` and :meth:`~SpecifierSet.filter`,
including the PEP 440 pre-release behaviour: with
``prereleases=None`` (the default), pre-releases are buffered and
emitted only when the iterable contains no in-range final release.

.. doctest::

>>> from packaging.version import Version
>>> r = SpecifierSet(">=1.0,<2.0").to_range()
>>> "1.5" in r
True
>>> Version("1.5") in r
True
>>> list(r.filter(["0.9", "1.5", "2.0"]))
['1.5']

Converting back to a SpecifierSet
---------------------------------

:meth:`~VersionRange.to_specifier_set` returns a single
:class:`SpecifierSet` whose :meth:`~SpecifierSet.to_range` yields the
same range, or ``None`` if no such single set exists. Redundant
specifiers are dropped, which makes the round-trip a useful
normalisation step:

.. doctest::

>>> r = SpecifierSet(">=1.0,<2.0,!=1.5").to_range()
>>> str(r.to_specifier_set())
'!=1.5,<2.0,>=1.0'
>>> # ``>2`` is subsumed by ``>=3``; ``!=1.0`` is outside ``>=3``.
>>> str(SpecifierSet("!=1.0,>2,>=3").to_range().to_specifier_set())
'>=3'

PEP 440 specifier sets are not closed under union, so the disjoint
union of two intervals returns ``None``;
:meth:`~VersionRange.to_specifier_sets` returns one
:class:`SpecifierSet` per interval:

.. doctest::

>>> r = (
... SpecifierSet(">=1.0,<2.0").to_range()
... | SpecifierSet(">=3.0,<4.0").to_range()
... )
>>> r.to_specifier_set() is None
True
>>> [str(s) for s in r.to_specifier_sets()]
['<2.0,>=1.0', '<4.0,>=3.0']

The empty range round-trips through ``SpecifierSet("<0")`` (``<0``
excludes the smallest possible PEP 440 version, ``0.dev0``):

.. doctest::

>>> VersionRange.empty().to_specifier_set() == SpecifierSet("<0")
True

Reference
---------

.. autoclass:: packaging.ranges.VersionRange
:members:
:special-members: __contains__, __bool__, __eq__, __hash__, __repr__
Loading
Loading