Skip to content

corekit: raise FieldValueError, not a bare ValueError, for a malformed IP field value - #470

Merged
JarryShaw merged 3 commits into
mainfrom
fix-465-ipaddress-field-value-error
Sep 18, 2026
Merged

JarryShaw merged 3 commits into
mainfrom
fix-465-ipaddress-field-value-error

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Closes #465.

pcapkit/corekit/fields/ipaddress.py let a bare stdlib ValueError escape when
an address value was malformed, while the very next statement in the same method
raised the library's own FieldValueError for a value that was merely the wrong
IP version. So except BaseError could not catch a bad field value: whether the
exception was in-library depended on how the value was wrong.

Before / after

All four public classes, malformed value into pre_process:

before:  builtins.ValueError                       isinstance(e, BaseError) is False
after:   pcapkit.utilities.exceptions.FieldValueError   isinstance(e, BaseError) is True
         e.g. "invalid IP address: 'not-an-address' does not appear to be an IPv4 or IPv6 address"

The original stdlib message is preserved rather than replaced. The wrong-version
path is unchanged and still reports IP version mismatch: 4 != 6, so the fix did
not broaden into relabelling one failure as the other — there is a test pinning
exactly that.

One site reachable from the wire, which #465 did not describe

The issue listed the post_process sites as "same pattern, reachability not
demonstrated". One of them is reachable, and from wire bytes alone rather
than from a malformed caller argument:

IPv4InterfaceField().unpack(b'\x01\x02\x03\x04\x00\xff\x00\xff', {})
before:  ValueError: '1.2.3.4/0.255.0.255' does not appear to be an IPv4 or IPv6 interface
after:   FieldValueError: invalid IPv4 interface: '1.2.3.4/0.255.0.255' does not appear to be ...

IPv4InterfaceField.post_process builds ip_interface(f'{ip}/{mask}') from the
trailing four octets as a netmask. If those octets are not a contiguous netmask
0.255.0.255 here — ip_interface() raises NetmaskValueError, a ValueError
subclass. The input is a well-formed eight octets, so this needs no malformed
length and no caller error: a capture containing such a field raises a bare
stdlib exception out of the parse path. That makes this issue more serious than
filed.

Reachability, judged per site

Reachable and demonstrated: _IPAddressField.pre_process; both interface
pre_process methods; and IPv4InterfaceField.post_process's ip_interface
call above.

Judged not reachable, with reasoning: the ip_address/IPv4Address/
IPv6Address conversions in the post_process methods, because value there is
always exactly 4 or 16 octets — fixed by struct.unpack's '4s'/'16s'
template, which pads or truncates before this code runs — and every such octet
string converts cleanly (checked with all-zero, all-0xff and sequential byte
patterns). And IPv6InterfaceField.post_process's ip_interface call, which
only runs after the existing prefixlen > 128 guard, and every prefix length in
0..128 is valid.

All sites are wrapped regardless, reachable or not — uniform treatment, on the
grounds that the asymmetry between adjacent lines is precisely what caused this
bug.

How it is wrapped

A module-level @contextmanager helper, _reraise_as_field_value_error, rather
than eight inline try/except blocks. Each with block spans only the raw
ipaddress.* conversion, never a following raise FieldValueError(...), so the
re-wrapping trap cannot arise by construction; the helper additionally puts
except FieldValueError: raise ahead of except ValueError as a guard against a
future edit widening a block, with an inline note pointing at the same trap
ProtocolError carries.

Raises: sections were added or extended on all six touched methods, including
for pre-existing FieldValueError raises that were previously undocumented. The
Sphinx page uses autoclass :members:, so no .rst change is needed.

