Skip to content

Commit

Permalink
Add a LegacySpecifierSet; resolves #74
Browse files Browse the repository at this point in the history
  • Loading branch information
jimporter committed Jul 10, 2020
1 parent a0b41c2 commit f1893d5
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 56 deletions.
46 changes: 45 additions & 1 deletion docs/specifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Reference
This class abstracts handling specifying the dependencies of a project. It
can be passed a single specifier (``>=3.0``), a comma-separated 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
specifier will 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")``).
Expand Down Expand Up @@ -128,6 +128,50 @@ Reference
all prerelease versions from being included.


.. class:: LegacySpecifierSet(specifier)

This class abstracts the handling of a set of legacy, setuptools style
specifiers. It can be passed a single specifier (``>=3.0``), a
comma-separated list of specifiers (``>=3.0,!=3.1``), or no specifier at
all. Each individual specifier will be parsed as a
(:class:`LegacySpecifier`). You may combine :class:`LegacySpecifierSet`
instances using the ``&`` operator
(``LegacySpecifierSet(">2") & LegacySpecifierSet("<4")``).

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

:param str specifiers: The string representation of a specifier or a
comma-separated list of specifiers which will
be parsed and normalized before use.
:raises InvalidSpecifier: If the given ``specifiers`` are not parseable
than this exception will be raised.

.. attribute:: prereleases

See :attr:`SpecifierSet.prereleases`.

.. method:: __contains__(version)

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

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

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

.. method:: __len__()

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

.. method:: __iter__()

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

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

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


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

This class abstracts the handling of a single `PEP 440`_ compatible
Expand Down
162 changes: 107 additions & 55 deletions packaging/specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .version import Version, LegacyVersion, parse

if TYPE_CHECKING: # pragma: no cover
from typing import List, Dict, Union, Iterable, Iterator, Optional, Callable, Tuple
from typing import List, Dict, Union, Iterable, Iterator, Optional, Callable, Set, Tuple

ParsedVersion = Union[Version, LegacyVersion]
UnparsedVersion = Union[Version, LegacyVersion, str]
Expand Down Expand Up @@ -651,25 +651,12 @@ def _pad_version(left, right):
return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))


class SpecifierSet(BaseSpecifier):
def __init__(self, specifiers="", prereleases=None):
# type: (str, Optional[bool]) -> None

# Split on , to break each individual specifier into it's own item, and
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]

# Parsed each individual specifier, attempting first to make it a
# Specifier and falling back to a LegacySpecifier.
parsed = set()
for specifier in split_specifiers:
try:
parsed.add(Specifier(specifier))
except InvalidSpecifier:
parsed.add(LegacySpecifier(specifier))
class _BaseSpecifierSet(BaseSpecifier):
def __init__(self, parsed_specifiers, prereleases):
# type: (Set[Specifier], Optional[bool]) -> None

# Turn our parsed specifiers into a frozen set and save them for later.
self._specs = frozenset(parsed)
self._specs = frozenset(parsed_specifiers)

# Store our prereleases value so we can use it later to determine if
# we accept prereleases or not.
Expand All @@ -683,7 +670,7 @@ def __repr__(self):
else ""
)

return "<SpecifierSet({0!r}{1})>".format(str(self), pre)
return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre)

def __str__(self):
# type: () -> str
Expand All @@ -696,11 +683,13 @@ def __hash__(self):
def __and__(self, other):
# type: (Union[SpecifierSet, str]) -> SpecifierSet
if isinstance(other, string_types):
other = SpecifierSet(other)
elif not isinstance(other, SpecifierSet):
other = self.__class__(other) # type: ignore
elif not isinstance(other, self.__class__):
# Currently, SpecifierSets and LegacySpecifierSets can't be
# combined.
return NotImplemented

specifier = SpecifierSet()
specifier = self.__class__() # type: ignore
specifier._specs = frozenset(self._specs | other._specs)

if self._prereleases is None and other._prereleases is not None:
Expand All @@ -711,26 +700,26 @@ def __and__(self, other):
specifier._prereleases = self._prereleases
else:
raise ValueError(
"Cannot combine SpecifierSets with True and False prerelease "
"overrides."
"Cannot combine {}s with True and False prerelease "
"overrides.".format(self.__class__.__name__)
)

return specifier

def __eq__(self, other):
# type: (object) -> bool
if isinstance(other, (string_types, _IndividualSpecifier)):
other = SpecifierSet(str(other))
elif not isinstance(other, SpecifierSet):
other = self.__class__(str(other)) # type: ignore
elif not isinstance(other, _BaseSpecifierSet):
return NotImplemented

return self._specs == other._specs

def __ne__(self, other):
# type: (object) -> bool
if isinstance(other, (string_types, _IndividualSpecifier)):
other = SpecifierSet(str(other))
elif not isinstance(other, SpecifierSet):
other = self.__class__(str(other)) # type: ignore
elif not isinstance(other, _BaseSpecifierSet):
return NotImplemented

