Skip to content

Commit 9983e4b

Browse files
authored
Return False instead of raising for .contains with invalid version (#932)
1 parent e83801f commit 9983e4b

File tree

2 files changed

+51
-10
lines changed

2 files changed

+51
-10
lines changed

src/packaging/specifiers.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@
1616
from typing import Callable, Final, Iterable, Iterator, TypeVar, Union
1717

1818
from .utils import canonicalize_version
19-
from .version import Version
19+
from .version import InvalidVersion, Version
2020

2121
UnparsedVersion = Union[Version, str]
2222
UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion)
2323
CallableOperator = Callable[[Version, str], bool]
2424

2525

26-
def _coerce_version(version: UnparsedVersion) -> Version:
26+
def _coerce_version(version: UnparsedVersion) -> Version | None:
2727
if not isinstance(version, Version):
28-
version = Version(version)
28+
try:
29+
version = Version(version)
30+
except InvalidVersion:
31+
return None
2932
return version
3033

3134

@@ -581,6 +584,8 @@ def filter(
581584
# Filter versions
582585
for version in iterable:
583586
parsed_version = _coerce_version(version)
587+
if parsed_version is None:
588+
continue
584589

585590
if operator_callable(parsed_version, self.version):
586591
# If it's not a prerelease or prereleases are allowed, yield it directly
@@ -894,14 +899,14 @@ def contains(
894899
>>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)
895900
True
896901
"""
897-
# Ensure that our item is a Version instance.
898-
if not isinstance(item, Version):
899-
item = Version(item)
902+
version = _coerce_version(item)
903+
if version is None:
904+
return False
900905

901-
if installed and item.is_prerelease:
906+
if installed and version.is_prerelease:
902907
prereleases = True
903908

904-
return bool(list(self.filter([item], prereleases=prereleases)))
909+
return bool(list(self.filter([version], prereleases=prereleases)))
905910

906911
def filter(
907912
self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
@@ -959,7 +964,7 @@ def filter(
959964

960965
if prereleases is not None:
961966
# If we have a forced prereleases value,
962-
# we can immediately return he iterator.
967+
# we can immediately return the iterator.
963968
return iter(iterable)
964969
else:
965970
# Handle empty SpecifierSet cases where prereleases is not None.
@@ -968,7 +973,10 @@ def filter(
968973

969974
if prereleases is False:
970975
return (
971-
item for item in iterable if not _coerce_version(item).is_prerelease
976+
item
977+
for item in iterable
978+
if (version := _coerce_version(item)) is not None
979+
and not version.is_prerelease
972980
)
973981

974982
# Finally if prereleases is None, apply PEP 440 logic:
@@ -978,6 +986,8 @@ def filter(
978986

979987
for item in iterable:
980988
parsed_version = _coerce_version(item)
989+
if parsed_version is None:
990+
continue
981991
if parsed_version.is_prerelease:
982992
found_prereleases.append(item)
983993
else:

tests/test_specifiers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,17 @@ def test_specifiers(self, version, spec, expected):
494494
assert Version(version) not in spec
495495
assert not spec.contains(Version(version))
496496

497+
@pytest.mark.parametrize(
498+
("spec", "version"),
499+
[
500+
("==1.0", "not a valid version"),
501+
("===invalid", "invalid"),
502+
],
503+
)
504+
def test_invalid_spec(self, spec, version):
505+
spec = Specifier(spec, prereleases=True)
506+
assert not spec.contains(version)
507+
497508
@pytest.mark.parametrize(
498509
(
499510
"specifier",
@@ -645,6 +656,9 @@ def test_specifiers_prereleases(
645656
(">=1.0", False, True, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
646657
(">=1.0", True, True, ["1.0", "2.0a1"], ["1.0", "2.0a1"]),
647658
(">=1.0", False, False, ["1.0", "2.0a1"], ["1.0"]),
659+
# Test that invalid versions are discarded
660+
(">=1.0", None, None, ["not a valid version"], []),
661+
(">=1.0", None, None, ["1.0", "not a valid version"], ["1.0"]),
648662
],
649663
)
650664
def test_specifier_filter(
@@ -960,6 +974,13 @@ def test_specifier_contains_installed_prereleases(
960974
(">=1.0,<=2.0", False, True, ["1.0", "1.5a1"], ["1.0", "1.5a1"]),
961975
(">=1.0,<=2.0dev", True, False, ["1.0", "1.5a1"], ["1.0"]),
962976
(">=1.0,<=2.0dev", False, True, ["1.0", "1.5a1"], ["1.0", "1.5a1"]),
977+
# Test that invalid versions are discarded
978+
("", None, None, ["invalid version"], []),
979+
("", None, False, ["invalid version"], []),
980+
("", False, None, ["invalid version"], []),
981+
("", None, None, ["1.0", "invalid version"], ["1.0"]),
982+
("", None, False, ["1.0", "invalid version"], ["1.0"]),
983+
("", False, None, ["1.0", "invalid version"], ["1.0"]),
963984
],
964985
)
965986
def test_specifier_filter(
@@ -1332,6 +1353,16 @@ def test_contains_exclusionary_bridges(
13321353
kwargs = {"prereleases": prereleases} if prereleases is not None else {}
13331354
assert spec.contains(version, **kwargs) == expected
13341355

1356+
@pytest.mark.parametrize(
1357+
("specifier", "input"),
1358+
[
1359+
(">=1.0", "not a valid version"),
1360+
],
1361+
)
1362+
def test_contains_rejects_invalid_specifier(self, specifier, input):
1363+
spec = SpecifierSet(specifier, prereleases=True)
1364+
assert not spec.contains(input)
1365+
13351366
@pytest.mark.parametrize(
13361367
("specifier", "expected"),
13371368
[

0 commit comments

Comments
 (0)