Skip to content

Commit a8f28e2

Browse files
committed
Add exclude-newer tests
1 parent 86881af commit a8f28e2

File tree

4 files changed

+411
-1
lines changed

4 files changed

+411
-1
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Tests for pip install --exclude-newer-than."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from tests.lib import PipTestEnvironment, TestData
8+
9+
10+
class TestExcludeNewer:
11+
"""Test --exclude-newer-than functionality."""
12+
13+
def test_exclude_newer_than_invalid_date(
14+
self, script: PipTestEnvironment, data: TestData
15+
) -> None:
16+
"""Test that --exclude-newer-than fails with invalid date format."""
17+
result = script.pip(
18+
"install",
19+
"--no-index",
20+
"-f",
21+
data.packages,
22+
"--exclude-newer-than=invalid-date",
23+
"simple",
24+
expect_error=True,
25+
)
26+
27+
# Should fail with date parsing error
28+
assert "invalid" in result.stderr.lower() or "error" in result.stderr.lower()
29+
30+
def test_exclude_newer_than_help_text(self, script: PipTestEnvironment) -> None:
31+
"""Test that --exclude-newer-than appears in help text."""
32+
result = script.pip("install", "--help")
33+
assert "--exclude-newer-than" in result.stdout
34+
assert "datetime" in result.stdout
35+
36+
@pytest.mark.parametrize("command", ["install", "download", "wheel"])
37+
def test_exclude_newer_than_available_in_commands(
38+
self, script: PipTestEnvironment, command: str
39+
) -> None:
40+
"""Test that --exclude-newer-than is available in relevant commands."""
41+
result = script.pip(command, "--help")
42+
assert "--exclude-newer-than" in result.stdout
43+
44+
@pytest.mark.network
45+
def test_exclude_newer_than_with_real_pypi(
46+
self, script: PipTestEnvironment
47+
) -> None:
48+
"""Test exclude-newer functionality against real PyPI with upload times."""
49+
# Use a small package with known old versions for testing
50+
# requests 2.0.0 was released in 2013
51+
52+
# Test 1: With an old cutoff date, should find no matching versions
53+
result = script.pip(
54+
"install",
55+
"--dry-run",
56+
"--exclude-newer-than=2010-01-01T00:00:00",
57+
"requests==2.0.0",
58+
expect_error=True,
59+
)
60+
# Should fail because requests 2.0.0 was uploaded after 2010
61+
assert "No matching distribution found" in result.stderr
62+
63+
# Test 2: With a date that should find the package
64+
result = script.pip(
65+
"install",
66+
"--dry-run",
67+
"--exclude-newer-than=2030-01-01T00:00:00",
68+
"requests==2.0.0",
69+
expect_error=False,
70+
)
71+
assert "Would install requests-2.0.0" in result.stdout
72+
73+
@pytest.mark.network
74+
def test_exclude_newer_than_date_formats(self, script: PipTestEnvironment) -> None:
75+
"""Test different date formats work with real PyPI."""
76+
# Test various date formats with a well known small package
77+
formats = [
78+
"2030-01-01",
79+
"2030-01-01T00:00:00",
80+
"2030-01-01T00:00:00+00:00",
81+
"2030-01-01T00:00:00-05:00",
82+
]
83+
84+
for date_format in formats:
85+
result = script.pip(
86+
"install",
87+
"--dry-run",
88+
f"--exclude-newer-than={date_format}",
89+
"requests==2.0.0",
90+
expect_error=False,
91+
)
92+
# All dates should allow the package
93+
assert "Would install requests-2.0.0" in result.stdout

tests/unit/test_cmdoptions.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from __future__ import annotations
22

3+
import datetime
34
import os
5+
from collections.abc import Callable
6+
from optparse import Option, OptionParser, Values
47
from pathlib import Path
58
from venv import EnvBuilder
69

710
import pytest
811

9-
from pip._internal.cli.cmdoptions import _convert_python_version
12+
from pip._internal.cli.cmdoptions import (
13+
_convert_python_version,
14+
_handle_exclude_newer_than,
15+
)
1016
from pip._internal.cli.main_parser import identify_python_interpreter
1117