return self._specs != other._specs
Expand Down Expand Up @@ -775,8 +764,7 @@ def contains(self, item, prereleases=None):
# type: (Union[ParsedVersion, str], Optional[bool]) -> bool

# Ensure that our item is a Version or LegacyVersion instance.
if not isinstance(item, (LegacyVersion, Version)):
item = parse(item)
parsed_item = self._coerce_version(item)

# Determine if we're forcing a prerelease or not, if we're not forcing
# one for this particular filter call, then we'll use whatever the
Expand All @@ -790,14 +778,16 @@ def contains(self, item, prereleases=None):
# short circuit that here.
# Note: This means that 1.0.dev1 would not be contained in something
# like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
if not prereleases and item.is_prerelease:
if not prereleases and parsed_item.is_prerelease:
return False

# We simply dispatch to the underlying specs here to make sure that the
# given version is contained within all of them.
# Note: This use of all() here means that an empty set of specifiers
# will always return True, this is an explicit design decision.
return all(s.contains(item, prereleases=prereleases) for s in self._specs)
return all(
s.contains(parsed_item, prereleases=prereleases) for s in self._specs
)

def filter(
self,
Expand All @@ -823,31 +813,93 @@ def filter(
# which will filter out any pre-releases, unless there are no final
# releases, and which will filter out LegacyVersion in general.
else:
filtered = [] # type: List[Union[ParsedVersion, str]]
found_prereleases = [] # type: List[Union[ParsedVersion, str]]
return self._filter_prereleases(iterable, prereleases) # type: ignore

for item in iterable:
# Ensure that we some kind of Version class for this item.
if not isinstance(item, (LegacyVersion, Version)):
parsed_version = parse(item)
else:
parsed_version = item

# Filter out any item which is parsed as a LegacyVersion
if isinstance(parsed_version, LegacyVersion):
continue
class LegacySpecifierSet(_BaseSpecifierSet):
def __init__(self, specifiers=""):
# type: (str) -> None

# Store any item which is a pre-release for later unless we've
# already found a final version or we are accepting prereleases
if parsed_version.is_prerelease and not prereleases:
if not filtered:
found_prereleases.append(item)
else:
filtered.append(item)
# Split on , to break each individual specifier into its own item, and
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]

# Parsed each individual specifier as a LegacySpecifier.
parsed = set(LegacySpecifier(specifier) for specifier in split_specifiers)

super(LegacySpecifierSet, self).__init__(parsed, None)

def _coerce_version(self, version):
# type: (UnparsedVersion) -> ParsedVersion
if not isinstance(version, (LegacyVersion, Version)):
version = LegacyVersion(version)
return version

def _filter_prereleases(
self,
iterable, # type: Iterable[Union[ParsedVersion, str]]
prereleases, # type: bool
):
# type: (...) -> Iterable[Union[ParsedVersion, str]]

# Note: We ignore prereleases, since LegacyVersions are never
# prereleases, and only have that field for compatibility.
return iterable


class SpecifierSet(_BaseSpecifierSet):
def __init__(self, specifiers="", prereleases=None):
# type: (str, Optional[bool]) -> None

# Split on , to break each individual specifier into its own item, and
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]

# Parsed each individual specifier, attempting first to make it a
# Specifier and falling back to a LegacySpecifier.
parsed = set()
for specifier in split_specifiers:
try:
parsed.add(Specifier(specifier))
except InvalidSpecifier:
parsed.add(LegacySpecifier(specifier))

# If we've found no items except for pre-releases, then we'll go
# ahead and use the pre-releases
if not filtered and found_prereleases and prereleases is None:
return found_prereleases
super(SpecifierSet, self).__init__(parsed, prereleases)

return filtered
def _coerce_version(self, version):
# type: (UnparsedVersion) -> ParsedVersion
if not isinstance(version, (LegacyVersion, Version)):
version = parse(version)
return version

def _filter_prereleases(
self,
iterable, # type: Iterable[Union[ParsedVersion, str]]
prereleases, # type: bool
):
# type: (...) -> Iterable[Union[ParsedVersion, str]]
filtered = [] # type: List[Union[ParsedVersion, str]]
found_prereleases = [] # type: List[Union[ParsedVersion, str]]

for item in iterable:
# Ensure that we some kind of Version class for this item.
parsed_version = self._coerce_version(item)

# Filter out any item which is parsed as a LegacyVersion
if isinstance(parsed_version, LegacyVersion):
continue

# Store any item which is a pre-release for later unless we've
# already found a final version or we are accepting prereleases
if parsed_version.is_prerelease and not prereleases:
if not filtered:
found_prereleases.append(item)
else:
filtered.append(item)

# If we've found no items except for pre-releases, then we'll go
# ahead and use the pre-releases
if not filtered and found_prereleases and prereleases is None:
return found_prereleases

return filtered
Loading

0 comments on commit f1893d5

Please sign in to comment.