Skip to content

Commit 78543cb

Browse files
committed
fix(const): find hyphenated HTTP methods by value in Method._missing_ (#2284)
Method._missing_ upper-cased the value and checked member names only, so Method('baseline-control') and Method('version-control') minted unregistered members (idempotent=False) instead of returning BASELINE_CONTROL and VERSION_CONTROL, whose names differ from their values. Fall back to _value2member_map_ with the upper-cased value before minting. Changed in the vendor template and mirrored identically into the generated pcapkit.const.http.method. Test: tests/const/test_const_method_missing_value_2284_unit.py covers both methods in any case, non-method strings still minting, get() staying case-sensitive, and template parity.
1 parent 7b57bb0 commit 78543cb

4 files changed

Lines changed: 117 additions & 2 deletions

File tree

‎docs/source/pcapkit/const/http.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ which is automatically generated from :class:`pcapkit.vendor.http.method.Method`
7171
:exc:`AttributeError` from ``key.upper()``, and ``get(b'GET')`` raised
7272
:exc:`TypeError`.
7373

74+
:meth:`Method._missing_ <pcapkit.const.http.method.Method._missing_>` also checks a
75+
folded ``value`` against member values, not only names, so a hyphenated method such
76+
as ``'baseline-control'`` resolves to :attr:`Method.BASELINE_CONTROL`, with the
77+
registered ``safe`` and ``idempotent``, in any case (:issue:`2284`).
78+
7479
.. autoclass:: pcapkit.const.http.method.Method
7580
:members:
7681
:undoc-members:

‎pcapkit/const/http/method.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ def _missing_(cls, value: 'str') -> 'Method':
316316
317317
Args:
318318
value: Value to get enum item. Matched case-insensitively against
319-
the canonical upper-case member names -- deliberately unlike
319+
the canonical upper-case member names, then against member
320+
values, so ``'baseline-control'`` resolves to
321+
``BASELINE_CONTROL`` (:issue:`2284`) -- deliberately unlike
320322
:meth:`get`, which is case-**sensitive** per
321323
:rfc:`9110#section-9.1`. The split was ruled deliberate on
322324
GitHub issue :issue:`896`; GitHub issue :issue:`908` is the pointer between
@@ -329,4 +331,6 @@ def _missing_(cls, value: 'str') -> 'Method':
329331
name = value.upper()
330332
if name in cls._member_map_:
331333
return cls._member_map_[name]
334+
if name in cls._value2member_map_:
335+
return cls._value2member_map_[name]
332336
return cls._unregistered_member(value, name)

‎pcapkit/vendor/http/method.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ def _missing_(cls, value: 'str') -> '{NAME}':
221221
222222
Args:
223223
value: Value to get enum item. Matched case-insensitively against
224-
the canonical upper-case member names -- deliberately unlike
224+
the canonical upper-case member names, then against member
225+
values, so ``'baseline-control'`` resolves to
226+
``BASELINE_CONTROL`` (:issue:`2284`) -- deliberately unlike
225227
:meth:`get`, which is case-**sensitive** per
226228
:rfc:`9110#section-9.1`. The split was ruled deliberate on
227229
GitHub issue :issue:`896`; GitHub issue :issue:`908` is the pointer between
@@ -234,6 +236,8 @@ def _missing_(cls, value: 'str') -> '{NAME}':
234236
name = value.upper()
235237
if name in cls._member_map_:
236238
return cls._member_map_[name]
239+
if name in cls._value2member_map_:
240+
return cls._value2member_map_[name]
237241
return cls._unregistered_member(value, name)
238242
'''.strip() # type: Callable[[str, str, str, str], str]
239243

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# -*- coding: utf-8 -*-
2+
"""``Method._missing_`` must find the hyphenated methods by value.
3+
4+
GitHub issue #2284. The constructor resolves case-insensitively (#896): it
5+
upper-cases the value and looks it up. It checked member *names* only, so the
6+
two RFC 3253 methods whose name differs from their value (``BASELINE_CONTROL``
7+
/ ``'BASELINE-CONTROL'``, ``VERSION_CONTROL`` / ``'VERSION-CONTROL'``) missed,
8+
and ``Method('baseline-control')`` minted an unregistered member with
9+
``idempotent=False`` instead of returning the registered one. #908 fixed the
10+
same gap in ``Method.get``; that override stays case-sensitive and is repinned
11+
here. Strings that are no method still mint an unregistered member (#860).
12+
13+
"""
14+
from __future__ import annotations
15+
16+
import inspect
17+
import unittest
18+
19+
#: Test tier (#1958): pure-Python lookups over a generated registry, which behave
20+
#: the same on every interpreter.
21+
__tier__ = 'LATEST'
22+
23+
#: Spellings of the two hyphenated methods, keyed by member name.
24+
SPELLINGS = {
25+
'BASELINE_CONTROL': ('baseline-control', 'Baseline-Control', 'BASELINE-CONTROL'),
26+
'VERSION_CONTROL': ('version-control', 'Version-Control', 'VERSION-CONTROL'),
27+
}
28+
29+
30+
class HyphenatedLookupTests(unittest.TestCase):
31+
"""The constructor returns the registered member in any casing."""
32+
33+
def test_hyphenated_methods_resolve_in_any_case(self) -> None:
34+
"""Without the fix, the lower- and mixed-case spellings mint a
35+
member named e.g. ``BASELINE-CONTROL`` with ``idempotent=False``."""
36+
from pcapkit.const.http.method import Method
37+
38+
for name, spellings in SPELLINGS.items():
39+
member = Method[name]
40+
for value in spellings:
41+
with self.subTest(value=value):
42+
probed = Method(value)
43+
self.assertIs(probed, member)
44+
self.assertTrue(probed.idempotent)
45+
self.assertFalse(probed.safe)
46+
47+
def test_name_lookup_still_folds_case(self) -> None:
48+
"""#896: the constructor's name lookup stays case-insensitive."""
49+
from pcapkit.const.http.method import Method
50+
51+
self.assertIs(Method('get'), Method.GET)
52+
self.assertIs(Method('Get'), Method.GET)
53+
54+
def test_get_stays_case_sensitive(self) -> None:
55+
"""#908/#896: ``get`` does not fold case, so a lower-case hyphenated
56+
spelling is still a distinct, unregistered method there."""
57+
from pcapkit.const.http.method import Method
58+
59+
probed = Method.get('baseline-control')
60+
self.assertIsNot(probed, Method.BASELINE_CONTROL)
61+
self.assertEqual(probed.value, 'baseline-control')
62+
63+
64+
class UnregisteredMemberTests(unittest.TestCase):
65+
"""A string matching no name or value still mints (#860)."""
66+
67+
def test_non_method_strings_mint_an_unregistered_member(self) -> None:
68+
"""The minted member keeps the caller's casing as its value, carries
69+
the upper-cased name, and is not added to either lookup table."""
70+
from pcapkit.const.http.method import Method
71+
72+
before = (len(Method._member_map_), len(Method._value2member_map_))
73+
for value in ('frob', 'Frob-Control', 'baseline control'):
74+
with self.subTest(value=value):
75+
probed = Method(value)
76+
self.assertNotIn(probed, list(Method))
77+
self.assertEqual(probed.value, value)
78+
self.assertEqual(probed.name, value.upper())
79+
self.assertFalse(probed.safe)
80+
self.assertFalse(probed.idempotent)
81+
self.assertEqual(
82+
(len(Method._member_map_), len(Method._value2member_map_)), before)
83+
84+
85+
class VendorTemplateParityTests(unittest.TestCase):
86+
"""The fix lives in the crawler template, so a regeneration keeps it."""
87+
88+
def test_the_crawler_template_carries_the_same_missing(self) -> None:
89+
"""The generated ``_missing_`` appears verbatim in the rendered
90+
template, and both consult ``_value2member_map_``."""
91+
from pcapkit.const.http.method import Method
92+
from pcapkit.vendor.http.method import LINE
93+
94+
rendered = LINE('Method', 'HTTP Method', '<ENUM>', 'pcapkit.vendor.http.method')
95+
source = inspect.getsource(Method._missing_.__func__) # type: ignore[attr-defined]
96+
97+
self.assertIn('cls._value2member_map_[name]', source)
98+
self.assertIn(source.rstrip('\n'), rendered)
99+
100+
101+
if __name__ == '__main__':
102+
unittest.main()

0 commit comments

Comments
 (0)