1218

@@ -51,3 +57,105 @@ def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
5157

5258
# Passing a non-existent file returns None
5359
assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None
60+
61+
62+
@pytest.mark.parametrize(
63+
"value, expected_check",
64+
[
65+
# Test with timezone info (should be preserved exactly)
66+
(
67+
"2023-01-01T00:00:00+00:00",
68+
lambda dt: dt
69+
== datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
70+
),
71+
(
72+
"2023-01-01T12:00:00-05:00",
73+
lambda dt: (
74+
dt
75+
== datetime.datetime(
76+
*(2023, 1, 1, 12, 0, 0),
77+
tzinfo=datetime.timezone(datetime.timedelta(hours=-5)),
78+
)
79+
),
80+
),
81+
],
82+
)
83+
def test_handle_exclude_newer_than_with_timezone(
84+
value: str, expected_check: Callable[[datetime.datetime], bool]
85+
) -> None:
86+
"""Test that timezone-aware ISO 8601 date strings are parsed correctly."""
87+
option = Option("--exclude-newer-than", dest="exclude_newer_than")
88+
opt = "--exclude-newer-than"
89+
parser = OptionParser()
90+
parser.values = Values()
91+
92+
_handle_exclude_newer_than(option, opt, value, parser)
93+
94+
result = parser.values.exclude_newer_than
95+
assert isinstance(result, datetime.datetime)
96+
assert expected_check(result)
97+
98+
99+
@pytest.mark.parametrize(
100+
"value, expected_date_time",
101+
[
102+
# Test basic ISO 8601 formats (timezone-naive, will get UTC timezone)
103+
("2023-01-01T00:00:00", (2023, 1, 1, 0, 0, 0)),
104+
("2023-12-31T23:59:59", (2023, 12, 31, 23, 59, 59)),
105+
# Test date only (will be extended to midnight)
106+
("2023-01-01", (2023, 1, 1, 0, 0, 0)),
107+
],
108+
)
109+
def test_handle_exclude_newer_than_naive_dates(
110+
value: str, expected_date_time: tuple[int, int, int, int, int, int]
111+
) -> None:
112+
"""Test that timezone-naive ISO 8601 date strings get UTC timezone applied."""
113+
option = Option("--exclude-newer-than", dest="exclude_newer_than")
114+
opt = "--exclude-newer-than"
115+
parser = OptionParser()
116+
parser.values = Values()
117+
118+
_handle_exclude_newer_than(option, opt, value, parser)
119+
120+
result = parser.values.exclude_newer_than
121+
assert isinstance(result, datetime.datetime)
122+
123+
# Check that the date/time components match
124+
(
125+
expected_year,
126+
expected_month,
127+
expected_day,
128+
expected_hour,
129+
expected_minute,
130+
expected_second,
131+
) = expected_date_time
132+
assert result.year == expected_year
133+
assert result.month == expected_month
134+
assert result.day == expected_day
135+
assert result.hour == expected_hour
136+
assert result.minute == expected_minute
137+
assert result.second == expected_second
138+
139+
# Check that UTC timezone was applied
140+
assert result.tzinfo == datetime.timezone.utc
141+
142+
143+
@pytest.mark.parametrize(
144+
"invalid_value",
145+
[
146+
"not-a-date",
147+
"2023-13-01", # Invalid month
148+
"2023-01-32", # Invalid day
149+
"2023-01-01T25:00:00", # Invalid hour
150+
"", # Empty string
151+
],
152+
)
153+
def test_handle_exclude_newer_than_invalid_dates(invalid_value: str) -> None:
154+
"""Test that invalid date strings raise ValueError."""
155+
option = Option("--exclude-newer-than", dest="exclude_newer_than")
156+
opt = "--exclude-newer-than"
157+
parser = OptionParser()
158+
parser.values = Values()
159+
160+
with pytest.raises(ValueError):
161+
_handle_exclude_newer_than(option, opt, invalid_value, parser)

