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
9 changes: 6 additions & 3 deletions .github/workflows/cron-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ jobs:
# `.[all]`, which is what `util/conda_vendor.py` below vendors (#1814):
# it derives `conda/requirements.txt` from `dependencies` and the `all`
# extra, walks what they require through the installed metadata, and
# pins each package at the version installed here. So this install is
# the step that resolves the newest releases, and the script itself
# reads nothing from the network.
# pins each package at the version installed here, but for its
# `PLATFORM_PINS` (#1827). So this install is the step that resolves the
# newest releases, and the script itself reads nothing from the network.
- name: Install and Setup
run: |
set -x
Expand Down Expand Up @@ -459,6 +459,9 @@ jobs:
python -m pip install -U pip setuptools wheel
python -m pip install -e .[all]

# #1827: pip evaluates conda/requirements.txt's platform markers here,
# on this leg's own runner, so Intel macOS fetches its own cryptography
# pin; the recipe's build.sh installs whatever this downloaded.
- name: Setup Conda Distribution
if: ${{ matrix.os != 'windows-latest' }}
run: |
Expand Down
3 changes: 2 additions & 1 deletion conda/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
aenum==3.1.17
cffi==2.1.1
chardet==7.6.0
cryptography==50.0.2
cryptography==48.0.1; sys_platform == "darwin" and platform_machine == "x86_64"
cryptography==50.0.2; sys_platform != "darwin" or platform_machine != "x86_64"
dictdumper==0.8.4.post6
emoji==2.16.0
pycparser==3.11
Expand Down
168 changes: 168 additions & 0 deletions tests/project/test_conda_cryptography_pin_1827.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
"""cryptography is pinned apart on Intel macOS, which has no wheel of its newest releases (#1827).

* :file:`conda/requirements.txt` carries a cryptography line per platform pin,
and on each conda build exactly one of them applies: 48.0.1 on ``osx-64``,
the installed release elsewhere.
* :file:`util/conda_vendor.py` writes those lines from ``PLATFORM_PINS``, and
``--check`` passes on them and fails when the table entry or the file's pin
goes, when the pin moves, and when the table names a package or platform no
build has. All offline, on stand-in metadata.

"""

from __future__ import annotations

import contextlib
import importlib.util
import io
import pathlib
import re
import tempfile
import unittest
from typing import TYPE_CHECKING
from unittest import mock

try:
import tomllib
except ImportError: # pragma: no cover
tomllib = None # type: ignore[assignment]

if TYPE_CHECKING:
from typing import Any

ROOT = pathlib.Path(__file__).resolve().parents[2]
REQUIREMENTS = ROOT / 'conda' / 'requirements.txt'
INTEL = 'cryptography==48.0.1; sys_platform == "darwin" and platform_machine == "x86_64"'
#: The other line, whose pin the weekly Conda Update moves.
REST = re.compile(r'cryptography==(\S+); sys_platform != "darwin" or platform_machine != "x86_64"')

_MODULE = [] # type: list[Any]


def vendor() -> 'Any':
""":file:`util/conda_vendor.py`, loaded once; skipped without :mod:`tomllib`, which it imports."""
if tomllib is None: # pragma: no cover
raise unittest.SkipTest('tomllib, which util/conda_vendor.py imports, needs Python 3.11+')
if not _MODULE:
spec = importlib.util.spec_from_file_location('conda_vendor', ROOT / 'util' / 'conda_vendor.py')
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore[union-attr]
_MODULE.append(module)
return _MODULE[0]


def committed() -> 'list[str]':
"""The committed file's cryptography lines."""
return [line for line in REQUIREMENTS.read_text(encoding='utf-8').splitlines()
if line.startswith('cryptography')]


def cryptography(module: 'Any', envs: 'list[dict[str, str]]') -> 'dict[str, Any]':
"""What the conda builds vendor for ``pyproject.toml``'s cryptography line, on stand-in metadata."""
return module.derive([req for req in module.direct() if req.name == 'cryptography'], envs, lambda name: [])


