diff --git a/Pipfile b/Pipfile index 7be3d8de14..952ad17747 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,8 @@ tbtrim = "~=0.3.1" pypcapkit = {editable = true,path = "."} pyshark = "*" dpkt = "*" -scapy = "*" +# The floor and marker match the Scapy extra in pyproject.toml (#1572). +scapy = {version = ">=2.6.0",markers = "python_version >= '3.7'"} cryptography = "*" # NB: ``pypcap`` is deliberately absent. It builds a C extension against # libpcap, so listing it here would break ``pipenv install --dev`` -- and even diff --git a/docs/source/index.rst b/docs/source/index.rst index c7d7fe568b..6d438f2353 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -436,9 +436,13 @@ preflight described under `Engine Comparison`_. .. note:: Only the four engines above carry extra constraints. :mod:`pcapkit` itself and - its ``default``, ``dpkt`` and ``scapy`` engines need nothing further -- - ``dpkt`` 1.9.8 and ``scapy`` 2.7.0 were both measured reading a capture on - Python 3.14. + its ``default``, ``dpkt`` and ``scapy`` engines need nothing beyond their + ``pip install`` -- ``dpkt`` 1.9.8 and ``scapy`` 2.7.0 were both measured + reading a capture on Python 3.14. The ``scapy`` engine needs **scapy 2.6.0 or + newer**, which the ``Scapy`` extra requires: 2.5.0 can drop a PCAP-NG + interface's timestamp resolution, reading nanoseconds as microseconds + (:issue:`1572`). :mod:`pcapkit` does not check the version at run time, so + upgrade a scapy installed some other way. For CLI usage, you will need to install the optional packages: diff --git a/pyproject.toml b/pyproject.toml index 01edd40316..13a0222e20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,7 +139,12 @@ crypto = [ "cryptography>=3.4" ] NGAP = [ "pycrate" ] # for normal users DPKT = [ "dpkt" ] -Scapy = [ "scapy" ] +# The floor is measured, #1572: on scapy 2.5.0 a PCAP-NG interface's +# ``if_tsresol`` is lost after a comment option without a newline, so a +# nanosecond interface reads as microseconds, and ``packet2dict`` reports DNS +# records and field order differently. 2.6.0, the next release, fixes all three. +# It needs Python 3.7, hence the marker, as ``PCAP_CT``'s below. +Scapy = [ "scapy>=2.6.0; python_version >= '3.7'" ] PyShark = [ "pyshark" ] # pypcap ships no wheels, so this compiles a C extension against libpcap and # needs both its headers and its shared library present; deliberately kept out @@ -255,7 +260,8 @@ all = [ "emoji", "cryptography>=3.4", "pycrate" ] # and it is not a place to add ``pypcap``/``pcap-ct`` just to silence a # finding CI has already decided to carry as advisory. dev = [ - "dpkt", "scapy", "pyshark", + # scapy: the same floor and marker as the Scapy extra above, #1572 + "dpkt", "scapy>=2.6.0; python_version >= '3.7'", "pyshark", # same marker and reasoning as the PyPCAPFile extra above and the old # ``all``: on 3.12+ the package installs cleanly and is then unusable, so # the marker keeps the resolver from fetching something that could never diff --git a/tests/test_tier_guard.py b/tests/test_tier_guard.py index fdf3ee4228..10d0ce404e 100644 --- a/tests/test_tier_guard.py +++ b/tests/test_tier_guard.py @@ -1055,6 +1055,20 @@ def test_dev_carries_what_all_used_to_for_the_toolchain(self) -> None: with self.subTest(absent=absent): self.assertFalse(_dependency_gates.provided_by(declared['dev'], absent)) + def test_scapy_is_floored_at_the_first_release_main_passes_on(self) -> None: + """#1572: ``Scapy`` and ``dev`` both require ``scapy>=2.6.0``. + + On 2.5.0 ``main`` reads a PCAP-NG nanosecond interface as microseconds and + ``packet2dict`` reports DNS records and field order differently; 2.6.0, the + next release, fixes all three. The marker is ``PCAP_CT``'s courtesy: 2.6.0 + needs Python 3.7, above this project's ``requires-python`` floor. + + """ + declared = _dependency_gates.declared_requirements() + floor = "scapy>=2.6.0; python_version >= '3.7'" + self.assertEqual([requirement.text for requirement in declared['Scapy']], [floor]) + self.assertIn(floor, [requirement.text for requirement in declared['dev']]) + def test_the_core_dependencies_are_read_from_project_not_build_system(self) -> None: """``[build-system] requires`` also holds ``setuptools``; this is not it.""" core = _dependency_gates.declared_requirements()[_dependency_gates.CORE]