Skip to content

Commit 7a79049

Browse files
committed
Add additional tests for --uploaded-prior-to
1 parent 893d315 commit 7a79049

File tree

5 files changed

+498
-1
lines changed

5 files changed

+498
-1
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Tests for pip install --uploaded-prior-to."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from tests.lib import PipTestEnvironment, TestData
8+
from tests.lib.server import (
9+
file_response,
10+
make_mock_server,
11+
package_page,
12+
server_running,
13+
)
14+
15+
16+
class TestUploadedPriorTo:
17+
"""Test --uploaded-prior-to functionality."""
18+
19+
def test_uploaded_prior_to_invalid_date(
20+
self, script: PipTestEnvironment, data: TestData
21+
) -> None:
22+
"""Test that invalid date format is rejected."""
23+
result = script.pip_install_local(
24+
"--uploaded-prior-to=invalid-date", "simple", expect_error=True
25+
)
26+
assert "invalid" in result.stderr.lower() or "error" in result.stderr.lower()
27+
28+
def test_uploaded_prior_to_file_index_no_upload_time(
29+
self, script: PipTestEnvironment, data: TestData
30+
) -> None:
31+
"""Test that file:// indexes are exempt from upload-time filtering."""
32+
result = script.pip(
33+
"install",
34+
"--index-url",
35+
data.index_url("simple"),
36+
"--uploaded-prior-to=2100-01-01T00:00:00",
37+
"simple",
38+
expect_error=False,
39+
)
40+
assert "Successfully installed simple" in result.stdout
41+
42+
def test_uploaded_prior_to_http_index_no_upload_time(
43+
self, script: PipTestEnvironment, data: TestData
44+
) -> None:
45+
"""Test that HTTP index without upload-time causes immediate error."""
46+
server = make_mock_server()
47+
simple_package = data.packages / "simple-1.0.tar.gz"
48+
server.mock.side_effect = [
49+
package_page({"simple-1.0.tar.gz": "/files/simple-1.0.tar.gz"}),
50+
file_response(simple_package),
51+
]
52+
53+
with server_running(server):
54+
result = script.pip(
55+
"install",
56+
"--index-url",
57+
f"http://{server.host}:{server.port}",
58+
"--uploaded-prior-to=2100-01-01T00:00:00",
59+
"simple",
60+
expect_error=True,
61+
)
62+
63+
assert "does not provide upload-time metadata" in result.stderr
64+
assert "--uploaded-prior-to" in result.stderr or "Cannot use" in result.stderr
65+
66+
@pytest.mark.network
67+
def test_uploaded_prior_to_with_real_pypi(self, script: PipTestEnvironment) -> None:
68+
"""Test filtering against real PyPI with upload-time metadata."""
69+
# Test with old cutoff date - should find no matching versions
70+
result = script.pip(
71+
"install",
72+
"--dry-run",
73+
"--no-deps",
74+
"--uploaded-prior-to=2010-01-01T00:00:00",
75+
"requests==2.0.0",
76+
expect_error=True,
77+
)
78+
assert "Could not find a version that satisfies" in result.stderr
79+
80+
# Test with future cutoff date - should find the package
81+
result = script.pip(
82+
"install",
83+
"--dry-run",
84+
"--no-deps",
85+
"--uploaded-prior-to=2030-01-01T00:00:00",
86+
"requests==2.0.0",
87+
expect_error=False,
88+
)
89+
assert "Would install requests-2.0.0" in result.stdout
90+
91+
@pytest.mark.network
92+
def test_uploaded_prior_to_date_formats(self, script: PipTestEnvironment) -> None:
93+
"""Test various date format strings are accepted."""
94+
formats = [
95+
"2030-01-01",
96+
"2030-01-01T00:00:00",
97+
"2030-01-01T00:00:00+00:00",
98+
"2030-01-01T00:00:00-05:00",
99+
]
100+
101+
for date_format in formats:
102+
result = script.pip(
103+
"install",
104+
"--dry-run",
105+
"--no-deps",
106+
f"--uploaded-prior-to={date_format}",
107+
"requests==2.0.0",
108+
expect_error=False,
109+
)
110+
assert "Would install requests-2.0.0" in result.stdout
111+
112+
def test_uploaded_prior_to_allows_local_files(
113+
self, script: PipTestEnvironment, data: TestData
114+
) -> None:
115+
"""Test that local file installs bypass upload-time filtering."""
116+
simple_wheel = data.packages / "simplewheel-1.0-py2.py3-none-any.whl"
117+
118+
result = script.pip(
119+
"install",
120+
"--no-index",
121+
"--uploaded-prior-to=2000-01-01T00:00:00",
122+
str(simple_wheel),
123+
expect_error=False,
124+
)
125+
assert "Successfully installed simplewheel-1.0" in result.stdout
126+
127+
def test_uploaded_prior_to_allows_find_links(
128+
self, script: PipTestEnvironment, data: TestData
129+
) -> None:
130+
"""Test that --find-links bypasses upload-time filtering."""
131+
result = script.pip(
132+
"install",
133+
"--no-index",
134+
"--find-links",
135+
data.find_links,
136+
"--uploaded-prior-to=2000-01-01T00:00:00",
137+
"simple==1.0",
138+
expect_error=False,
139+
)
140+
assert "Successfully installed simple-1.0" in result.stdout