class CommittedLinesTests(unittest.TestCase):
"""The committed cryptography lines, evaluated for each conda build."""

def test_each_build_reads_one_cryptography_line(self) -> None:
from packaging.requirements import Requirement
module = vendor()
lines = committed()
self.assertIn(INTEL, lines)
self.assertEqual(len([line for line in lines if REST.fullmatch(line)]), 1, lines)
self.assertEqual(module.PLATFORM_PINS, {'cryptography': {'osx-64': '48.0.1'}})
for env in module.environments(module.conda_pythons()):
subdir = next(subdir for subdir, (sys_platform, _, machine) in module.PLATFORMS.items()
if (sys_platform, machine) == (env['sys_platform'], env['platform_machine']))
with self.subTest(subdir=subdir, python=env['python_version']):
live = [line for line in lines if Requirement(line).marker.evaluate(env)]
self.assertEqual(len(live), 1, live)
self.assertEqual(live[0] == INTEL, subdir == 'osx-64', live)


class WriterTests(unittest.TestCase):
"""The writer emits a line per platform pin."""

def test_the_writer_emits_the_platform_lines(self) -> None:
module = vendor()
envs = module.environments(module.conda_pythons())
self.assertEqual(module.lines(cryptography(module, envs), envs, lambda name: '50.0.2'),
[INTEL, 'cryptography==50.0.2; sys_platform != "darwin" or platform_machine != "x86_64"'])

def test_a_python_marker_is_kept_beside_the_platform_one(self) -> None:
from packaging.requirements import Requirement
module = vendor()
envs = module.environments(['3.11', '3.12'])
found = module.derive([Requirement('cryptography; python_version >= "3.12"')], envs, lambda name: [])
lines = module.lines(found, envs, lambda name: '50.0.2')
self.assertEqual(lines, [
'cryptography==48.0.1; (python_version == "3.12") and '
'(sys_platform == "darwin" and platform_machine == "x86_64")',
'cryptography==50.0.2; (python_version == "3.12") and '
'(sys_platform != "darwin" or platform_machine != "x86_64")',
])
for env in envs:
with self.subTest(env=(env['python_version'], env['sys_platform'], env['platform_machine'])):
live = [line for line in lines if Requirement(line).marker.evaluate(env)]
self.assertEqual(len(live), env['python_version'] == '3.12')


class CheckTests(unittest.TestCase):
"""``--check`` against the committed cryptography lines."""

def problems(self, text: 'str') -> 'list[str]':
module = vendor()
envs = module.environments(module.conda_pythons())
return module.compare(text, cryptography(module, envs), envs)

def test_the_committed_lines_pass(self) -> None:
lines = committed()
self.assertIn(INTEL, lines)
self.assertEqual(self.problems('\n'.join(lines)), [])

def test_without_the_override_the_platform_lines_fail(self) -> None:
with mock.patch.object(vendor(), 'PLATFORM_PINS', {}):
problems = self.problems('\n'.join(committed()))
self.assertEqual(len(problems), 1, problems)
self.assertTrue(problems[0].startswith('marker: cryptography has ['), problems)

def test_a_dropped_or_moved_platform_pin_fails(self) -> None:
rest = [line for line in committed() if line != INTEL]
dropped = self.problems('\n'.join(rest))
self.assertEqual([problem.split(':')[0] for problem in dropped], ['marker'], dropped)
unmarked = self.problems('\n'.join(rest + ['cryptography==48.0.1']))
self.assertEqual([problem.split(':')[0] for problem in unmarked], ['marker'], unmarked)
moved = self.problems('\n'.join(rest + [INTEL.replace('48.0.1', '49.0.0')]))
self.assertEqual(moved, ["pin: cryptography pins '49.0.0' where sys_platform == \"darwin\" and "
"platform_machine == \"x86_64\", and PLATFORM_PINS '48.0.1'"])

