Skip to content

Commit

Permalink
Refactor specifiers to handle legacy specifiers
Browse files Browse the repository at this point in the history
The original specifier code assumed that the only kind of specifiers
that would ever be in play are PEP 440 compliant specifiers. However
it quickly came to light that there are a lot of legacy cases where
a specifier cannot be cleanly parsed using PEP 440.

This changes the specifier handling code to have:

* An abstract base class, ``BaseSpecifier`` which functions as the
  definition for what a specifier class must contain.
* A ``LegacySpecifier`` which interprets all versions as a
  ``LegacyVersion``. This does not implement any of the new operator
  types such as ``~=`` or ``===`` and is intended to act like
  setuptools previously did.
* A ``Specifier`` which interprets specifiers as defined in PEP 440.
* A ``SpecifierSet`` which can hold one or more specifiers and
  attempts to sanely allow the mixed usage of both ``Specifier`` and
  ``LegacySpecifier``.

A major change in behavior is that a ``Specifier`` (and now a
``LegacySpecifier``) will only represent one single specifier and will
not accept a comma separated list of them. The new ``SpecifierSet``
will handle cases where you have one or more specifiers and will
attempt to sanely handle the case where you have a mixture of
``Specifier`` and ``LegacySpecifier`` instances.

In particular, a ``SpecifierSet`` has the following behaviors:

If given no specifiers:

* It will *not* match any ``LegacyVersion`` items.
* It will *not* match any pre-release versions, unless there are no
  final releases available that match.

If given any specifiers:

* It will match only if the given version(s) match each and every
  specifier contained within the ``Specifier`` set.

The side effects of this are:

* If two specifiers are given, one that matches pre-releases and one
  that doesn't, then the ``SpecifierSet`` as a whole will not accept
  any pre-releases.
* Even versions that can be parsed as PEP 440 will not use any of the
  new semantics when being interpreted by a ``LegacySpecifier``, so
  something like ``SpecifierSet(">=1.0-1-1")`` will end up allowing
  pre-releases since ``LegacyVersion`` has no concept of them.
  • Loading branch information
dstufft committed Nov 18, 2014
1 parent 9542495 commit 892f42a
Show file tree
Hide file tree
Showing 5 changed files with 930 additions and 291 deletions.
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[run]
branch = True
omit = packaging/_compat.py

[report]
exclude_lines =
@abc.abstractmethod
@abc.abstractproperty
105 changes: 83 additions & 22 deletions docs/specifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ Usage

.. doctest::

>>> from packaging.specifiers import Specifier
>>> from packaging.specifiers import SpecifierSet
>>> from packaging.version import Version
>>> spec1 = Specifier("~=1.0")
>>> spec1 = SpecifierSet("~=1.0")
>>> spec1
<Specifier('~=1.0')>
>>> spec2 = Specifier(">=1.0")
<SpecifierSet('~=1.0')>
>>> spec2 = SpecifierSet(">=1.0")
>>> spec2
<Specifier('>=1.0')>
<SpecifierSet('>=1.0')>
>>> # We can combine specifiers
>>> combined_spec = spec1 & spec2
>>> combined_spec
<Specifier('>=1.0,~=1.0')>
<SpecifierSet('>=1.0,~=1.0')>
>>> # We can also implicitly combine a string specifier
>>> combined_spec &= "!=1.1"
>>> combined_spec
<Specifier('!=1.1,>=1.0,~=1.0')>
<SpecifierSet('!=1.1,>=1.0,~=1.0')>
>>> # Create a few versions to check for contains.
>>> v1 = Version("1.0a5")
>>> v2 = Version("1.0")
Expand All @@ -48,37 +48,42 @@ Usage
Reference
---------

.. class:: Specifier(specifier, prereleases=None)
.. class:: SpeciferSet(specifiers, prereleases=None)