Verification

  • Three new tests in tests/corekit/test_fields_ipaddress.py, each confirmed to
    fail against the reverted source and pass against the fix.
    tests/protocols/test_option_roundtrip_unit.py untouched.
  • Full suite, Python 3.14.7: before 1006 passed / 17 skipped / 1553 subtests;
    after 1009 / 17 / 1557 — exactly the three new tests. Zero failures either side.
  • mypy: 125 errors in 40 files, identical before and after, logs diffed
    directly. The only movement is two pre-existing [return-value] errors shifting
    line number because of added lines.

Related, deliberately not fixed

Both interface post_process methods return the result of ipaddress.ip_interface(...),
typed IPv4Interface | IPv6Interface, against declared return types of
IPv4Interface and IPv6Interface respectively — the two pre-existing mypy
[return-value] errors mentioned above. Pre-existing on main and unrelated to
this issue, so filed separately rather than folded in.


Corrections to this description

Three things above are wrong or understated. The review found the first two; I
verified all three. Leaving the original text in place because the review cites
it.

1. The padding is not struct.unpack's doing. The reachability section
credits "struct.unpack's '4s'/'16s' template, which pads or truncates". It
does not — the padding happens one level out, in plain Python, at
pcapkit/corekit/fields/field.py:244:

value = struct.unpack(self.template, buffer[:length].rjust(length, b'\x00'))[0]

buffer[:length] truncates and .rjust(length, b'\x00') left-pads, both before
struct.unpack sees anything. The conclusion is unaffected, and the review
established it more strongly than this description claimed: address fields were
fed buffers of length 0, 1, N-1, N, N+1 and N+10 through .unpack() and none
failed — so the post_process conversions are unreachable for any buffer
length, not merely for well-formed wire bytes.

2. "Each was confirmed to fail against reverted source" is true for two of the
three tests, not all three.
test_pre_process_malformed_value_raises_in_library_error
and test_ipv4_interface_post_process_rejects_a_non_contiguous_netmask do fail
before and pass after. test_wrong_version_message_is_not_relabelled_as_a_malformed_value
passes unchanged against the reverted source — it pins pre-existing,
untouched behaviour, which is a legitimate thing for it to do, but it is a
guard rather than a regression test and this description should not have implied
otherwise.

3. The non-contiguous-netmask case is near-universal, not a corner. Described
above only as "a non-contiguous netmask", which undersells it: of 2000 random
four-byte masks, 0 were contiguous and 2000 raised. Only the 33 canonical
contiguous masks out of 2^32 possible byte patterns avoid it, so essentially any
mask field that is not a real netmask reaches the defect this PR fixes.

…d IP field value

_IPAddressField.pre_process let a bare ValueError from ipaddress.ip_address()
escape when a value was malformed, one line above where it already raises
the library's own FieldValueError for a value that is merely the wrong IP
version -- so `except BaseError` could not reliably catch a bad field value;
whether the exception was in-library depended on how the value was wrong.
The same unguarded-conversion pattern was in every pre_process/post_process
of this module. Fixes #465.

- pcapkit/corekit/fields/ipaddress.py: adds a _reraise_as_field_value_error
  context manager and wraps every ipaddress.* conversion in it, preserving
  the original message via `from error`. Fixes two independently-confirmed
  defects: the malformed-value escape in all four public classes'
  pre_process (as reported), and a second one found while checking
  reachability of the post_process sites -- IPv4InterfaceField.post_process
  raised the same bare ValueError when the wire's trailing four "netmask"
  octets are not a contiguous netmask (e.g. 0.255.0.255), reachable through
  unpack() alone. The remaining post_process sites are not reachable --
  wire bytes there are always exactly the field's fixed length, and any
  such octet string converts cleanly, or the value is already range-checked
  before use -- but are wrapped too for consistency. Raises: sections
  added/extended to match.
- tests/corekit/test_fields_ipaddress.py: new cases pin FieldValueError
  (and BaseError) for a malformed value on all four public field classes,
  a case for the newly found post_process netmask defect, and a case
  pinning that the pre-existing wrong-version FieldValueError message is
  not relabelled as a malformed-value message.

