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
7 changes: 5 additions & 2 deletions docs/source/contributing/conventions/process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ On the tree, in :file:`pyproject.toml`:
.. code-block:: toml

cli = [ "emoji" ]
crypto = [ "cryptography>=3.4" ]
crypto = [
"cryptography>=3.4; platform_python_implementation != 'PyPy' or python_version >= '3.11'",
"cryptography>=3.4,<46; platform_python_implementation == 'PyPy' and python_version < '3.11'",
]
NGAP = [ "pycrate" ]

all = [ "emoji", "cryptography>=3.4", "pycrate" ]
all = [ "emoji", "cryptography>=3.4; platform_python_implementation != 'PyPy' or python_version >= '3.11'", "cryptography>=3.4,<46; platform_python_implementation == 'PyPy' and python_version < '3.11'", "pycrate" ]

So ``all`` is exactly the union of those extras, written out as literals rather
than referenced.
Expand Down
5 changes: 4 additions & 1 deletion docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,10 @@ so it adds no context to the ruleset. No ``Compat``, ``engine-tests`` or
``pypcap-parity`` leg runs PyPy. On PyPy 8.0, which setup-python's ``pypy3.11``
is, PyPI has no Linux wheel of |cryptography|_, so the install step builds it
with the image's Rust and ``libssl-dev``, and none of ``lxml``, so the ``test``
extra carries ``lxml`` on CPython only.
extra carries ``lxml`` on CPython only. On ``pypy3.10`` the ``crypto`` and ``all``
extras cap |cryptography|_ below 46, as 45.0.7 is the newest release with a Linux
``pp310`` wheel and the newest release's sdist does not build there (`#2040
<https://github.com/JarryShaw/PyPCAPKit/issues/2040>`__).

On PyPy an xdist worker's RSS grows with every test it runs, rather than in a few
heavy modules, so a PyPy leg's ``Run unit tests`` hands its pytest command to
Expand Down
22 changes: 20 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,18 @@ pcapkit-vendor = "pcapkit.vendor.__main__:main"
# for CLI display
cli = [ "emoji" ]
# for ESP payload decryption, c.f. pcapkit.protocols.internet.esp
crypto = [ "cryptography>=3.4" ]
#
# Capped below 46 on PyPy older than 3.11 (#2040), and nowhere else, which the
# complementary marker on the first line keeps. 45.0.7 is the newest release
# with a manylinux ``pp310`` wheel: 46.0.0 through 46.0.3 ship ``pp310`` wheels
# for macOS x86_64 and Windows only, and 46.0.4 onwards none at all. Without the
# cap, pip on a Linux PyPy 3.10 builds the newest release's sdist, and that
# build stops at PyO3's minimum supported version, 3.11. ``all`` below repeats
# both lines.
crypto = [
"cryptography>=3.4; platform_python_implementation != 'PyPy' or python_version >= '3.11'",
"cryptography>=3.4,<46; platform_python_implementation == 'PyPy' and python_version < '3.11'",
]
# for NGAP decoding, c.f. pcapkit.protocols.application.ngap. Counted among
# ``all``'s *core addons* by the owner's ruling on #910 -- everything needed for
# the library itself to work at full functionality -- even though the
Expand Down Expand Up @@ -260,7 +271,14 @@ vendor = [ "requests[socks]", "beautifulsoup4[html5lib]", "pycrate" ]
# only as pre-releases, and ``all`` should not be how somebody ends up with a
# beta they did not ask for. Install either explicitly: ``pip install
# pypcapkit[PyPCAP]`` or ``pip install pypcapkit[PCAP_CT]``.
all = [ "emoji", "cryptography>=3.4", "pycrate" ]
#
# The two ``cryptography`` lines are the ``crypto`` extra's, cap included (#2040).
all = [
"emoji",
"cryptography>=3.4; platform_python_implementation != 'PyPy' or python_version >= '3.11'",
"cryptography>=3.4,<46; platform_python_implementation == 'PyPy' and python_version < '3.11'",
"pycrate",
]
# Not for an end user -- for CI, which has a different need entirely: pylint,
# mypy and Sphinx's autodoc all resolve imports against whatever is installed,
# so narrowing ``all`` above would otherwise have made every one of
Expand Down
100 changes: 100 additions & 0 deletions tests/project/test_pypy310_cryptography_cap_2040.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""#2040: the ``crypto`` and ``all`` extras cap ``cryptography`` on PyPy below 3.11, and only there.