def test_an_override_no_build_can_use_fails(self) -> None:
pins = {'cryptography': {'osx-64': '48.0.1', 'win-64': '1.0'}, 'gone': {'osx-64': '1.0'}}
with mock.patch.object(vendor(), 'PLATFORM_PINS', pins):
problems = self.problems('\n'.join(committed()))
self.assertEqual(problems, ['override: cryptography is pinned for win-64, which no conda leg builds',
'override: gone is pinned for osx-64, and no conda build vendors it'])

def test_the_writer_refuses_an_override_no_build_can_use(self) -> None:
module = vendor()
with tempfile.TemporaryDirectory() as temp:
target = pathlib.Path(temp) / 'requirements.txt'
saved = module.FILE, module.ROOT, module._requires, module._version
module.FILE, module.ROOT = target, pathlib.Path(temp)
module._requires = lambda name: []
module._version = lambda name: '1.0'
try:
with mock.patch.object(module, 'PLATFORM_PINS', {'gone': {'osx-64': '1.0'}}), \
contextlib.redirect_stderr(io.StringIO()) as errors:
self.assertEqual(module.main([]), 1)
self.assertFalse(target.exists())
self.assertIn('override: gone is pinned for osx-64', errors.getvalue())
finally:
module.FILE, module.ROOT, module._requires, module._version = saved


if __name__ == '__main__':
unittest.main()
22 changes: 13 additions & 9 deletions tests/project/test_conda_vendor_1814.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import textwrap
import unittest
import zipfile
from unittest import mock
from typing import TYPE_CHECKING

try:
Expand Down Expand Up @@ -211,13 +212,15 @@ def test_compare_passes_on_written_lines_and_names_each_stale_one(self) -> None:
module = vendor()
envs = module.environments(self.PYTHONS)
found = self.found('core', 'crypt', 'backport; python_version < "3.11"')
written = requirement_lines(*module.lines(found, envs, VERSIONS.__getitem__))
self.assertEqual(module.compare(written, found, envs), [])
repinned = written.replace('core==1.0', 'core==9.9')
self.assertEqual(module.compare(repinned, found, envs), [])
stale = requirement_lines('backport==1.0; python_version < "3.11"', 'core==1.0',
'crypt==1.0', 'ffi==1.0', 'helper==1.0', 'lxml==6.1.3')
problems = module.compare(stale, found, envs)
# The stand-in packages have no platform pins (#1827's are its own tests').
with mock.patch.object(module, 'PLATFORM_PINS', {}):
written = requirement_lines(*module.lines(found, envs, VERSIONS.__getitem__))
self.assertEqual(module.compare(written, found, envs), [])
repinned = written.replace('core==1.0', 'core==9.9')
self.assertEqual(module.compare(repinned, found, envs), [])
stale = requirement_lines('backport==1.0; python_version < "3.11"', 'core==1.0',
'crypt==1.0', 'ffi==1.0', 'helper==1.0', 'lxml==6.1.3')
problems = module.compare(stale, found, envs)
self.assertEqual([problem.split(':')[0] + ':' + problem.split()[1] for problem in problems],
['dead:backport', 'stale:lxml', 'missing:parser'])

Expand All @@ -237,8 +240,9 @@ def test_every_live_direct_requirement_is_listed_and_no_dead_one(self) -> None:
self.assertNotIn(name, have)
continue
self.assertIn(name, have)
marker = module.render_marker(entry['builds'], envs)
self.assertEqual(have[name], None if marker is None else str(Marker(marker)))
# #1827: a line per platform pin, each with its own marker.
self.assertEqual(set(have[name]), {None if marker is None else str(Marker(marker))
for marker, _ in module.pins(name, entry, envs)})

def test_check_passes_on_its_own_output_and_fails_on_a_stale_file(self) -> None:
module = vendor()
Expand Down
Loading
Loading