# -*- coding: utf-8 -*-
# mypy: disable-error-code=assignment
# pylint: disable=line-too-long
"""FTP Command
=================
.. module:: pcapkit.const.ftp.command
This module contains the constant enumeration for **FTP Command**,
which is automatically generated from :class:`pcapkit.vendor.ftp.command.Command`.
"""
from typing import TYPE_CHECKING
from aenum import IntEnum, IntFlag, StrEnum, auto
from pcapkit.corekit.enum import EnumLookup, EnumRegistry
from pcapkit.corekit.sentinels import NO_DEFAULT
from pcapkit.utilities.exceptions import EnumValueError
if TYPE_CHECKING:
from typing import Any, Optional, Type
__all__ = ['Command']
[docs]
class FEATCode(EnumRegistry, StrEnum):
"""Keyword returned in FEAT response line for this command/extension,
c.f., :rfc:`5797#section-3`.
.. note::
Declares every FEAT keyword the IANA registry's own ``FEAT code``
column names -- the group markers below plus the per-command
keywords generated after them (data-driven, not hand-picked; see
:meth:`~pcapkit.vendor.ftp.command.Command.process`) -- rather than
minting the per-command ones at import time as an incidental side
effect of building :class:`Command`'s own rows. GitHub issue :issue:`860`:
that import-time mutation is the same defect shape :issue:`775` rules
out on :class:`~pcapkit.const.pcapng.filter_type.FilterType`. ``_missing_``
still unmints for a keyword that
turns up on the wire but names none of these -- the extendability the
owner conditionally asked to keep on GitHub issue :issue:`860`: unsure what a
FEAT value absent from the RFC's list means in the CSV table, the owner
said that if the expected handling is this registry's approach, as industry
convention has it, the registry stays extendable as is, and then approved
declaring the registry's keywords as real members with ``_missing_``
still covering the rest. No custom ``__new__`` here, so the base's
generic :meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`
needs no override.
"""
#: FTP standard commands [:rfc:`0959`].
base = '<base>'
#: Historic experimental commands [:rfc:`0775`][:rfc:`1639`].
hist = '<hist>'
#: FTP Security Extensions [:rfc:`2228`].
secu = '<secu>'
#: FTP Feature Negotiation [:rfc:`2389`].
feat = '<feat>'
#: FTP Extensions for NAT/IPv6 [:rfc:`2428`].
nat6 = '<nat6>'
#: Authentication/Security Mechanism [2][:rfc:`2773`][:rfc:`4217`]
AUTH = 'AUTH'
#: Hostname [:rfc:`7151`]
HOST = 'HOST'
#: Language (for Server Messages) [:rfc:`2640`]
UTF8 = 'UTF8'
#: File Modification Time [:rfc:`3659`]
MDTM = 'MDTM'
#: List Directory (for machine) [:rfc:`3659`]
MLST = 'MLST'
#: Protection Buffer Size [:rfc:`4217`]
PBSZ = 'PBSZ'
#: Data Channel Protection Level [:rfc:`4217`]
PROT = 'PROT'
#: Restart (for STREAM mode) [3][:rfc:`3659`]
REST = 'REST'
#: File Size [:rfc:`3659`]
SIZE = 'SIZE'
#: Trivial Virtual File Store [:rfc:`3659`]
TVFS = 'TVFS'
def __repr__(self) -> 'str':
return f'<{self.__class__.__name__} [{self._name_}]>'
[docs]
@classmethod
def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'FEATCode':
"""Resolve ``key`` case-insensitively, per :rfc:`5797#section-2`.
One of the few case-insensitive overrides the ruling on GitHub issue
:issue:`877` allows, and the registry's own defining document states the
comparison rule outright rather than leaving it to be inferred --
:rfc:`5797#section-2`, on the ``FEAT Code`` column this class is
generated from: *"IANA maintains uniqueness of feature names (FEAT
codes) based on case-insensitive comparison."* Two FEAT codes
therefore cannot differ only by case, so folding the caller's key
cannot resolve ambiguously.
:rfc:`2389#section-3.2`, which defines the ``FEAT`` response the
codes appear in, points the same way from the wire side: *"The
feature-label and feature-parms are nominally case sensitive,
however ... it is to be expected that those definitions will usually
specify the label and parameters in a case independent manner. Where
this is done, implementations are recommended to use upper case
letters when transmitting the feature response."* A caller feeding a
``FEAT`` response line is therefore holding upper case by
recommendation, while some of this registry's codes are registered in
lower case -- measured on the IANA CSV: ``base``, ``feat``, ``hist``,
``nat6`` and ``secu``, against all-upper-case keywords for the rest,
with none mixed. Without this override ``get('BASE')`` raises
:exc:`KeyError`, which is the defect GitHub issue :issue:`903`'s audit found.
The obvious objection, answered: :rfc:`5797` uses case *presentationally*
to tell a real keyword from a placeholder -- *"defined FEAT keywords
codes are listed in all uppercase, whereas placeholder keywords ... are
listed in lowercase"* -- so folding might look like it discards that
distinction. It does not. Only the inbound ``key`` is folded; every
member keeps the registrar's own casing, and the fold itself is backed by
the RFC: the final ruling on GitHub issue :issue:`877` is that an enum treats its
values as case-insensitive where the RFC states they are, and as
case-sensitive otherwise. So ``get('BASE').name`` is still ``'base'`` and
still says placeholder. And the uniqueness rule quoted above is what makes
that safe: a real keyword ``BASE`` could not be registered alongside the
placeholder ``base``, so there is no second member for the fold to hide.
Folds only as a *fallback*. An exact name or value hit is delegated to
:meth:`~pcapkit.corekit.enum.EnumLookup.get` untouched, so the base's
own precedence -- name before value -- and its non-minting ``str``
path both survive: nothing here calls ``cls(key)``, so a key matching
no member, folded or not, still raises rather than growing the
registry. ``get('ZZ-NOT-REAL')`` therefore raises
:exc:`KeyError` while ``FEATCode('ZZ-NOT-REAL')`` yields an
unregistered member.
Args:
key: Name or value to look up. A non-``str`` key is passed
straight through, since case cannot apply to it.
default: As :meth:`~pcapkit.corekit.enum.EnumLookup.get`. Not
folded -- it names an already-registered value rather than
arriving from the wire, so the caller spells it from this
module.
Returns:
The canonical member for ``key``, or for ``default``.
Raises:
KeyError: If no member matches ``key`` exactly or case-insensitively
and there is no usable ``default``.
ValueError: As :meth:`~pcapkit.corekit.enum.EnumLookup.get`, for a
non-``str`` key.
"""
if isinstance(key, str) and not (
key in cls._member_map_ or # pylint: disable=no-member
key in cls._value2member_map_
):
folded = key.casefold()
for name, member in cls._member_map_.items(): # pylint: disable=no-member
if name.casefold() == folded:
return member
for member in cls._value2member_map_.values():
if member.value.casefold() == folded:
return member
return super().get(key, default)
[docs]
@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:`_missing_` resolves 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__}')
[docs]
@classmethod
def _missing_(cls, value: 'str') -> 'FEATCode':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item.
"""
if not isinstance(value, str):
raise ValueError(f'{value!r} is not a valid {cls.__name__}')
return cls._unregistered_member(value, value.upper())
[docs]
class CommandType(EnumLookup, IntFlag):
"""Type of "kind" of command, based on :rfc:`959#section-4`.
Built on :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub issue
:issue:`930`, which finished :issue:`877`'s phase 2. This class defines no
``get`` of its own, so ``get``/``get_all`` are the base's, and its own
:meth:`_missing_` range guard below stands as written, since
:class:`~pcapkit.corekit.enum.EnumLookup` does not touch that hook.
"""
undefined = 0
#: Access control.
A = auto()
#: Parameter setting.
P = auto()
#: Service execution.
S = auto()
[docs]
@classmethod
def _missing_(cls, value: 'int') -> 'CommandType':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item.
"""
if not (isinstance(value, int) and 0 <= value <= 0x07):
raise ValueError(f'{value!r} is not a valid {cls.__name__}')
return super()._missing_(value)
[docs]
class Command(EnumRegistry, StrEnum):
"""[Command] FTP Command"""
if TYPE_CHECKING:
#: Feature code. Keyword returned in FEAT response line for this command/extension,
#: c.f., :rfc:`5797#section-2.2`.
feat: 'Optional[FEATCode]'
#: Brief description of command / extension.
desc: 'Optional[str]'
#: Type of "kind" of command, based on :rfc:`959#section-4`.
type: 'CommandType'
#: Expectation for support in modern FTP implementations.
conf: 'ConformanceRequirement'
def __new__(cls, name: 'str', feat: 'Optional[FEATCode]' = None,
desc: 'Optional[str]' = None, type: 'CommandType' = CommandType.undefined,
conf: 'ConformanceRequirement' = ConformanceRequirement.O) -> 'Type[Command]':
"""Create a member from a registry row.
Args:
name: Command word, which is also the member's value.
feat: Feature code of the ``FEAT`` response line for the command.
desc: Brief description of the command.
type: Kind of command, based on :rfc:`959#section-4`.
conf: Expectation for support in modern FTP implementations.
"""
obj = str.__new__(cls, name)
obj._value_ = name
obj.feat = feat
obj.desc = desc
obj.type = type
obj.conf = conf
return obj
[docs]
def __repr__(self) -> 'str':
"""Show the member with its description, as ``<Command.NAME: description>``."""
return f'<{self.__class__.__name__}.{self._name_}: {self.desc}>'
#: Abort [:rfc:`959`]
ABOR: 'Command' = 'ABOR', FEATCode.base, 'Abort', CommandType.S, ConformanceRequirement.M
#: Account [:rfc:`959`]
ACCT: 'Command' = 'ACCT', FEATCode.base, 'Account', CommandType.A, ConformanceRequirement.M
#: Authentication/Security Data [:rfc:`2228`][:rfc:`2773`][:rfc:`4217`]
ADAT: 'Command' = 'ADAT', FEATCode.secu, 'Authentication/Security Data', CommandType.A, ConformanceRequirement.O
#: FTP64 ALG status [:rfc:`6384#section-11`]
ALGS: 'Command' = 'ALGS', None, 'FTP64 ALG status', 0, ConformanceRequirement.O
#: Allocate [:rfc:`959`]
ALLO: 'Command' = 'ALLO', FEATCode.base, 'Allocate', CommandType.S, ConformanceRequirement.M
#: Append (with create) [:rfc:`959`]
APPE: 'Command' = 'APPE', FEATCode.base, 'Append (with create)', CommandType.S, ConformanceRequirement.M
#: Authentication/Security Mechanism [2][:rfc:`2773`][:rfc:`4217`]
AUTH: 'Command' = 'AUTH', FEATCode.AUTH, 'Authentication/Security Mechanism', CommandType.A, ConformanceRequirement.O
#: Clear Command Channel [:rfc:`2228`]
CCC: 'Command' = 'CCC', FEATCode.secu, 'Clear Command Channel', CommandType.A, ConformanceRequirement.O
#: Change to Parent Directory [:rfc:`959`]
CDUP: 'Command' = 'CDUP', FEATCode.base, 'Change to Parent Directory', CommandType.A, ConformanceRequirement.O
#: Confidentiality Protected Command [:rfc:`2228`]
CONF: 'Command' = 'CONF', FEATCode.secu, 'Confidentiality Protected Command', CommandType.A, ConformanceRequirement.O
#: Change Working Directory [:rfc:`959`]
CWD: 'Command' = 'CWD', FEATCode.base, 'Change Working Directory', CommandType.A, ConformanceRequirement.M
#: Delete File [:rfc:`959`]
DELE: 'Command' = 'DELE', FEATCode.base, 'Delete File', CommandType.S, ConformanceRequirement.M
#: Privacy Protected Command [:rfc:`2228`][:rfc:`2773`][:rfc:`4217`]
ENC: 'Command' = 'ENC', FEATCode.secu, 'Privacy Protected Command', CommandType.A, ConformanceRequirement.O
#: Extended Port [:rfc:`2428`]
EPRT: 'Command' = 'EPRT', FEATCode.nat6, 'Extended Port', CommandType.P, ConformanceRequirement.O
#: Extended Passive Mode [:rfc:`2428`]
EPSV: 'Command' = 'EPSV', FEATCode.nat6, 'Extended Passive Mode', CommandType.P, ConformanceRequirement.O
#: Feature Negotiation [:rfc:`2389`]
FEAT: 'Command' = 'FEAT', FEATCode.feat, 'Feature Negotiation', CommandType.A, ConformanceRequirement.M
#: Help [:rfc:`959`]
HELP: 'Command' = 'HELP', FEATCode.base, 'Help', CommandType.S, ConformanceRequirement.M
#: Hostname [:rfc:`7151`]
HOST: 'Command' = 'HOST', FEATCode.HOST, 'Hostname', CommandType.A, ConformanceRequirement.O
#: Language (for Server Messages) [:rfc:`2640`]
LANG: 'Command' = 'LANG', FEATCode.UTF8, 'Language (for Server Messages)', CommandType.P, ConformanceRequirement.O
#: List [:rfc:`959`][:rfc:`1123`]
LIST: 'Command' = 'LIST', FEATCode.base, 'List', CommandType.S, ConformanceRequirement.M
#: Data Port [:rfc:`1545`][:rfc:`1639`]
LPRT: 'Command' = 'LPRT', FEATCode.hist, 'Data Port', CommandType.P, ConformanceRequirement.H
#: Passive Mode [:rfc:`1545`][:rfc:`1639`]
LPSV: 'Command' = 'LPSV', FEATCode.hist, 'Passive Mode', CommandType.P, ConformanceRequirement.H
#: File Modification Time [:rfc:`3659`]
MDTM: 'Command' = 'MDTM', FEATCode.MDTM, 'File Modification Time', CommandType.S, ConformanceRequirement.O
#: Integrity Protected Command [:rfc:`2228`][:rfc:`2773`][:rfc:`4217`]
MIC: 'Command' = 'MIC', FEATCode.secu, 'Integrity Protected Command', CommandType.A, ConformanceRequirement.O
#: Make Directory [:rfc:`959`]
MKD: 'Command' = 'MKD', FEATCode.base, 'Make Directory', CommandType.S, ConformanceRequirement.O
#: List Directory (for machine) [:rfc:`3659`]
MLSD: 'Command' = 'MLSD', FEATCode.MLST, 'List Directory (for machine)', CommandType.S, ConformanceRequirement.O
#: List Single Object [:rfc:`3659`]
MLST: 'Command' = 'MLST', FEATCode.MLST, 'List Single Object', CommandType.S, ConformanceRequirement.O
#: Transfer Mode [:rfc:`959`]
MODE: 'Command' = 'MODE', FEATCode.base, 'Transfer Mode', CommandType.P, ConformanceRequirement.M
#: Name List [:rfc:`959`][:rfc:`1123`]
NLST: 'Command' = 'NLST', FEATCode.base, 'Name List', CommandType.S, ConformanceRequirement.M
#: No-Op [:rfc:`959`]
NOOP: 'Command' = 'NOOP', FEATCode.base, 'No-Op', CommandType.S, ConformanceRequirement.M
#: Options [:rfc:`2389`]
OPTS: 'Command' = 'OPTS', FEATCode.feat, 'Options', CommandType.P, ConformanceRequirement.M
#: Password [:rfc:`959`]
PASS: 'Command' = 'PASS', FEATCode.base, 'Password', CommandType.A, ConformanceRequirement.M
#: Passive Mode [:rfc:`959`][:rfc:`1123`]
PASV: 'Command' = 'PASV', FEATCode.base, 'Passive Mode', CommandType.P, ConformanceRequirement.M
#: Protection Buffer Size [:rfc:`4217`]
PBSZ: 'Command' = 'PBSZ', FEATCode.PBSZ, 'Protection Buffer Size', CommandType.P, ConformanceRequirement.O
#: Data Port [:rfc:`959`]
PORT: 'Command' = 'PORT', FEATCode.base, 'Data Port', CommandType.P, ConformanceRequirement.M
#: Data Channel Protection Level [:rfc:`4217`]
PROT: 'Command' = 'PROT', FEATCode.PROT, 'Data Channel Protection Level', CommandType.P, ConformanceRequirement.O
#: Print Directory [:rfc:`959`]
PWD: 'Command' = 'PWD', FEATCode.base, 'Print Directory', CommandType.S, ConformanceRequirement.O
#: Logout [:rfc:`959`]
QUIT: 'Command' = 'QUIT', FEATCode.base, 'Logout', CommandType.A, ConformanceRequirement.M
#: Reinitialize [:rfc:`959`]
REIN: 'Command' = 'REIN', FEATCode.base, 'Reinitialize', CommandType.A, ConformanceRequirement.M
#: Restart (for STREAM mode) [3][:rfc:`3659`]
REST: 'Command' = 'REST', FEATCode.REST, 'Restart (for STREAM mode)', CommandType.S | CommandType.P, ConformanceRequirement.M
#: Retrieve [:rfc:`959`]
RETR: 'Command' = 'RETR', FEATCode.base, 'Retrieve', CommandType.S, ConformanceRequirement.M
#: Remove Directory [:rfc:`959`]
RMD: 'Command' = 'RMD', FEATCode.base, 'Remove Directory', CommandType.S, ConformanceRequirement.O
#: Rename From [:rfc:`959`]
RNFR: 'Command' = 'RNFR', FEATCode.base, 'Rename From', CommandType.S | CommandType.P, ConformanceRequirement.M
#: Rename To [:rfc:`959`][RFC Errata 5748]
RNTO: 'Command' = 'RNTO', FEATCode.base, 'Rename To', CommandType.S, ConformanceRequirement.M
#: Site Parameters [:rfc:`959`][:rfc:`1123`]
SITE: 'Command' = 'SITE', FEATCode.base, 'Site Parameters', CommandType.S, ConformanceRequirement.M
#: File Size [:rfc:`3659`]
SIZE: 'Command' = 'SIZE', FEATCode.SIZE, 'File Size', CommandType.S, ConformanceRequirement.O
#: Structure Mount [:rfc:`959`]
SMNT: 'Command' = 'SMNT', FEATCode.base, 'Structure Mount', CommandType.A, ConformanceRequirement.O
#: Status [:rfc:`959`]
STAT: 'Command' = 'STAT', FEATCode.base, 'Status', CommandType.S, ConformanceRequirement.M
#: Store [:rfc:`959`]
STOR: 'Command' = 'STOR', FEATCode.base, 'Store', CommandType.S, ConformanceRequirement.M
#: Store Unique [:rfc:`959`][:rfc:`1123`]
STOU: 'Command' = 'STOU', FEATCode.base, 'Store Unique', CommandType.A, ConformanceRequirement.O
#: File Structure [:rfc:`959`]
STRU: 'Command' = 'STRU', FEATCode.base, 'File Structure', CommandType.P, ConformanceRequirement.M
#: System [:rfc:`959`]
SYST: 'Command' = 'SYST', FEATCode.base, 'System', CommandType.S, ConformanceRequirement.O
#: Representation Type [4][:rfc:`959`]
TYPE: 'Command' = 'TYPE', FEATCode.base, 'Representation Type', CommandType.P, ConformanceRequirement.M
#: User Name [:rfc:`959`]
USER: 'Command' = 'USER', FEATCode.base, 'User Name', CommandType.A, ConformanceRequirement.M
#: None [:rfc:`775`][:rfc:`1123`]
XCUP: 'Command' = 'XCUP', FEATCode.hist, None, CommandType.S, ConformanceRequirement.H
#: None [:rfc:`775`][:rfc:`1123`]
XCWD: 'Command' = 'XCWD', FEATCode.hist, None, CommandType.S, ConformanceRequirement.H
#: None [:rfc:`775`][:rfc:`1123`]
XMKD: 'Command' = 'XMKD', FEATCode.hist, None, CommandType.S, ConformanceRequirement.H
#: None [:rfc:`775`][:rfc:`1123`]
XPWD: 'Command' = 'XPWD', FEATCode.hist, None, CommandType.S, ConformanceRequirement.H
#: None [:rfc:`775`][:rfc:`1123`]
XRMD: 'Command' = 'XRMD', FEATCode.hist, None, CommandType.S, ConformanceRequirement.H
#: Trivial Virtual File Store [:rfc:`3659`]
TVFS: 'Command' = 'TVFS', FEATCode.TVFS, 'Trivial Virtual File Store', CommandType.P, ConformanceRequirement.O
[docs]
@classmethod
def _unregistered_member(cls, value: 'str', name: 'str') -> 'Command':
"""Build a member absent from this registry's own lookup tables.
Leaves :attr:`feat`, :attr:`desc`, :attr:`type` and :attr:`conf` 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 and :meth:`__repr__` (which reads :attr:`desc`) would
raise on the result. There is no more specific value to reconstruct
them from -- a bare wire command word carries none of them,
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.http.method.Method`, 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.
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.feat = None
obj.desc = None
obj.type = CommandType.undefined
obj.conf = ConformanceRequirement.O
return obj
[docs]
@classmethod
def register(cls, value: 'str', name: 'str') -> 'Command':
"""Register a new command ``value`` under ``name``, upper-cased.
:meth:`get` and :meth:`_missing_` look a command up by its
upper-case name, so ``name`` is canonicalised to upper case here
before :meth:`~pcapkit.corekit.enum.EnumRegistry.register` mints it.
``value`` is kept as given, as a declared member's value is.
Args:
value: Value of the new member.
name: Name of the new member, in any case.
Returns:
The newly registered member.
Raises:
ValueError: As :meth:`~pcapkit.corekit.enum.EnumRegistry.register`.
"""
return super().register(value, name.upper())
[docs]
@classmethod
def register_alias(cls, value: 'str', name: 'str') -> 'Command':
"""Add ``name``, upper-cased, as a further name for the member at ``value``.
Canonicalised for the same reason as :meth:`register`, so that
:meth:`get` finds the alias.
Args:
value: Value of the existing member to alias.
name: Alias to add for it, in any case.
Returns:
The existing member, now reachable under the upper-cased ``name``
as well.
Raises:
ValueError: As
:meth:`~pcapkit.corekit.enum.EnumRegistry.register_alias`.
"""
return super().register_alias(value, name.upper())
[docs]
@classmethod
def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command':
"""Resolve ``key`` to a member, matching the command code case-insensitively.
A non-:class:`str` ``key`` is delegated to
:meth:`~pcapkit.corekit.enum.EnumLookup.get`, as
:meth:`Method.get <pcapkit.const.http.method.Method.get>` delegates
every key, so :meth:`_validate_value` refuses it there rather than
``key.upper()`` being called on it (GitHub issue :issue:`1824`). It is
a :class:`classmethod`, as the base is, because a zero-argument
``super()`` needs a first argument to bind. ``Command.get('X')`` binds
the same as it would on a :class:`staticmethod`.
``default`` is not forwarded on that path, as :meth:`Method.get <pcapkit.const.http.method.Method.get>` does not
forward its own: here it supplies the *value* of the unregistered
member built on a name miss, while the base's ``default`` must name a
*registered* value. A refused ``key`` therefore raises whatever
``default`` is.
Args:
key: Key to get enum item. Looked up case-insensitively, as
:rfc:`959#section-5` treats FTP command codes, against member
names canonicalised to upper case on registration.
default: Value for the unregistered member built when ``key``
matches no member name. ``None``, the default, uses ``key``
itself.
Returns:
The member named ``key`` upper-cased. On a miss, an unregistered
member with that name and the value ``default`` (or ``key``),
which is not added to the lookup tables, so ``get`` never registers
a command (:issue:`860`).
Raises:
EnumValueError: 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 :meth:`_missing_`. A :exc:`ValueError`, so an
``except ValueError`` caller is unaffected.
"""
if not isinstance(key, str):
# NOTE: unreachable only by the ``str`` annotation, which a caller
# at run time need not honour (GitHub issue #1824).
return super().get(key) # type: ignore[unreachable]
name = key.upper()
if name not in cls._member_map_: # pylint: disable=no-member
# NOTE: the value keeps the caller's casing (``default`` or ``key``), not ``name``;
# minting would cache the first casing seen for every later call (GitHub issue #860).
return cls._unregistered_member(default if default is not None else key, name)
return cls._member_map_[name] # pylint: disable=no-member
[docs]
@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:`register` refuses a value the constructor refuses, in the same
words (:issue:`1816`). :meth:`get` reaches it as well, through the
base, for every non-:class:`str` key (:issue:`1824`), so it refuses
the same values. Any :class:`str` passes, since :meth:`_missing_`
resolves 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__}')
[docs]
@classmethod
def _missing_(cls, value: 'str') -> 'Command':
"""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.
"""
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]
return cls._unregistered_member(value, name)