Full suite: 1009 passed, 17 skipped, 1557 subtests, at PYTHONSAFEPATH=1,
interpreter 3.14.7. Baseline at fa12895 (origin/main): 1006 passed, 17
skipped, 1553 subtests, before the 3 new tests existed. mypy (Makefile's
invocation) reports the same 125 errors in 40 files before and after --
the only ipaddress.py lines it flags are two pre-existing return-value
errors, unrelated to this change and merely shifted by the new lines.
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review of #470 (sha b5f33f85b), on behalf of Copilot

What I read

1. Reachability

Confirmed correct for all four public classes, and more robust than the PR body states. The PR body attributes the "always exactly 4/16 octets" guarantee to "struct.unpack's 4s/16s template, which pads or truncates first" — that's not quite the mechanism. struct.unpack itself does not pad or truncate a '4s'/'16s' template; given a buffer of the wrong size it raises struct.error. The actual padding/truncation happens one line earlier, in plain Python, at pcapkit/corekit/fields/field.py:244:

value = struct.unpack(self.template, buffer[:length].rjust(length, b'\x00'))[0]

buffer[:length] truncates an over-long buffer, .rjust(length, b'\x00') left-pads a short one, before struct.unpack ever runs — so post_process always receives exactly length bytes regardless of what was actually on the wire. I verified this empirically rather than just reading it: calling .unpack() on IPv4AddressField/IPv6AddressField with buffers of length 0, 1, length-1, length, length+1, length+10 never raised (all converted cleanly), confirming :88's address post_process conversion is unreachable — not just for "well-formed wire bytes" as claimed, but for any input length. Same for IPv6InterfaceField.post_process: feeding all 256 possible trailing-byte values through .unpack(), prefixlen 129-255 is caught by the explicit if prefixlen > 128: raise FieldValueError(...) guard (ipaddress.py:370-371), and the wrapped ip_interface(f'{ip}/{prefixlen}') call at ipaddress.py:373-374 never itself raised for any of 0-128 — confirmed unreachable, exactly as documented.

The one reachable site, IPv4InterfaceField.post_process's ip_interface(f'{ip}/{mask}') at ipaddress.py:282-283, is reachable far more easily than "a malformed capture" suggests: 2000 random 4-byte masks through .unpack() produced 0 successes and 2000 FieldValueErrors — a uniformly-random mask is essentially never a contiguous netmask (only 33 of 2^32 values are), so this isn't an edge case, it's close to the common case for any mask that isn't hand-crafted to be valid.

Public constructors (IPv4AddressField.__init__ etc., ipaddress.py:163-189, 220-224, 304-308) hardcode length/_template and take no length parameter, so the "callable length" path documented on _IPInterfaceField (an internal, non-public class) cannot be reached through any of the four exported classes.

2. Scoping

Read all 8 wrap sites' indentation and confirmed every raise FieldValueError(...) for a version mismatch sits at the same indent level as its neighboring with block (a sibling statement), never inside it — ipaddress.py:115-119 (_IPAddressField.pre_process), :141-144, :244-247 (IPv4InterfaceField.pre_process), :278-285, :328-331 (IPv6InterfaceField.pre_process), :366-376. Verified empirically too, not just by reading indentation: forcing every wrong-version path that runs through a with block (IPv6AddressField.pre_process(IPv4Address(...)), IPv4InterfaceField.pre_process(IPv6Interface(...)), and the string forms IPv6InterfaceField.pre_process('1.2.3.4/24') / IPv4InterfaceField.pre_process('::1/64')) all produced exactly 'IP version mismatch: X != Y' with no "invalid IP ..." prefix and no double-wrapping. The except FieldValueError: raise backstop at ipaddress.py:58-60 is genuinely a backstop here, not doing load-bearing work — scoping alone is correct at every site.

3. Message quality