tests/unit/test_finder.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import logging
23
from collections.abc import Iterable
34
from unittest.mock import Mock, patch
@@ -10,14 +11,19 @@
1011

1112
import pip._internal.utils.compatibility_tags
1213
from pip._internal.exceptions import BestVersionAlreadyInstalled, DistributionNotFound
14+
from pip._internal.index.collector import LinkCollector
1315
from pip._internal.index.package_finder import (
1416
CandidateEvaluator,
1517
InstallationCandidate,
1618
Link,
1719
LinkEvaluator,
1820
LinkType,
21+
PackageFinder,
1922
)
23+
from pip._internal.models.search_scope import SearchScope
24+
from pip._internal.models.selection_prefs import SelectionPreferences
2025
from pip._internal.models.target_python import TargetPython
26+
from pip._internal.network.session import PipSession
2127
from pip._internal.req.constructors import install_req_from_line
2228

2329
from tests.lib import TestData, make_test_finder
@@ -574,3 +580,72 @@ def test_find_all_candidates_find_links_and_index(data: TestData) -> None:
574580
versions = finder.find_all_candidates("simple")
575581
# first the find-links versions then the page versions
576582
assert [str(v.version) for v in versions] == ["3.0", "2.0", "1.0", "1.0"]
583+
584+
585+
class TestPackageFinderExcludeNewerThan:
586+
"""Test PackageFinder integration with exclude_newer_than functionality."""
587+
588+
def test_package_finder_create_with_exclude_newer_than(self) -> None:
589+
"""Test that PackageFinder.create() accepts exclude_newer_than parameter."""
590+
session = PipSession()
591+
search_scope = SearchScope([], [], no_index=False)
592+
link_collector = LinkCollector(session, search_scope)
593+
selection_prefs = SelectionPreferences(
594+
allow_yanked=False,
595+
allow_all_prereleases=False,
596+
)
597+
exclude_newer_than = datetime.datetime(
598+
2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
599+
)
600+
601+
finder = PackageFinder.create(
602+
link_collector=link_collector,
603+
selection_prefs=selection_prefs,
604+
exclude_newer_than=exclude_newer_than,
605+
)
606+
607+
assert finder._exclude_newer_than == exclude_newer_than
608+
609+
def test_package_finder_make_link_evaluator_with_exclude_newer_than(self) -> None:
610+
"""Test that PackageFinder creates LinkEvaluator with exclude_newer_than."""
611+
612+
session = PipSession()
613+
search_scope = SearchScope([], [], no_index=False)
614+
link_collector = LinkCollector(session, search_scope)
615+
selection_prefs = SelectionPreferences(
616+
allow_yanked=False,
617+
allow_all_prereleases=False,
618+
)
619+
exclude_newer_than = datetime.datetime(
620+
2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
621+
)
622+
623+
finder = PackageFinder.create(
624+
link_collector=link_collector,
625+
selection_prefs=selection_prefs,
626+
exclude_newer_than=exclude_newer_than,
627+
)
628+
629+
link_evaluator = finder.make_link_evaluator("test-package")
630+
assert link_evaluator._exclude_newer_than == exclude_newer_than
631+
632+
def test_package_finder_exclude_newer_than_none(self) -> None:
633+
"""Test that PackageFinder works correctly when exclude_newer_than is None."""
634+
session = PipSession()
635+
search_scope = SearchScope([], [], no_index=False)
636+
link_collector = LinkCollector(session, search_scope)
637+
selection_prefs = SelectionPreferences(
638+
allow_yanked=False,
639+
allow_all_prereleases=False,
640+
)
641+
642+
finder = PackageFinder.create(
643+
link_collector=link_collector,
644+
selection_prefs=selection_prefs,
645+
exclude_newer_than=None,
646+
)
647+
648+
assert finder._exclude_newer_than is None
649+
650+
link_evaluator = finder.make_link_evaluator("test-package")
651+
assert link_evaluator._exclude_newer_than is None

0 commit comments

Comments
 (0)