# -*- coding: utf-8 -*-
"""HTTP Method
=================
.. module:: pcapkit.vendor.http.method
This module contains the vendor crawler for **HTTP Method**,
which is automatically generating :class:`pcapkit.const.http.method.Method`.
"""
import csv
import re
import sys
from typing import TYPE_CHECKING
from pcapkit.vendor.default import Vendor
if TYPE_CHECKING:
from typing import Callable
__all__ = ['Method']
#: Default constant template of enumeration registry from IANA CSV.
LINE = lambda NAME, DOCS, ENUM, MODL: f'''\
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long
"""{(name := DOCS.split(' [', maxsplit=1)[0])}
{'=' * (len(name) + 6)}
.. module:: {MODL.replace('vendor', 'const')}
This module contains the constant enumeration for **{name}**,
which is automatically generated from :class:`{MODL}.{NAME}`.
"""
from typing import TYPE_CHECKING
from aenum import StrEnum
from pcapkit.corekit.enum import EnumRegistry
from pcapkit.utilities.exceptions import EnumValueError
if TYPE_CHECKING:
from typing import Any, Optional, Type
__all__ = ['{NAME}']
class {NAME}(EnumRegistry, StrEnum):
"""[{NAME}] {DOCS}
.. versionchanged:: 1.5.0
Neither ``_missing_`` nor ``get()`` mints a member, per the owner's
ruling on GitHub issue :issue:`860`: only IANA-registered values are
legitimate members, and a new one is created through :meth:`register`,
which can supply :attr:`safe` and :attr:`idempotent` where a bare wire
method verb carries neither. ``get()`` matches ``key`` case-sensitively,
as :rfc:`9110` makes the method token (:issue:`896`), and checks it
against member values as well as names (:issue:`908`). A member's
:class:`str` payload is its value, so ``Method.GET == 'GET'``
(:issue:`870`).
"""
if TYPE_CHECKING:
#: Safe method.
safe: 'bool'
#: Idempotent method.
idempotent: 'bool'
def __new__(cls, value: 'str', safe: 'bool' = False,
idempotent: 'bool' = False) -> 'Type[{NAME}]':
obj = str.__new__(cls, value)
obj._value_ = value
obj.safe = safe
obj.idempotent = idempotent
return obj
def __repr__(self) -> 'str':
return f'<{{self.__class__.__name__}}.{{self._value_}}>'
{ENUM}
@classmethod
def _unregistered_member(cls, value: 'str', name: 'str') -> '{NAME}':
"""Build a member absent from this registry's own lookup tables.
Leaves :attr:`safe` and :attr:`idempotent` at the same defaults
:meth:`__new__` itself would, rather than missing entirely --
:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`
bypasses :meth:`__new__` (it calls :class:`str`'s directly), so
those attributes would otherwise be absent. There is no more
specific value to reconstruct them from -- a bare wire method verb
carries neither, which is exactly the owner's reasoning for why
``get()``/``_missing_`` must not mint one: :meth:`register` is the
path that can actually supply them.
Args:
value: Value to get enum item -- the convention here, shared with
:class:`~pcapkit.const.ftp.command.FEATCode` and
:class:`~pcapkit.const.ftp.command.Command`, is the caller's
own casing, unchanged: an unregistered member's *value* is
exactly what was observed on the wire, matching how a
*registered* member's own value is exactly what was
declared, never reformatted. Only :attr:`name` -- the
identifier, not the value -- is canonicalised.
A *registered* member carries its value the same way:
:meth:`__new__` passes ``value`` to ``str.__new__``, as
:class:`~pcapkit.const.ftp.command.Command`'s own
``__new__`` does (:issue:`870`), so both kinds of member
carry their own value as real :class:`str` content
(``str(Method.GET) == 'GET'`` and
``str(Method('frob')) == 'frob'``).
name: Bare label for the unregistered member -- here, the
canonical upper-case form of ``value``, matching the name
every *registered* member of this class is looked up by,
since :issue:`860`'s open-vocabulary registries have no manufactured
placeholder label to fall back to.
"""
obj = super()._unregistered_member(value, name)
obj.safe = False
obj.idempotent = False
return obj
@classmethod
def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}':
"""Backport support for original codes.
Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for the
lookup itself, so ``key`` is checked against member values as well
as names (GitHub issue :issue:`908`). That is what resolves the two
IANA methods whose member *name* differs from their *value* --
``BASELINE_CONTROL`` / ``'BASELINE-CONTROL'`` and
``VERSION_CONTROL`` / ``'VERSION-CONTROL'``, a hyphen being unusable
in a Python identifier -- which :meth:`_missing_` (and so the
constructor) also finds by value.
It is a :class:`classmethod`, as the base is, because a
zero-argument ``super()`` needs a first argument to bind
(:issue:`908`). ``{NAME}.get('X')`` binds the same as it would on a
:class:`staticmethod`.
``default`` does not follow the base's ``NO_DEFAULT`` sentinel and
value-only fallback: the base's ``default`` must name a
*registered* value (resolved through ``_value2member_map_``), while
this override's ``default`` supplies the *value* of the
unregistered member it builds on a miss.
.. versionchanged:: 1.5.0
A non-:class:`str` ``key`` raises :exc:`ValueError`
(:issue:`908`). ``get(42)`` and ``get(None)`` raised
:exc:`AttributeError` from ``key.upper()``, and ``get(b'GET')``
raised :exc:`TypeError`.
Args:
key: Key to get enum item. Looked up case-**sensitively**,
per :rfc:`9110#section-9.1` -- the method token is
case-sensitive, unlike
:meth:`~pcapkit.const.ftp.command.Command.get`'s equivalent override, which stays
case-insensitive because :rfc:`959#section-5` says FTP
command codes are not. Checked against both member names
and values, via the base's own precedence -- name before
value -- so a value-only match such as
``'BASELINE-CONTROL'`` resolves too (:issue:`908`).
default: Value for the unregistered member built when ``key``
matches neither a name nor a value. ``None``, the
default, uses ``key`` itself.
Raises:
ValueError: If ``key`` is not a :class:`str`. This reaches the
caller from the base's non-``str`` branch, where
:meth:`_validate_value` refuses it before ``cls(key)`` could
reach ``_missing_``, and is **not** caught by
the ``except KeyError`` fallback below -- only a failed
*name* lookup is. Raising is the intended behaviour for
``get(b'GET')`` as well: a member whose name and
value were both the :class:`bytes` object is not a thing
this registry should hand back.
:meta private:
"""
try:
return super().get(key)
except KeyError:
# NOTE: the value is ``default`` if the caller supplied one, or
# else ``key`` exactly as given -- never its upper-cased form --
# so an unregistered member's value is the caller's own casing,
# the same convention :meth:`_unregistered_member` documents and
# :class:`~pcapkit.const.ftp.command.FEATCode` already followed.
# The name is always the canonical upper-case form, matching
# :meth:`_missing_` and every registered member's own name. Two
# calls naming the same method in different case, e.g.
# ``get('frob')`` and ``get('FROB')``, therefore build results
# that are *not* equal -- each is exactly what its own caller
# passed, per GitHub issue #860's conversion away from minting.
return cls._unregistered_member(default if default is not None else key, key.upper())
@classmethod
def _validate_value(cls, value: 'Any') -> 'None':
"""Reject ``value`` unless it is a :class:`str`, as :meth:`_missing_` does.
Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value`
with the check :meth:`_missing_` makes, and the message it raises, so
:meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the
values :meth:`get` refuses, in the same words (:issue:`1816`). Any
:class:`str` passes, since :meth:`get` and :meth:`_missing_` resolve
every one.
Args:
value: Value about to be looked up or minted.
Raises:
EnumValueError: If ``value`` is not a :class:`str`. A
:exc:`ValueError`, so an ``except ValueError`` caller is
unaffected.
"""
if not isinstance(value, str):
raise EnumValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
@classmethod
def _missing_(cls, value: 'str') -> '{NAME}':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item. Matched case-insensitively against
the canonical upper-case member names -- deliberately unlike
:meth:`get`, which is case-**sensitive** per
:rfc:`9110#section-9.1`. The split was ruled deliberate on
GitHub issue :issue:`896`; GitHub issue :issue:`908` is the pointer between
the two, so the two rationales are not left contradicting each
other with nothing tying them together.
"""
if not isinstance(value, str):
raise ValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
name = value.upper()
if name in cls._member_map_:
return cls._member_map_[name] # type: ignore[return-value]
return cls._unregistered_member(value, name)
'''.strip() # type: Callable[[str, str, str, str], str]
[docs]
class Method(Vendor):
"""HTTP Method"""
#: Link to registry.
LINK = 'https://www.iana.org/assignments/http-methods/methods.csv'
def process(self, data: 'list[str]') -> 'list[str]': # type: ignore[override]
"""Process CSV data.
Args:
data: CSV data.
Returns:
Enumeration fields.
"""
reader = csv.reader(data)
next(reader) # header
enum = [] # type: list[str]
for item in reader:
meth = item[0]
if meth == '*':
continue
safe = item[1]
idem = item[2]
rfcs = item[3]
temp = [] # type: list[str]
for rfc in self.split_references(rfcs):
if 'RFC' in rfc and re.match(r'\d+', rfc[3:]):
#temp.append(f'[{rfc[:3]} {rfc[3:]}]')
temp_split = rfc[3:].split(', ', maxsplit=1)
if len(temp_split) > 1:
temp.append(f'[:rfc:`{temp_split[0]}#{temp_split[1].lower()}`]'.replace(' ', '-'))
else:
temp.append(f'[:rfc:`{temp_split[0]}`]')
else:
temp.append(f'[{rfc}]'.replace('_', ' '))
desc = self.wrap_comment(re.sub(
r'\r*\n', ' ', f'{meth} {"".join(temp) if rfcs else ""}',
flags=re.MULTILINE))
name = self.safe_name(meth).upper()
safe_flag = 'True' if safe == 'yes' else 'False'
idem_flag = 'True' if idem == 'yes' else 'False'
pres = f"{name} = {meth!r}, {safe_flag}, {idem_flag}"
sufs = f'#: {desc}'
enum.append(f'{sufs}\n {pres}')
return enum
def context(self, data: 'list[str]') -> 'str':
"""Generate constant context.
Args:
data: CSV data.
Returns:
Constant context.
"""
enum = self.process(data)
ENUM = '\n\n '.join(map(lambda s: s.rstrip(), enum)).strip()
return LINE(self.NAME, self.DOCS, ENUM, self.__module__)
if __name__ == '__main__':
sys.exit(Method()) # type: ignore[arg-type]