Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/cron-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ jobs:
# reaches the installed release goes. Not one of
# util/verdict_covers_checkout.py's bot-managed paths, which says why
# (#1643). An unreadable package keeps its pin, as does a pin no release
# can move to (#2025), and each warns, as does a pin past its review-by
# date; none fails the step.
# can move to (#2025) and a pin whose own release has lost a wheel while
# an older one still qualifies (#2028), and each warns, as does a pin past
# its review-by date; none fails the step.
- name: Refresh the platform pins
run: python util/conda_vendor.py --refresh-pins

Expand Down
76 changes: 75 additions & 1 deletion tests/project/test_conda_platform_pin_refresh_1851.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
review-by date, drops it once it reaches the installed release or if it
already pins it, and leaves it, with a warning, when PyPI cannot be read or
when no release has a wheel for every conda Python, naming the Pythons no
release has one for (#2025).
release has one for (#2025), or when the pinned release has lost the wheel an
older release still has, naming the Pythons it lacks (#2028).
``--refresh-pins`` rewrites :file:`conda/platform-pins.toml` only on a change.
* :func:`conda_vendor._fetch` reads PyPI through :mod:`requests`, a stand-in
here, and raises each of its errors as :class:`OSError`. It imports requests
Expand Down Expand Up @@ -146,6 +147,17 @@ def recorded(**more: 'list[dict[str, Any]]') -> 'dict[str, Any]':
INTEL_AGAIN = files('51.0.0', 'cp311-abi3-macosx_10_9_universal2', 'cp311-abi3-manylinux_2_34_x86_64')


def lost_wheel() -> 'dict[str, Any]':
"""The recording with 48.0.1's cp311 osx-64 wheel yanked, and 47.0.0 still serving every Python (#2028).

48.0.1 keeps a cp312 ``abi3`` universal2 wheel, so it lacks cp311 alone.

"""
yanked = files('48.0.1', 'cp311-abi3-macosx_10_9_universal2', yanked=True)[:1]
return recorded(**{'47.0.0': files('47.0.0', 'cp311-abi3-macosx_10_9_universal2'),
'48.0.1': yanked + files('48.0.1', 'cp312-abi3-macosx_10_9_universal2')})


def table(version: 'str' = '48.0.1', review_by: 'datetime.date' = datetime.date(2026, 12, 1),
) -> 'dict[str, dict[str, Any]]':
"""A ``PLATFORM_PINS`` pinning cryptography to ``version`` on osx-64."""
Expand Down Expand Up @@ -393,6 +405,39 @@ def test_a_pin_no_single_release_serves_warns_for_every_tag(self) -> None:
'hold: no release up to 3.0 has wheels for all of cp311, cp312 '
'on osx-64 at once')])

def test_a_pin_whose_release_lost_a_wheel_warns_with_the_tag_it_lacks(self) -> None:
# #2028: 47.0.0 still has every wheel, so the 48.0.1 pin used to stay without a word.
result, notes = self.refresh(lost_wheel())
self.assertEqual(result, table())
self.assertEqual(notes, [('warning', 'cryptography 48.0.1 on osx-64 stays as it is, and may no longer '
'hold: 48.0.1 has no wheel for cp311 on osx-64, and the newest '
'release up to 52.0.0 with wheels for all of cp311, cp312, cp313, '
'cp314, cp315 there is 47.0.0')])

def test_a_pin_whose_release_lost_every_wheel_warns_for_every_tag(self) -> None:
# #2028: 49.0.0 has arm64 macOS wheels alone, and 48.0.1 has universal2 ones.
result, notes = self.refresh(recorded(), pins=table('49.0.0'))
self.assertEqual(result, table('49.0.0'))
self.assertEqual(notes, [('warning', 'cryptography 49.0.0 on osx-64 stays as it is, and may no longer '
'hold: 49.0.0 has no wheel for cp311, cp312, cp313, cp314, cp315 '
'on osx-64, and the newest release up to 52.0.0 with wheels for '
'all of cp311, cp312, cp313, cp314, cp315 there is 48.0.1')])

def test_a_pin_pypi_no_longer_lists_warns(self) -> None:
# #2028: a pinned release gone from PyPI, or a pre-release, is not one newest_wheel weighs.
for version in ('48.5.0', '49.0.0rc1'):
with self.subTest(pin=version):
result, notes = self.refresh(recorded(**{'49.0.0rc1': INTEL_AGAIN}), pins=table(version))
self.assertEqual(result, table(version))
self.assertEqual(notes, [('warning', f'cryptography {version} on osx-64 stays as it is, and may no '
f'longer hold: PyPI lists no stable release {version}, and '
'the newest release up to 52.0.0 with wheels for all of '
'cp311, cp312, cp313, cp314, cp315 there is 48.0.1')])

def test_a_pin_above_the_installed_release_is_not_called_lost(self) -> None:
# #2028: 49.0.0 is above the installed 48.5.0, so newest_wheel never weighed it.
self.assertEqual(self.refresh(recorded(), installed='48.5.0', pins=table('49.0.0')), (table('49.0.0'), []))

