Source code for pcapkit.vendor.http.method

# -*- 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}"""

    if TYPE_CHECKING:
        #: Safe method.
        safe: 'bool'
        #: Idempotent method.
        idempotent: 'bool'

    def __new__(cls, value: 'str', safe: 'bool' = False,
                idempotent: 'bool' = False) -> 'Type[{NAME}]':
        """Create a member from a registry row.

        Args:
            value: Method token, which is also the member's value.
            safe: Whether the method is safe.
            idempotent: Whether the method is idempotent.

        """
        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}':
        """Resolve ``key`` to a member, matching the method token case-sensitively.

        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 the constructor also finds by
        value, before it falls back to :meth:`_missing_`.

        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.

        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.

        Returns:
            The member whose name or value is ``key``. On a miss, an
            unregistered member named ``key`` upper-cased, with the value
            ``default`` (or ``key``), which is not added to the lookup tables,
            so ``get`` never registers a method (:issue:`860`).

        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 miss fallback described under *Returns* -- only a failed
                lookup is. Raising is the intended behaviour for
                ``get(b'GET')`` as well, since no member of this registry
                has a :class:`bytes` name or value.

        """
        try:
            return super().get(key)
        except KeyError:
            # NOTE: the value keeps the caller's casing (``default`` or ``key``); the name is the
            # upper-case form :meth:`_missing_` uses. ``get`` does not mint (GitHub issue #860).
            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, then against member
                values, so ``'baseline-control'`` resolves to
                ``BASELINE_CONTROL`` by the value match the fix for
                :issue:`2284` added -- deliberately unlike :meth:`get`, which
                checks names and values too (GitHub issue :issue:`908`) but
                case-**sensitively**, per :rfc:`9110#section-9.1`
                (:issue:`896`). The owner kept this
                fold on GitHub issue :issue:`2552`, departing from that RFC on
                purpose: HTTP methods are case-insensitive here, and member
                names follow Python's naming conventions.

        """
        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]
        if name in cls._value2member_map_:
            return cls._value2member_map_[name]
        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]