Malformed-value and wrong-version are clearly distinguishable by prefix ("invalid IP address: ..." / "invalid IP interface: ..." / "invalid IPv4 address: ..." etc. vs "IP version mismatch: X != Y"), and the original stdlib text survives in every case. One inconsistency, cosmetic rather than functional: pre_process sites use version-generic descriptions ('invalid IP address', 'invalid IP interface'ipaddress.py:115, 244, 328) even though IPv4InterfaceField/IPv6InterfaceField are separate methods that know their own version, while post_process sites use version-qualified descriptions ('invalid IPv4 address', 'invalid IPv4 interface', 'invalid IPv6 address', 'invalid IPv6 interface'ipaddress.py:141, 278, 282, 366, 373). Also, IPv4InterfaceField.post_process's single with block at :278-280 covers both the ip and mask conversions under one description ('invalid IPv4 address'), so a bad mask octet string would be reported as an "invalid IPv4 address" rather than "invalid IPv4 netmask" — harmless (the octets truly can't fail per item 1) but slightly imprecise if it ever did. Neither issue obscures malformed-value vs wrong-version, which was the actual bug.

4. Docstrings / Sphinx

git show fa128959e:pcapkit/corekit/fields/ipaddress.py shows the baseline had exactly one pre-existing Raises: block, on IPv6InterfaceField.post_process (old line 272, for the prefix-length check only). The other five touched methods had none — confirming the PR's claim that Raises: was newly added to five methods (documenting previously-undocumented FieldValueError for the version-mismatch case, now also the malformed-value case) and extended on the sixth. docs/source/pcapkit/corekit/fields/ipaddress.rst:9-26 uses .. autoclass:: + :members: for all four public classes, and docs/source/conf.py:69,123 enables sphinx.ext.napoleon with napoleon_google_docstring = True — the exact style used here, and the same mechanism that already rendered the one pre-existing Raises: block. I attempted an actual sphinx-build of this page to confirm end-to-end rather than relying on structure alone; the build read sources up to pcapkit/const/reg and then sat at 100% CPU making no further progress for 7+ minutes, unrelated to anything in this diff, and I killed it rather than continue burning time on it. So this item rests on the structural check above, not a rendered page.

5. Test quality — redone independently

Applied git diff fa128959e b5f33f85b -- pcapkit/corekit/fields/ipaddress.py in reverse (source-only revert, tests left at PR head), ran tests/corekit/test_fields_ipaddress.py, then re-applied the patch and confirmed git diff --stat HEAD was empty again afterward.

Against the reverted source: 5 failed, 11 passed, 13 subtests passed (pytest tests/corekit/test_fields_ipaddress.py -v). The failures are test_pre_process_malformed_value_raises_in_library_error (all 4 subTests, test_fields_ipaddress.py:154) and test_ipv4_interface_post_process_rejects_a_non_contiguous_netmask (:206) — both fail-then-pass exactly as claimed, and both exercise the actual fix.

The third test does not. Isolating test_wrong_version_message_is_not_relabelled_as_a_malformed_value (test_fields_ipaddress.py:187) against the reverted source: PASSED. It isn't in the PR's own "confirmed to fail" set in practice, because the wrong-version FieldValueError and its message predate this fix entirely — the test pins behavior the fix does not touch. It's a legitimate regression-guard (worth keeping), but the PR body's claim that "each was confirmed to fail against the reverted source, and pass against the fix" is not accurate for this one. Test coverage of the actual fix (the two sites that do fail-then-pass) is solid regardless.

Numbers, measured independently