tests/lib/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import datetime
34
import json
45
import os
56
import pathlib
@@ -104,6 +105,7 @@ def make_test_finder(
104105
allow_all_prereleases: bool = False,
105106
session: PipSession | None = None,
106107
target_python: TargetPython | None = None,
108+
uploaded_prior_to: datetime.datetime | None = None,
107109
) -> PackageFinder:
108110
"""
109111
Create a PackageFinder for testing purposes.
@@ -122,6 +124,7 @@ def make_test_finder(
122124
link_collector=link_collector,
123125
selection_prefs=selection_prefs,
124126
target_python=target_python,
127+
uploaded_prior_to=uploaded_prior_to,
125128
)
126129

127130

tests/unit/test_cmdoptions.py

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

3+
import datetime
34
import os
5+
from optparse import Option, OptionParser, Values
46
from pathlib import Path
57
from venv import EnvBuilder
68

79
import pytest
810

9-
from pip._internal.cli.cmdoptions import _convert_python_version
11+
from pip._internal.cli.cmdoptions import (
12+
_convert_python_version,
13+
_handle_uploaded_prior_to,
14+
)
1015
from pip._internal.cli.main_parser import identify_python_interpreter
1116

1217

@@ -51,3 +56,122 @@ def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
5156

5257
# Passing a non-existent file returns None
5358
assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None
59+
60+
61+
@pytest.mark.parametrize(
62+
"value, expected_datetime",
63+
[
64+
(
65+
"2023-01-01T00:00:00+00:00",
66+
datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
67+
),
68+
(
69+
"2023-01-01T12:00:00-05:00",
70+
datetime.datetime(
71+
*(2023, 1, 1, 12, 0, 0),
72+
tzinfo=datetime.timezone(datetime.timedelta(hours=-5)),
73+
),
74+
),
75+
],
76+
)
77+
def test_handle_uploaded_prior_to_with_timezone(
78+
value: str, expected_datetime: datetime.datetime
79+
) -> None:
80+
"""Test that timezone-aware ISO 8601 date strings are parsed correctly."""
81+
option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
82+
opt = "--uploaded-prior-to"
83+
parser = OptionParser()
84+
parser.values = Values()
85+
86+
_handle_uploaded_prior_to(option, opt, value, parser)
87+
88+
result = parser.values.uploaded_prior_to
89+
assert isinstance(result, datetime.datetime)
90+
assert result == expected_datetime
91+
92+
93+
@pytest.mark.parametrize(
94+
"value, expected_date_time",
95+
[
96+
# Test basic ISO 8601 formats (timezone-naive, will get local timezone)
97+
("2023-01-01T00:00:00", (2023, 1, 1, 0, 0, 0)),
98+
("2023-12-31T23:59:59", (2023, 12, 31, 23, 59, 59)),
99+
# Test date only (will be extended to midnight)
100+
("2023-01-01", (2023, 1, 1, 0, 0, 0)),
101+
],
102+
)
103+
def test_handle_uploaded_prior_to_naive_dates(
104+
value: str, expected_date_time: tuple[int, int, int, int, int, int]
105+
) -> None:
106+
"""Test that timezone-naive ISO 8601 date strings get local timezone applied."""
107+
option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
108+
opt = "--uploaded-prior-to"
109+
parser = OptionParser()
110+
parser.values = Values()
111+
112+
_handle_uploaded_prior_to(option, opt, value, parser)
113+
114+
result = parser.values.uploaded_prior_to
115+
assert isinstance(result, datetime.datetime)
116+
117+
# Check that the date/time components match
118+
assert result.timetuple()[:6] == expected_date_time
119+
120+
# Check that local timezone was applied (result should not be timezone-naive)
121+
assert result.tzinfo is not None
122+
123+
# Verify it's equivalent to what .astimezone() produces on a naive datetime
124+
naive_dt = datetime.datetime(*expected_date_time)
125+
expected_with_local_tz = naive_dt.astimezone()
126+
assert result == expected_with_local_tz
127+
128+
129+
@pytest.mark.parametrize(
130+
"invalid_value",
131+
[
132+
"not-a-date",
133+
"2023-13-01", # Invalid month
134+
"2023-01-32", # Invalid day
135+
"2023-01-01T25:00:00", # Invalid hour
136+
"", # Empty string
137+
],
138+
)
139+
def test_handle_uploaded_prior_to_invalid_dates(invalid_value: str) -> None:
140+
"""Test that invalid date strings raise SystemExit via raise_option_error."""
141+
option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
142+
opt = "--uploaded-prior-to"
143+
parser = OptionParser()
144+
parser.values = Values()
145+
146+
with pytest.raises(SystemExit):
147+
_handle_uploaded_prior_to(option, opt, invalid_value, parser)
148+
149+
150+
def test_handle_uploaded_prior_to_naive() -> None:
151+
"""
152+
Test that a naive datetime is interpreted as local time.
153+
"""
154+
option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
155+
opt = "--uploaded-prior-to"
156+
parser = OptionParser()
157+
parser.values = Values()
158+
159+
# Parse a naive datetime
160+
naive_input = "2023-06-15T14:30:00"
161+
_handle_uploaded_prior_to(option, opt, naive_input, parser)
162+
result = parser.values.uploaded_prior_to
163+
164+
assert result.hour == 14, (
165+
f"Expected hour=14 (from input), got hour={result.hour}. "
166+
"This suggests the naive datetime was incorrectly interpreted as UTC "
167+
"and converted to local timezone."
168+
)
169+
assert result.minute == 30
170+
assert result.year == 2023
171+
assert result.month == 6
172+
assert result.day == 15
173+
174+
# Verify by creating the same datetime with explicit local timezone
175+
local_tz = datetime.datetime.now().astimezone().tzinfo
176+
expected = datetime.datetime(2023, 6, 15, 14, 30, 0, tzinfo=local_tz)
177+
assert result == expected

tests/unit/test_finder.py

Lines changed: 38 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
@@ -575,3 +576,40 @@ def test_find_all_candidates_find_links_and_index(data: TestData) -> None:
575576
versions = finder.find_all_candidates("simple")
576577
# first the find-links versions then the page versions
577578
assert [str(v.version) for v in versions] == ["3.0", "2.0", "1.0", "1.0"]
579+
580+
581+
class TestPackageFinderUploadedPriorTo:
582+
"""Test PackageFinder integration with uploaded_prior_to functionality.
583+
584+
Only effective with indexes that provide upload-time metadata.
585+
"""
586+
587+
def test_package_finder_create_with_uploaded_prior_to(self) -> None:
588+
"""Test that PackageFinder.create() accepts uploaded_prior_to parameter."""
589+
uploaded_prior_to = datetime.datetime(
590+
2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
591+
)
592+
593+
finder = make_test_finder(uploaded_prior_to=uploaded_prior_to)
594+
595+
assert finder._uploaded_prior_to == uploaded_prior_to
596+
597+
def test_package_finder_make_link_evaluator_with_uploaded_prior_to(self) -> None:
598+
"""Test that PackageFinder creates LinkEvaluator with uploaded_prior_to."""
599+
uploaded_prior_to = datetime.datetime(
600+
2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
601+
)
602+
603+
finder = make_test_finder(uploaded_prior_to=uploaded_prior_to)
604+
605+
link_evaluator = finder.make_link_evaluator("test-package")
606+
assert link_evaluator._uploaded_prior_to == uploaded_prior_to
607+
608+
def test_package_finder_uploaded_prior_to_none(self) -> None:
609+
"""Test that PackageFinder works correctly when uploaded_prior_to is None."""
610+
finder = make_test_finder(uploaded_prior_to=None)
611+
612+
assert finder._uploaded_prior_to is None
613+
614+
link_evaluator = finder.make_link_evaluator("test-package")
615+
assert link_evaluator._uploaded_prior_to is None

0 commit comments

Comments
 (0)