This class abstracts handling of specifying the dependencies of a project.
It implements the scheme defined in `PEP 440`_. You may combine Specifier
instances using the ``&`` operator (``Specifier(">2") & Specifier(">3")``).
This class abstracts handling specifying the dependencies of a project. It
can be passed a single specifier (``>=3.0``), a comma seperated list of
specifiers (``>=3.0,!=3.1``), or no specifier at all. Each individual
specifier be attempted to be parsed as a PEP 440 specifier
(:class:`Specifier`) or as a legacy, setuptools style specifier
(:class:`LegacySpecifier`). You may combine :class:`SpecifierSet` instances
using the ``&`` operator (``SpecifierSet(">2") & SpecifierSet("<4")``).

Both the membership test and the combination supports using raw strings
in place of already instantiated objects.
Both the membership tests and the combination support using raw strings
in place of already instatiated bojects.

:param str specifier: The string representation of a specifier which will
be parsed and normalized before use.
:param bool prereleases: This tells the specifier if it should accept
:param str specifiers: The string representation of a specifier or a comma
seperated list of specifiers which will be parsed and
normalized before use.
:param bool prereleases: This tells the SpecifierSet if it should accept
prerelease versions if applicable or not. The
default of ``None`` will autodetect it from the
given specifiers.
:raises InvalidSpecifier: If the ``specifier`` does not conform to PEP 440
in any way then this exception will be raised.
:raises InvalidSpecifier: If the given ``specifiers`` are not parseable
than this exception will be raised.

.. attribute:: prereleases

A boolean value indicating whether this :class:`Specifier` represents
a specifier that includesa pre-release versions. This can be set to
either ``True`` or ``False`` to explicitly enable or disable
A boolean value indicating whether this :class:`SpecifierSet`
represents a specifier that includesa pre-release versions. This can be
set to either ``True`` or ``False`` to explicitly enable or disable
prereleases or it can be set to ``None`` (the default) to enable
autodetection.

.. method:: contains(version, prereleases=None)

Determines if ``version``, which can be either a version string, a
:class:`Version`, or a :class:`LegacyVersion` object, is contained
within this specifier.
within this set of specifiers.

This will either match or not match prereleases based on the
``prereleases`` parameter. When ``prereleases`` is set to ``None``
Expand Down Expand Up @@ -106,6 +111,62 @@ Reference
all prerelease versions from being included.


.. class:: Specifier(specifier, prereleases=None)

This class abstracts the handling of a single `PEP 440`_ compataible
specifier. It is generally not required to instatiate this manually,
preferring instead to work with :class:`SpecifierSet`.

:param str specifier: The string representation of a specifier which will
be parsed and normalized before use.
:param bool prereleases: This tells the specifier if it should accept
prerelease versions if applicable or not. The
default of ``None`` will autodetect it from the
given specifiers.
:raises InvalidSpecifier: If the ``specifier`` does not conform to PEP 440
in any way then this exception will be raised.

.. attribute:: prereleases

See :attr:`SpecifierSet.prereleases`.

.. method:: contains(version, prereleases=None)

See :meth:`SpecifierSet.contains()`.

.. method:: filter(iterable, prereleases=None)

See :meth:`SpecifierSet.filter()`.


.. class:: LegacySpecifier(specifier, prereleases=None)

This class abstracts the handling of a single legacy, setuptools tyle
specifier. It is generally not required to instatiate this manually,
preferring instead to work with :class:`SpecifierSet`.

:param str specifier: The string representation of a specifier which will
be parsed and normalized before use.
:param bool prereleases: This tells the specifier if it should accept
prerelease versions if applicable or not. The
default of ``None`` will autodetect it from the
given specifiers.
:raises InvalidSpecifier: If the ``specifier`` is not parseable than this
will be raised.

.. attribute:: prereleases

See :attr:`SpecifierSet.prereleases`.

.. method:: contains(version, prereleases=None)

See :meth:`SpecifierSet.contains()`.

.. method:: filter(iterable, prereleases=None)

See :meth:`SpecifierSet.filter()`.


.. exception:: InvalidSpecifier

Raised when attempting to create a :class:`Specifier` with a specifier
Expand Down
13 changes: 13 additions & 0 deletions packaging/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,16 @@
string_types = str,
else:
string_types = basestring,


def with_metaclass(meta, *bases):
"""
Create a base class with a metaclass.
"""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
Loading

0 comments on commit 892f42a

Please sign in to comment.