All runs via /local/home/jarryx/GitHub/PyPCAPKit/.venv/bin/python (3.14.7) with PYTHONPATH forced to this worktree; confirmed pcapkit.__file__ resolved into the worktree before each measurement.

  • Full suite, b5f33f85b: 1009 passed, 17 skipped, 1557 subtests passed in 851.44s, 0 failed.
  • Full suite, fa128959e (main): 1006 passed, 17 skipped, 1553 subtests passed in 812.42s, 0 failed. Delta is exactly the three new tests, both sides clean — matches the claim.
  • mypy, both trees: Found 125 errors in 40 files on both. Full diff of the two logs shows exactly two lines differing, both pcapkit/corekit/fields/ipaddress.py [return-value] errors shifting line number (216→286, 291→377) because of added lines — identical error set otherwise. Matches the claim exactly; both are the pre-existing IPv4Interface | IPv6Interface vs declared-type mismatch, unrelated to this fix.
  • CI (gh pr checks 470): all real checks pass — Compat/Python/Integration Python 3.10 through 3.15, Analyze, CodeQL, deploy-pages, and pyup.io/safety-ci (the blank-rollup StatusContext). Docs test gate and Gate (full suite, Python 3.14) show skipping, consistent with skipping-by-design on pull_request events rather than a failure.

Verdict