def test_a_package_not_installed_leaves_the_pin_and_warns(self) -> None:
result, notes = self.refresh(recorded(**{'51.0.0': INTEL_AGAIN}), installed=None)
self.assertEqual(result, table())
Expand Down Expand Up @@ -548,6 +593,35 @@ def test_a_stuck_pin_is_warned_about_and_passes(self) -> None:
'and may no longer hold: no release up to 52.0.0 has a wheel for cp38 '
'on osx-64\n')

def test_a_pin_whose_release_lost_a_wheel_is_warned_about_and_passes(self) -> None:
# #2028: the step stays green, as the workflow's non-blocking rule has it, but says so.
module = vendor()
with Scratch(table()) as scratch:
before = scratch.table.read_bytes()
module._fetch = lambda name: lost_wheel()
with mock.patch.object(module, 'conda_pythons', lambda: list(PYTHONS)):
status, output = scratch.main('--refresh-pins')
after = scratch.table.read_bytes()
self.assertEqual(status, 0)
self.assertEqual(after, before)
self.assertEqual(output, '::warning title=Platform pins::cryptography 48.0.1 on osx-64 stays as it is, '
'and may no longer hold: 48.0.1 has no wheel for cp311 on osx-64, and the newest '
'release up to 52.0.0 with wheels for all of cp311, cp312, cp313, cp314, cp315 '
'there is 47.0.0\n')

def test_a_pin_still_the_newest_to_qualify_passes_silently(self) -> None:
# #2028: 48.0.1 is the newest release with an osx-64 wheel for every Python, so it is not lost.
module = vendor()
with Scratch(table()) as scratch:
before = scratch.table.read_bytes()
module._fetch = lambda name: recorded(**{'47.0.0': files('47.0.0', 'cp311-abi3-macosx_10_9_universal2')})
with mock.patch.object(module, 'conda_pythons', lambda: list(PYTHONS)):
status, output = scratch.main('--refresh-pins')
after = scratch.table.read_bytes()
self.assertEqual(status, 0)
self.assertEqual(after, before)
self.assertEqual(output, '')


class CheckTests(unittest.TestCase):
"""``--check`` on a pin in date and past it, never reading the network."""
Expand Down
27 changes: 24 additions & 3 deletions util/conda_vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,20 @@ def stuck(project: 'dict[str, Any]', subdir: 'str', pythons: 'list[str]', have:
return f'no release up to {have} has wheels for all of {_tags(pythons)} on {subdir} at once'


def lost(project: 'dict[str, Any]', subdir: 'str', pythons: 'list[str]', pin: 'str') -> 'str':
"""Why the release ``pin`` names no longer qualifies on ``subdir``, for its warning (#2028).

It names the conda Pythons the pinned release has no wheel for there, or
says PyPI lists no such release, as :func:`_releases` weighs them.

"""
from packaging.version import Version # pylint: disable=import-outside-toplevel
wheels = next((files for version, _, files in _releases(project) if version == Version(pin)), None)
if wheels is None:
return f'PyPI lists no stable release {pin}'
return f'{pin} has no wheel for {_tags(lacking(wheels, subdir, pythons))} on {subdir}'


#: What :func:`_fetch` raises when PyPI cannot be read: :class:`OSError` for a
#: failed request, and :class:`ValueError` for a reply that is not JSON, as
#: requests releases before its own ``JSONDecodeError`` raise it.
Expand All @@ -636,9 +650,10 @@ def refresh(table: 'dict[str, dict[str, Pin]]', fetch: 'Callable[[str], dict[str
reaches the installed version that way, or already pins it, is dropped, as
the other platforms vendor that version. A package or pin it cannot settle
stays as it is, and so does a pin no release up to the installed version has
a wheel for every conda Python, which :func:`stuck` warns about (#2025). Each
note is a workflow command, ``notice`` for a change and ``warning`` for a pin
left alone, and its text.
a wheel for every conda Python, which :func:`stuck` warns about (#2025), or
whose own release no longer has one when an older release does, which
:func:`lost` warns about (#2028). Each note is a workflow command,
``notice`` for a change and ``warning`` for a pin left alone, and its text.

"""
from packaging.version import Version # pylint: disable=import-outside-toplevel
Expand Down Expand Up @@ -684,6 +699,12 @@ def refresh(table: 'dict[str, dict[str, Pin]]', fetch: 'Callable[[str], dict[str
# #2025: a pin no release can move to used to stay without a word.
notes.append(('warning', f'{name} {pin.version} on {subdir} stays as it is, and may no longer '
f'hold: {stuck(project, subdir, pythons, have)}'))
elif Version(newest) < current < ceiling:
# #2028: the pinned release itself lost a wheel, which used to pass without a word too.
notes.append(('warning', f'{name} {pin.version} on {subdir} stays as it is, and may no longer '
f'hold: {lost(project, subdir, pythons, pin.version)}, and the '
f'newest release up to {have} with wheels for all of {_tags(pythons)} '
f'there is {newest}'))
return {name: platforms for name, platforms in result.items() if platforms}, notes


Expand Down
Loading