fix(const): refuse a multi-bit proto in AppType._dispatch rather than resolving it LSB-first (#759) - #783
Conversation
|
Cross-review at 1. The "12,341 declared single-bit ports" figure is wrong, and I re-derived it myself rather than take the reviewer's word. Measured on So the single-bit set is 1,766, not 12,341 — and 12,341 is 50 short of the total 12,391, not of the single-bit subset, which suggests the figure was a total with something excluded and then mislabelled. That makes digest 2. 3. The changelog checkbox is unticked with no annotation. House form is One overstatement worth correcting while you are in there. The body says only the UDP half of the 46 is #759's cross-registry dispatch. Measured: of the 23 UDP-origin mismatches, only 2 (ports 888 and 999) actually return a different final service than UDP's own single-bit lookup would; the other 21 land on TCP mechanically but coincide with UDP's canonical string, exactly like the TCP-origin cases. The 23/23 split is right, the characterisation of what each half means is not. Verified good, independently derived. The ten call sites are confirmed by direct read before the reviewer consulted your AST test — Not verified by the reviewer: the live-IANA byte-identical regeneration, and the coverage/pylint deltas — out of its budget, not disputed. |
8261139 to
515a826
Compare
|
All three corrected at So 12,341 is the distinct The blast radius is now stated honestly, and pinned. Of the 46: 44 are
Two things done right that I did not ask for. You re-fetched IANA before regenerating, confirmed the CSV byte-identical to your baseline ( Label back to |
|
Round-two cross-review at All three required changes confirmed applied, and the reviewer independently recomputed the four counts rather than accepting either my figures or the author's: 12,391 members / 10,625 multi-bit / 1,766 single-bit / 12,341 distinct The const-file delta really is comment-only. Diffing The divergent pin has teeth. One honest limit worth recording. The reviewer could not reproduce the exact digest hex The merge check was redone rather than trusted. Merged Settled in round one and not re-run: the ten call sites, Six CI legs still outstanding (21 success, 3 expected skips, 0 failures), so this is a review verdict rather than a merge signal. Host load ran 3.08 → 4.84 during the review from other agents, so it was deliberately kept to the requested checks. |
| # ignored, so nothing there is ambiguous to begin with. | ||
| namespaces = show_flag_values(proto) | ||
| if len(namespaces) > 1: | ||
| raise ProtocolError('%r names %d transport protocols, and so %d registries of %s; ' |
There was a problem hiding this comment.
Because this region is a str.format() template in the vendor generator, not hand-written source. pcapkit/vendor/reg/apptype/apptype.py emits the const file through .format() — you can see the delimiters in the same template at :243, __registries__: 'dict[TransportProtocol, Type[{NAME}]]' = {{}}, where the empty dict has to be written {{}} to survive expansion.
So emitting an f-string means writing f'{{proto!r}} names {{len(namespaces)}} …' in the template: every brace doubled, and the generator line stops reading like the code it produces.
Measured, so the local-versus-global convention is explicit:
| scope | raise …Error(f'… |
raise …Error('…' % …) |
|---|---|---|
pcapkit/ whole tree |
605 | 2 |
const/reg/apptype/apptype.py on origin/main |
0 | 6 |
So the repo convention really is f-strings, and this generated file is the exception — all six of its pre-existing raises are %-style (:92, :2308, :2368, :2378, :2478, :2484), and the new one matched them rather than being the only f-string in the file.
Happy to switch it if you would rather the emitted code follow the tree convention — it is one raise, so four doubled braces in the template. Say which and I will make it match.
There was a problem hiding this comment.
yea, id like to keep f-string convention across the library. only use % substitution when inevitable.
There was a problem hiding this comment.
Recorded, in your words: "yea, id like to keep f-string convention across the library. only use % substitution when inevitable."
It is not inevitable here — the doubled-brace form survives the generator's .format() intact. Verified before routing it, template in and expanded code out:
raise ProtocolError(f'{{proto!r}} names {{len(namespaces)}} transport protocols, and so ' <- in the template
f'{{len(namespaces)}} registries of {{cls.__name__}}; look the port up '
'under one transport protocol at a time')
raise ProtocolError(f'{proto!r} names {len(namespaces)} transport protocols, and so ' <- emitted
f'{len(namespaces)} registries of {cls.__name__}; look the port up '
'under one transport protocol at a time')
expanded raise parses as valid Python: True
So the new raise converts, alongside the loop flattening, in the same amend.
Scoping the other six deliberately, rather than quietly. This file has 6 pre-existing %-style raises — :92, :2308, :2368, :2378, :2478, :2484 — all of which your convention also wants converted. I am leaving them out of #783 and filing them as their own issue, because #783 is the multi-bit-proto refusal and a const diff that also sweeps style across a 12,391-member generated file is much harder to review for the thing it is actually doing.
Say the word if you would rather have all seven in one pass here and I will fold them in instead.
There was a problem hiding this comment.
Correcting myself on one detail above. I said this region is a str.format() template. It is not — BASE at pcapkit/vendor/reg/apptype/apptype.py:93 is a lambda returning an f-string:
BASE = lambda NAME, DOCS, FLAG, TABLE, MISS, MODL: f'''\
The brace-doubling conclusion is unchanged, since an f-string escapes braces the same way — and :243's = {{}} is still the evidence for it. But anyone following my earlier wording would go looking for a .format() call that is not there. The generated raise and the flattening are unaffected.
| raise ProtocolError('%r names %d transport protocols, and so %d registries of %s; ' | ||
| 'look the port up under one transport protocol at a time' | ||
| % (proto, len(namespaces), len(namespaces), cls.__name__)) | ||
| for namespace in namespaces: |
There was a problem hiding this comment.
if already raised on multiple namespaces, then why the loop on one element list?
There was a problem hiding this comment.
You are right, and it is a real simplification — the loop cannot iterate more than once.
After the len(namespaces) > 1 raise directly above it, namespaces is length 0 or 1, so the for is an if wearing a for. Both surviving lengths measured:
undefined int=0 show_flag_values=[] len=0
tcp int=1 show_flag_values=[1] len=1
tcp|udp int=3 show_flag_values=[1, 2] len=2 <- raises above
The 0-length case is live, not theoretical — it is the default argument reaching the delegating path:
AppType.get(80, proto=undefined)
-> ValueError: <TransportProtocol.undefined: 0> names no transport protocol registry of AppType
so the fall-through to the trailing ValueError has to stay. What the loop was doing was making a one-or-zero case look like an n-case, which reads as if a composite could still be resolved here — the opposite of what the raise above it decided.
Flattening to:
if namespaces:
subclass = cls.__registries__.get(TransportProtocol(namespaces[0]))
if subclass is not None:
return subclass
raise ValueError(...)Applying that to the generator template and regenerating, then re-posting the revision. Flipping to review: needs-changes until it lands.
515a826 to
7caec43
Compare
… resolving it LSB-first (#759) `AppType._dispatch` resolved a `proto` naming several transport protocols by taking its lowest set bit: `show_flag_values` iterates LSB-first and `tcp` is the lowest declared bit, so every composite containing it dispatched into the TCP registry whatever else it named. `TransportProtocol` is an `aenum.IntFlag` and a member carries the whole set IANA assigned the service, so `tcp | udp` is an ordinary value to read off one and an ordinary thing to pass back in. Re-measured on `fe80b8525`, sweeping all 10,625 multi-transport members through `get(m.port, proto=m.proto)`: 0 exceptions, 0 mints, and 46 answers naming a service other than the member's own, at 20 distinct ports. Those 46 are not all defects. 44 differ only in that the member is not its port's canonical, which a single-bit lookup does too and which `get` documents; 23 -- the UDP-declared half -- came back as a member of the *TCP* registry, so the type and `proto` were wrong whatever the service string said; and 2 named a service UDP does not answer for the port at all. Those two are the whole blast radius: port 888, where `accessbuilder` resolved to TCP's `cddbp`, and port 999, where `puprouter` resolved to TCP's `garcon` against UDP's `applix`. All of it silent, and undetectable to a caller checking equality, since `AppType.__eq__` compares on `port` alone. Identical counts at `83b58ebda` and `932cb48d1`, so long-standing rather than a regression. It now raises `ProtocolError` from `pcapkit.utilities.exceptions` -- a `ValueError` subclass, so the documented contract of `get` and `get_all` still holds, and a loud error rather than a quiet one because nothing catches it. Refusing costs no caller: the ten library call sites into the lookup each name one transport protocol, and `register_apptype`, the one place that reads a member's composite `proto`, tests it with `in` and never reaches the lookup. The change is in the `BASE` template of `pcapkit/vendor/reg/apptype/apptype.py`, which generates `pcapkit/const/reg/apptype/apptype.py`. The const file's matching edit was applied by hand rather than by a crawl, because regenerating fetches IANA's live registry and would rewrite unrelated member rows; expanding `BASE` and diffing the `_dispatch` region against the const file gives 78 lines each and 0 diff lines, so it is byte-identical to what regeneration emits. `tcp.py`, `udp.py`, `sctp.py` and `dccp.py` are untouched.
7caec43 to
9aada6a
Compare
|
Delta cross-review at What it re-derived independently, not accepted:
The refutation, and it was right. My brief said the branch sits on top of current One thing it flagged worth keeping: the Setting |
) The generated `pcapkit.const.reg.apptype.apptype` module had 6 `raise ValueError(...)` calls still using `%` formatting, left that way by #783 so its own diff over a 12,391-member file stayed reviewable. Per the maintainer's f-string convention (#783), they now match the other 605 f-string raises tree-wide: - `TransportProtocol._missing_`: `'%r is not a valid %s' % (...)` -> `f'{value!r} is not a valid {cls.__name__}'` - `AppType.__new__`: the "holds no members" guard - `AppType._dispatch`: the non-port `key` guard and the trailing "names no transport protocol registry" guard - `AppType._missing_`: both range/registry guards (same message as the first bullet) Each message was verified byte-identical old vs new (same repr()/str() semantics for %r/!r and %s/{}), and exercised through its real call path (`AppType.get('http', proto=tcp)`, `AppType.get(80, proto=undefined)`, `extend_enum(AppType, ...)`, out-of-range `AppType(...)`) with matching live exception text before and after. The edit is in the `BASE` template of `pcapkit/vendor/reg/apptype/apptype.py` (braces doubled, as an f-string template emitting f-strings) and applied by hand to the const file rather than by a crawl, to avoid rewriting the IANA-derived member rows. Expanding `BASE` with dummy TABLE/MISS and diffing against the const file shows exactly 2 non-equal regions -- the docstring's list-table and the range-row tail of `_missing_`, both TABLE/MISS-dependent -- and 0 elsewhere. Left unconverted, and said why in the PR: the three %-formatted dunders (`__new__`, `__repr__`, `__str__`) the issue also mentions -- out of the issue's stated raise-only scope, and `__new__`'s format string sets every member's underlying `StrEnum` value across all ~12,391 real members, a much larger blast radius than an error path. The module-level `consider-using-f-string` pylint disable stays: those dunders and the `extend_enum(..., '%d' % value, ...)` calls still use `%`. `tests/const/test_const_enum_builtin_parity.py::test_every_bespoke_template_ carries_the_guard` (GitHub issue #647) pinned all four bespoke vendor templates' guards against one shared `%`-style literal, so converting `reg.apptype.apptype`'s copy broke it in CI (7 legs). `BESPOKE_TEMPLATES` is now a dict keyed to each template's own guard text -- the other three still raise with `%` (#796 tracks sweeping them) -- rather than an `or` of both forms, which would let a genuinely deleted guard pass. Proved by deleting the guard from `pcapkit.vendor.tcp.flags` locally: the test failed with the expected subTest and message, then the file was restored and the tree verified clean. Build/test: `tests/const/test_const_apptype_split_unit.py` (19), `tests/vendor/test_vendor_reg_apptype_generator_unit.py` (6), and `tests/const/test_const_enum_builtin_parity.py` (20, `requests` importable so neither guard test skips) all pass under both pytest and plain unittest. mypy clean before and after. pylint: 0 new findings in either touched apptype file; R0801 unchanged at 575 tree-wide. Closes #792
) The generated `pcapkit.const.reg.apptype.apptype` module had 6 `raise ValueError(...)` calls still using `%` formatting, left that way by #783 so its own diff over a 12,391-member file stayed reviewable. Per the maintainer's f-string convention (#783), they now match the other 605 f-string raises tree-wide: - `TransportProtocol._missing_`: `'%r is not a valid %s' % (...)` -> `f'{value!r} is not a valid {cls.__name__}'` - `AppType.__new__`: the "holds no members" guard - `AppType._dispatch`: the non-port `key` guard and the trailing "names no transport protocol registry" guard - `AppType._missing_`: both range/registry guards (same message as the first bullet) Each message was verified byte-identical old vs new (same repr()/str() semantics for %r/!r and %s/{}), and exercised through its real call path (`AppType.get('http', proto=tcp)`, `AppType.get(80, proto=undefined)`, `extend_enum(AppType, ...)`, out-of-range `AppType(...)`) with matching live exception text before and after. The edit is in the `BASE` template of `pcapkit/vendor/reg/apptype/apptype.py` (braces doubled, as an f-string template emitting f-strings) and applied by hand to the const file rather than by a crawl, to avoid rewriting the IANA-derived member rows. Expanding `BASE` with dummy TABLE/MISS and diffing against the const file shows exactly 2 non-equal regions -- the docstring's list-table and the range-row tail of `_missing_`, both TABLE/MISS-dependent -- and 0 elsewhere. Left unconverted, and said why in the PR: the three %-formatted dunders (`__new__`, `__repr__`, `__str__`) the issue also mentions -- out of the issue's stated raise-only scope, and `__new__`'s format string sets every member's underlying `StrEnum` value across all ~12,391 real members, a much larger blast radius than an error path. The module-level `consider-using-f-string` pylint disable stays: those dunders and the `extend_enum(..., '%d' % value, ...)` calls still use `%`. `tests/const/test_const_enum_builtin_parity.py::test_every_bespoke_template_ carries_the_guard` (GitHub issue #647) pinned all four bespoke vendor templates' guards against one shared `%`-style literal, so converting `reg.apptype.apptype`'s copy broke it in CI (7 legs). `BESPOKE_TEMPLATES` is now a dict keyed to each template's own guard text -- the other three still raise with `%` (#798 tracks sweeping them, the `%`-formatted dunders, and dropping the disable) -- rather than an `or` of both forms, which would let a genuinely deleted guard pass. Proved by deleting the guard from `pcapkit.vendor.tcp.flags` locally: the test failed with the expected subTest and message, then the file was restored and the tree verified clean. Build/test: `tests/const/test_const_apptype_split_unit.py` (19), `tests/vendor/test_vendor_reg_apptype_generator_unit.py` (6), and `tests/const/test_const_enum_builtin_parity.py` (20, `requests` importable so neither guard test skips) all pass under both pytest and plain unittest. mypy clean before and after. pylint: 0 new findings in either touched apptype file; R0801 unchanged at 575 tree-wide. Closes #792
) (#797) The generated `pcapkit.const.reg.apptype.apptype` module had 6 `raise ValueError(...)` calls still using `%` formatting, left that way by #783 so its own diff over a 12,391-member file stayed reviewable. Per the maintainer's f-string convention (#783), they now match the other 605 f-string raises tree-wide: - `TransportProtocol._missing_`: `'%r is not a valid %s' % (...)` -> `f'{value!r} is not a valid {cls.__name__}'` - `AppType.__new__`: the "holds no members" guard - `AppType._dispatch`: the non-port `key` guard and the trailing "names no transport protocol registry" guard - `AppType._missing_`: both range/registry guards (same message as the first bullet) Each message was verified byte-identical old vs new (same repr()/str() semantics for %r/!r and %s/{}), and exercised through its real call path (`AppType.get('http', proto=tcp)`, `AppType.get(80, proto=undefined)`, `extend_enum(AppType, ...)`, out-of-range `AppType(...)`) with matching live exception text before and after. The edit is in the `BASE` template of `pcapkit/vendor/reg/apptype/apptype.py` (braces doubled, as an f-string template emitting f-strings) and applied by hand to the const file rather than by a crawl, to avoid rewriting the IANA-derived member rows. Expanding `BASE` with dummy TABLE/MISS and diffing against the const file shows exactly 2 non-equal regions -- the docstring's list-table and the range-row tail of `_missing_`, both TABLE/MISS-dependent -- and 0 elsewhere. Left unconverted, and said why in the PR: the three %-formatted dunders (`__new__`, `__repr__`, `__str__`) the issue also mentions -- out of the issue's stated raise-only scope, and `__new__`'s format string sets every member's underlying `StrEnum` value across all ~12,391 real members, a much larger blast radius than an error path. The module-level `consider-using-f-string` pylint disable stays: those dunders and the `extend_enum(..., '%d' % value, ...)` calls still use `%`. `tests/const/test_const_enum_builtin_parity.py::test_every_bespoke_template_ carries_the_guard` (GitHub issue #647) pinned all four bespoke vendor templates' guards against one shared `%`-style literal, so converting `reg.apptype.apptype`'s copy broke it in CI (7 legs). `BESPOKE_TEMPLATES` is now a dict keyed to each template's own guard text -- the other three still raise with `%` (#798 tracks sweeping them, the `%`-formatted dunders, and dropping the disable) -- rather than an `or` of both forms, which would let a genuinely deleted guard pass. Proved by deleting the guard from `pcapkit.vendor.tcp.flags` locally: the test failed with the expected subTest and message, then the file was restored and the tree verified clean. Build/test: `tests/const/test_const_apptype_split_unit.py` (19), `tests/vendor/test_vendor_reg_apptype_generator_unit.py` (6), and `tests/const/test_const_enum_builtin_parity.py` (20, `requests` importable so neither guard test skips) all pass under both pytest and plain unittest. mypy clean before and after. pylint: 0 new findings in either touched apptype file; R0801 unchanged at 575 tree-wide. Closes #792
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit (relative to its parent, 4233555) now names all 20 bullets it carries, not just the 8 this session added on top of the 10 already there -- a first draft of this message named only its own 8 and left the other 10 silent, which a cross-review caught. Six are breaking, matching the crediting PRs' own `breaking` label in each case: - #754 -- AppType split into per-transport registries; the 1,004 portless and 704 transportless rows stop being members. - #764 -- an out-of-range port in `AppType.get` is refused, not minted. - #778 -- `@final` enforced at runtime on `Info`/`Schema`. - #575 -- four `.get()`-backed enum fields fall through to `_unregistered_member` instead of minting; 14 of 23 sample captures change output. - #759 -- `AppType._dispatch` on a multi-transport `proto` now raises `ProtocolError` instead of silently resolving to whichever transport owns the lowest set bit. - #805 -- `FieldBase.length` on a negative resolved length now raises `ProtocolError` instead of letting a bare `struct.error` escape. A second cross-review caught both: their crediting PRs (#783, #811) both carry GitHub's own `breaking` label, and neither bullet said so. - #772 (with #790's docstring reword), #766, #787, #794 (with #791's citation repoint), #792/#798, #802, #779 (via #782, a further #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #796, #800 -- the other 14, non-breaking. Also restores a measurement an earlier round in this same diff dropped while updating an adjacent one: the #692 entry's "mypy is unmoved at 112 errors" silently lost its "and pylint ... 364 messages" half when `EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence. Restored to the last value earlier rounds signed off on rather than re-measured, since this branch's own `pcapkit/` tree predates several since-merged PRs and a fresh run would not be measuring the same thing the original round measured. Unmoved, and not silently dropped this time: `93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES` 44 to 43 in the several other places that already carried the fix. On which PRs get a bullet: there is no clean "user-facing only" rule -- #766 and #791 are pure CI/test/lint-comment entries that are in, while #773 and #763 are the same kind of thing and are out. The real pattern across this file's 46 commits is closer to "each round's author judged it worth a reader's time," which is inconsistent by construction. This round leaves that inconsistency as found rather than trying to retrofit a rule, but did add #779 (via #782) on reconsideration -- its own PR body names it as sharing #766's hazard, and pre-existing precedent already treats that hazard's instances as bullet-worthy. `changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0. `test_changelog_md.py` 47 passed.
…s in #817 and #821 Two bullets, both non-breaking, appended after the #800 entry in merge order. Bullet count 128 to 130 (`grep -cE '^\* \*\*'`). - #804 (PR #817) -- the three `__repr__` methods #798 left `%`-formatted are f-strings now, dropping `consider-using-f-string` from both const modules and both vendor templates; the other bespoke templates in `{const,vendor}/{ftp,http}/` still carry the disable, so #804's claim holds for this pair only, not for those directories. - #682 (PR #821) -- `TCP.__proto__` no longer binds `httpv1.HTTP` directly for ports 80/8080; both repoint to the generic HTTP proxy `_guess_version` identifies through, which only became reliable once #800/#814 landed. `udp.py` already pointed there, so that side of the PR is prose-only (its port rows and docstring), not a code change, and the entry says so. Protochain over the 23 sample captures is *not* byte-identical: 9 frames in `options-transport.pcap` go `Raw` to `HTTP/2`, all 231 HTTP/1.1 frames are unaffected, and `_guess_version`'s entry count goes 0 to 252. Not marked `**a breaking change to**`: PR #821's own labels are `bug,fix,docs,test`, no `breaking`, unlike #759/#783 and #805/#811 last round, whose crediting PRs did carry it. The entry does say what a `breaking`-blind reader would still want to know -- TCP:80/8080 traffic that is neither valid HTTP/1 nor preface-carrying now reaches `_guess_version`'s fall-through arm instead of the direct `httpv1` bind's unconditional `Raw`, which is where the 12 (of 252) fall-throughs the PR measured come from. `util/changelog_md.py` regenerated `CHANGELOG.md`, first pass, no line-spanning literal this round; `--check` exit 0. `test_changelog_md.py` 47 passed.
Please follow the guide below
You will be asked some questions, please read them carefully and answer honestly
Put an
xinto all the boxes [ ] relevant to your pull request (like that [x])Use Preview tab to see how your pull request will actually look like
Searched for similar pull requests
Followed the coding style (
make pylint,make mypy,make isort)make testpasses, and a test case covers the changeAdded a changelog entry under
docs/source/changelog/and regeneratedCHANGELOG.md, if the change is user-visible — N/A — changelog centralised in docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) #657What is the purpose of your pull request?
Tick the commit type your subject line carries.
fix— corrects a defectfeat— adds a featureperf— changes performance, not behaviourrefactor— changes neither behaviour nor performancetest— tests onlydocs— documentation onlyci— workflows or build toolingchore— anything elseDescription of your pull request and other information
Fixes #759.
_dispatchresolved a compositeprotoby its lowest set bit, so any composite containingtcpdispatched into the TCP registry. It now raisesProtocolErrorfrompcapkit.utilities.exceptions— aValueErrorsubclass, soget/get_all's documentedRaises:still holds, and loud rather than quiet because nothing catches it (unlike_missing_'s guard, whichConstEnumBuiltinParityTests.test_the_exception_is_not_an_in_library_onerequires to stay a non-BaseError).Blast radius, re-derived on
fe80b8525. Sweeping all 10,625 multi-transport members throughget(m.port, proto=m.proto): 0 exceptions, 0 mints, 46 answers naming a service other than the member's own, at 20 distinct ports. Those 46 are three different things, and only the last is a wrong answer: 44 differ only in that the member is not its port's canonical, which a single-bit lookup does too and whichgetdocuments; 23 — the UDP-declared half — came back as a member of the TCP registry, sotype()and.protowere wrong whatever the service string said; and 2 named a service UDP does not answer for the port at all. Those two are the whole blast radius: port 888,accessbuilder→ TCP'scddbp, and port 999,puprouter→ TCP'sgarconagainst UDP'sapplix. #759's body found the first and generalised from it. After the fix: 10,625 refusals, 0 answers.Population, since four figures here are easy to confuse. 12,391 declared members (TCP 6147, UDP 6143, SCTP 91, DCCP 10); of those 10,625 carry a multi-bit
.protoand 1,766 a single-bit one; they sit on 12,341 distinct(registry, port)pairs (TCP 6121, UDP 6119, SCTP 91, DCCP 10), the other 50 sharing a port with another service as IANA's three on TCP/80 do. The invariance digest covers those 12,341 rows — one per(registry, port), asregistry|port|resolved service|int(resolved proto), each looked up by passing the registry's own single transport bit whatever the member's.protoholds:3a457683c7b00609b06526aa02e3c361a910fa4a0e22d25ca16b4ab01acc053a, identical either side of the fix and identical again withprotopassed as a name. So it is the argument that is single-bit, not the member set — an earlier revision of this description labelled it "12,341 single-bit ports", which is a set that does not exist. Restricted to the 1,766 single-bit-.protomembers it is 1,763 rows (three share a port), digest308715ae6642a91be547d73e1c07f84bed7f27b7158951e831201c936807814b, also identical either side. All four counts are asserted in the test.Regeneration. The change is in the crawler's
BASEtemplate; the const file is regenerated from it against IANA's live CSV (sha25634328ed0…, re-fetched and unchanged), which reproduces all five committed files byte-identically before the change, so the diff is only the guard and its comment. Members unchanged. Siblings byte-identical, md5 before == after:be2b6a48tcp.py,6e169b8audp.py,39a7749dsctp.py,556f8403dccp.py.Tests.
tests/const/test_const_apptype_split_unit.py15 → 19 tests, 808 → 892 subtests, agreeing underpython -m unittest(19 OK). The two refusal tests fail against the pre-fix const file under both runners; the two invariance tests (single-bit resolution, and an AST pin on all ten library call sites passing one transport protocol) pass either side by design. Coverage of the changed file 51.752% → 51.843%: +4 statements, +2 branch arcs, 0 new misses.pylintreports the same 14 findings asmainfor these two files,mypyandisortclean.make testnot run in full — targeted selections only (tests/const,tests/vendor,tests/project= 251 tests / 2382 subtests, plus the transport/schema/corekit/foundation callers = 111 / 188).Label-worthy: this regenerates a const table, and it is a public-contract change —
AppType.get(port, proto=<composite>)previously returned a member and now raises. No in-library caller passes a composite;register_apptypereads a member's compositeprotobut tests it withinand never reaches the lookup.