45.0.7 is the newest ``cryptography`` with a manylinux ``pp310`` wheel. Without
the cap pip on the ``pypy3.10`` leg builds the newest release's sdist, which
fails, so the leg never runs a test. Each environment below gets exactly one live
``cryptography`` requirement from each extra, read from :file:`pyproject.toml`
and evaluated with :mod:`packaging.markers`: capped on PyPy 3.10, uncapped on
PyPy 3.11 and on CPython 3.10.

"""

from __future__ import annotations

import pathlib
import unittest

from packaging.requirements import Requirement
from packaging.version import Version

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

#: Test tier (#1958): reads pyproject.toml, which is the same on every interpreter.
__tier__ = 'LATEST'

ROOT = pathlib.Path(__file__).resolve().parents[2]
PYPROJECT = ROOT / 'pyproject.toml'

#: The newest release with a manylinux ``pp310`` wheel, and the first without one.
LAST_PP310 = Version('45.0.7')
FIRST_WITHOUT = Version('46.0.0')
#: The release ``pypy3.10`` resolved to before the cap, whose sdist fails there.
NEWEST = Version('50.0.2')


def env(implementation: 'str', python: 'str') -> 'dict[str, str]':
"""A Linux x86_64 marker environment for ``implementation`` at ``python``."""
return {
'implementation_name': implementation.lower(),
'platform_python_implementation': implementation,
'python_version': python,
'python_full_version': f'{python}.0',
'implementation_version': f'{python}.0',
'sys_platform': 'linux', 'platform_system': 'Linux', 'os_name': 'posix',
'platform_machine': 'x86_64', 'platform_release': '', 'platform_version': '',
'extra': '',
}


ENVS = {
'PyPy 3.10': env('PyPy', '3.10'),
'PyPy 3.11': env('PyPy', '3.11'),
'CPython 3.10': env('CPython', '3.10'),
}


@unittest.skipIf(tomllib is None, 'tomllib, which reads pyproject.toml, needs Python 3.11+')
class TestCryptographyCap(unittest.TestCase):
"""Which ``cryptography`` each extra asks for, per interpreter."""

extras = {} # type: dict[str, list[str]]

@classmethod
def setUpClass(cls) -> 'None':
cls.extras = tomllib.loads(PYPROJECT.read_text(encoding='utf-8'))['project']['optional-dependencies']

def live(self, extra: 'str', name: 'str') -> 'Requirement':
"""The one ``cryptography`` requirement of ``extra`` live on ``ENVS[name]``."""
found = [requirement for requirement in map(Requirement, self.extras[extra])
if requirement.name == 'cryptography'
and (requirement.marker is None or requirement.marker.evaluate(ENVS[name]))]
self.assertEqual(len(found), 1,
f'{extra} has {len(found)} live cryptography requirements on {name}, '
f'not one: {[str(requirement) for requirement in found]}')
return found[0]

def test_pypy310_is_capped_at_the_last_release_with_a_pp310_wheel(self) -> 'None':
for extra in ('crypto', 'all'):
with self.subTest(extra=extra):
specifier = self.live(extra, 'PyPy 3.10').specifier
self.assertIn(LAST_PP310, specifier)
self.assertIn(Version('3.4'), specifier)
self.assertNotIn(FIRST_WITHOUT, specifier)
self.assertNotIn(NEWEST, specifier)

def test_every_other_interpreter_stays_uncapped(self) -> 'None':
for extra in ('crypto', 'all'):
for name in ('PyPy 3.11', 'CPython 3.10'):
with self.subTest(extra=extra, env=name):
specifier = self.live(extra, name).specifier
self.assertIn(NEWEST, specifier)
self.assertIn(Version('3.4'), specifier)
self.assertNotIn(Version('3.3'), specifier)


if __name__ == '__main__':
unittest.main()
7 changes: 5 additions & 2 deletions tests/test_tier_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1428,8 +1428,11 @@ def test_the_extras_read_back_what_pyproject_declares(self) -> None:
"""Spot checks on the extras the rest of this suite reasons about."""
declared = _dependency_gates.declared_requirements()

self.assertEqual([requirement.text for requirement in declared['crypto']],
['cryptography>=3.4'])
# Two lines since #2040: the second caps PyPy below 3.11.
self.assertEqual(
[requirement.text for requirement in declared['crypto']],
["cryptography>=3.4; platform_python_implementation != 'PyPy' or python_version >= '3.11'",
"cryptography>=3.4,<46; platform_python_implementation == 'PyPy' and python_version < '3.11'"])
self.assertTrue(_dependency_gates.provided_by(declared['NGAP'], 'pycrate'))
self.assertTrue(_dependency_gates.provided_by(declared['cli'], 'emoji'))
# The `test` extra carries requests and bs4 (#507) but not html5lib.
Expand Down
Loading