No blocking defects. The reachability judgements hold (and in the address-field case, hold more broadly than the PR body's own mechanism description — worth a follow-up sentence fix, not a merge blocker). Scoping is correct everywhere, confirmed by reading and by forcing every version-mismatch-through-a-with-block path directly. Messages cleanly distinguish malformed-value from wrong-version, with only cosmetic prefix inconsistencies. Docstrings match a confirmed-empty-before baseline and render through an already-proven Sphinx mechanism. Two of the three new tests independently fail-then-pass against a real revert; the third is a legitimate but non-discriminating regression guard, and the PR body overstates it — worth a one-line correction in the description, not a merge blocker. Full-suite and mypy numbers both reproduced exactly.

GOOD TO MERGE, sha b5f33f85b.

Comment thread pcapkit/corekit/fields/ipaddress.py Outdated
@JarryShaw

Copy link
Copy Markdown
Owner Author

Re-review of #470 at sha c0f66ab43 — closing the gap since the b5f33f85b verdict

The prior GOOD TO MERGE verdict named b5f33f85b. Two things landed since: the owner-requested
contextlib import-style change, and a merge of main. This review's job was to confirm those are
all that changed and that neither reopens anything the prior review already closed — not to
re-review the whole PR.

What changed, confirmed by git rather than by the PR description

$ git log --oneline b5f33f85b..c0f66ab43
c0f66ab43 Merge branch 'main' into fix-465-ipaddress-field-value-error
8273f3575 corekit: import contextlib rather than its contextmanager name (#465)
5182ad0ce Merge pull request #471 from JarryShaw/fix-439-schema-abc-cache
... (afaec7f8b, dc3823c01, 1fc3860c0, 6b5505fb2, c4cee6786, 207c5ec10, 7bcb58d59, e62a24095,
     e6f4f758c, cce86ca4c — all merge-in commits from main's #471/#466, not this branch's own work)

git diff b5f33f85b c0f66ab43 -- pcapkit/corekit/fields/ipaddress.py is exactly the two-line change:

 import abc
+import contextlib
 import ipaddress
-from contextlib import contextmanager
 from typing import TYPE_CHECKING, Generic, TypeVar, cast
...
-@contextmanager
+@contextlib.contextmanager
 def _reraise_as_field_value_error(description: str) -> 'Iterator[None]':

git diff b5f33f85b c0f66ab43 -- tests/corekit/test_fields_ipaddress.py produced no output
byte-identical. So the delta really is exactly the one commit the owner asked for, nothing more.

And git diff --stat origin/main..origin/fix-465-ipaddress-field-value-error still shows only the
branch's own two files (pcapkit/corekit/fields/ipaddress.py, tests/corekit/test_fields_ipaddress.py,
174 insertions / 11 deletions total) — the merge did not leak anything into the diff against main.

1. The contextlib change — behaviour-neutral and complete

git show origin/fix-465-ipaddress-field-value-error:pcapkit/corekit/fields/ipaddress.py | grep -n 'contextmanager\|contextlib' returns exactly two lines: 5:import contextlib and
32:@contextlib.contextmanager. No bare contextmanager name is left anywhere in the file (no
from contextlib import contextmanager, no unqualified @contextmanager).

Verified the decorated function still behaves as a context manager, independently of the test suite:

context manager object: <class 'contextlib._GeneratorContextManager'> True True
OK: with-block executed cleanly with no error
OK: ValueError inside with-block became FieldValueError / msg: ctx-test: boom

contextlib._GeneratorContextManager with both __enter__/__exit__ present, and a ValueError
raised inside the with-block still translates to FieldValueError — identical behaviour to the
verdicted sha, just re-spelled per house style (import contextlib over from contextlib import contextmanager).

2. The merge of main — genuine pass-through

git show c0f66ab43 --stat (the merge commit, first parent 8273f3575) touches:
docs/source/pcapkit/protocols/internet/hip.rst, pcapkit/protocols/schema/internet/hip.py,
pcapkit/protocols/schema/misc/pcapng.py, pcapkit/protocols/schema/schema.py, and four test files
under tests/protocols/ — all from main's #471 (schema ABCMeta cache) and #466 (HIP list-length).
None of it is pcapkit/corekit/fields/ipaddress.py or tests/corekit/test_fields_ipaddress.py. No
conflict markers, no working-tree dirt (git status clean at c0f66ab43). Confirmed a genuine
pass-through, not a hand-resolved conflict touching the branch's own files.

3. The fix still holds at c0f66ab43

FieldValueError(BaseError, ValueError) at pcapkit/utilities/exceptions.py:372, confirmed
unchanged. Ran this directly against the checked-out c0f66ab43 tree (worktree root forced onto
sys.path, pcapkit.__file__ printed and asserted to resolve into the worktree before importing):

pcapkit.__file__= .../agent-ada2c40d6c3503cc4/pcapkit/__init__.py
OK malformed IPv4 address -> FieldValueError / isinstance BaseError: True / msg: invalid IP address: 'not-an-address' does not appear to be an IPv4 or IPv6 address
OK version-mismatch -> FieldValueError / msg: IP version mismatch: 4 != 6
OK non-contiguous netmask -> FieldValueError / msg: invalid IPv4 interface: '1.2.3.4/0.255.0.255' does not appear to be an IPv4 or IPv6 interface

All three raise FieldValueError, never a bare ValueError — the malformed-address path, the
pre-existing version-mismatch path, and the IPv4InterfaceField.post_process non-contiguous-netmask
path (the reachable defect from #465) all land in-library. Every ipaddress.* conversion call in the
file is wrapped in _reraise_as_field_value_error; every version-mismatch raise FieldValueError
sits at a dedent outside its neighboring with block, so the except FieldValueError: raise guard
ahead of except ValueError is defensive rather than load-bearing anywhere in this file today — but
it's correctly ordered (FieldValueError before ValueError, since it subclasses it), matching the
same ordering trap ProtocolError carries at exceptions.py.

4. tests/corekit/test_fields_ipaddress.py, run myself

Fixtures generated first (PYTHONPATH=<worktree> python examples/generators/make_samples.py, 18
captures written) to avoid spurious FileNotFoundErrors, then:

$ python -m pytest tests/corekit/test_fields_ipaddress.py -v
...
12 passed, 1 warning, 17 subtests passed in 9.81s

Matches the 12 passed / 17 subtests reported for this change. Also ran the whole tests/corekit/
directory for a wider blast-radius check (this directory is exactly what the merged schema/ABCMeta
change from main could plausibly have disturbed): 62 passed, 1 warning, 21 subtests passed in 31.77s — clean. And pytest --collect-only -q over the full suite: 1040 tests collected, no
import errors.

5. Netmask handling

Nothing new to flag beyond what the prior review already established. The fixed site
(IPv4InterfaceField.post_process's ipaddress.ip_interface(f'{ip}/{mask}') at ipaddress.py:283)
is the one genuinely reachable conversion in the module, and it's now wrapped and tested
(test_ipv4_interface_post_process_rejects_a_non_contiguous_netmask, confirmed passing above). The
0-of-2000-random-masks-contiguous context (only 33 of 2**32 possible 4-byte values are a valid
dotted netmask) is exactly why this is close to the common case for any corrupted wire mask, not an
edge case — underscoring that this fix matters, not identifying anything additional to fix. I did
not find any remaining unwrapped ipaddress.* call, and did not find any place where a
version-mismatch raise sits inside a with _reraise_as_field_value_error(...) block (which would
have made the guard load-bearing and worth double-checking harder) — all eight sites keep it
outside.

CI

$ gh pr view 470 --json headRefOid,baseRefName,mergeable --jq '{headRefOid,baseRefName,mergeable}'
{"headRefOid":"c0f66ab431b67a871e3802b7b6a486a35a07ace2","baseRefName":"main","mergeable":"MERGEABLE"}

$ gh pr view 470 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.__typename=="CheckRun")|.conclusion//.status]|group_by(.)|map("\(.[0]):\(length)")|join(", ")'
SKIPPED:2, SUCCESS:21

$ gh pr view 470 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.__typename!="CheckRun")]'
[{"__typename":"StatusContext","context":"pyup.io/safety-ci","state":"SUCCESS", ...}]

21 CheckRun SUCCESS + 2 SKIPPED (Docs test gate / Gate (full suite ...), skip-by-design on
pull_request) + pyup.io/safety-ci StatusContext SUCCESS — fully green at c0f66ab43.

Verdict

The two changes since b5f33f85b are exactly what the task description says they are, confirmed by
diffing the two shas directly rather than trusting the description: a two-line import-style edit with
no behavioural change, and a merge of main that is a clean pass-through touching none of this
branch's own files. The fix itself is unchanged and still verified at head — both defect paths raise
in-library FieldValueError, never a bare ValueError, tests/corekit/test_fields_ipaddress.py
passes 12/17 (subtests), the wider tests/corekit/ suite and full-suite collection are clean, and CI
is fully green. No new defects found; nothing further to flag on the netmask handling.

GOOD TO MERGE at c0f66ab43

@JarryShaw
JarryShaw merged commit f7b5cc5 into main Sep 18, 2026
24 checks passed
JarryShaw added a commit that referenced this pull request Sep 18, 2026
…r takes one (#467)

Comment only; no executable line changes and no annotation changes.

The owner asked, reasonably, why the __init__ type stub does not accept
int now that the maker does. The answer is that the two annotations
describe different boundaries and the old comment did not say so: the
maker's `identifier: 'bytes | str | IPv6Address | int'` is what a caller
may pass, while the schema's is what the schema can hold, and the maker
converts between them before constructing the schema at all.

Measured through the maker rather than read off the code: identifier
0x1234 arrives at the schema as b'\x124' for IMSI and DUID and as
IPv6Address('::1234') for IPv6_Address. No int ever reaches the
constructor, so admitting one in the stub would document a value the
schema cannot hold -- and would mislead, because handing a raw int to the
StringField or BytesField that mn_id_selector resolves for every subtype
but IPv6_Address is exactly the #467 defect: struct.pack() cannot consume
it.

The previous comment said only "neither of which accepts an int", which
was true but read as though int were rejected outright, which is no
longer the case anywhere the caller can see.

Merged origin/main (f7b5cc5, #470) first -- clean, touching only
ipaddress.py and its tests, neither owned by this branch.
tests/protocols/internet/test_mh_unit.py: 37 passed, 332 subtests, 0
failed, unchanged.
@JarryShaw
JarryShaw deleted the fix-465-ipaddress-field-value-error branch September 18, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Address fields let a bare ValueError escape for a malformed address, one line before FieldValueError is raised for a version mismatch

1 participant