Source code for pcapkit.const.ftp.command

# -*- 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 was the same defect shape :issue:`775` removed from :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 our current 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')`` raised :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 still raises :exc:`KeyError` while ``FEATCode('ZZ-NOT-REAL')`` still yields an unregistered member, exactly as before. 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())
class CommandType(EnumLookup, IntFlag): """Type of "kind" of command, based on :rfc:`959#section-4`. Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub issue :issue:`930`, finishing :issue:`877`'s phase 2. Pure re-parenting as far as ``get``/``get_all`` are concerned -- this class defines no ``get`` of its own to reconcile with the base -- and its own :meth:`_missing_` range guard below is untouched, since :class:`EnumLookup` does not touch that hook. """ undefined = 0 #: Access control. A = auto() #: Parameter setting. P = auto() #: Service execution. S = auto() @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) class ConformanceRequirement(EnumLookup, IntEnum): """Expectation for support in modern FTP implementations. Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub issue :issue:`930`, finishing :issue:`877`'s phase 2 -- pure re-parenting, since this class defines neither ``get`` nor ``_missing_`` of its own to reconcile with the base. """ #: Mandatory to implement. M = auto() #: Optional. O = auto() #: Historic. H = auto()
[docs] class Command(EnumRegistry, StrEnum): """[Command] FTP Command .. versionchanged:: 1.5.0 Neither ``_missing_`` nor ``get()`` mints a member. The owner ruled on GitHub issue :issue:`860` that ``get`` should not mint: only IANA-registered values are legitimate members, a new one is properly created through :meth:`register`, and ``get`` is not given enough information to create one. Concretely true here -- a bare wire command word carries no :attr:`feat`/:attr:`desc`/:attr:`type`/:attr:`conf`, so a minted member had each of them hollowed out to its default; :meth:`register` is the path that can actually supply them. """ 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]': obj = str.__new__(cls, name) obj._value_ = name obj.feat = feat obj.desc = desc obj.type = type obj.conf = conf return obj def __repr__(self) -> 'str': 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())
@classmethod def get(cls, key: 'str', default: 'Optional[str]' = None) -> 'Command': """Backport support for original codes. 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 ``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. .. versionchanged:: 1.5.0 A non-:class:`str` ``key`` raises :exc:`~pcapkit.utilities.exceptions.EnumValueError`, with or without ``default`` (:issue:`1824`). ``get(42)`` and ``get(None)`` raised :exc:`AttributeError` from ``key.upper()``, and ``get(b'ABOR')`` returned an unregistered member whose name and value were both the :class:`bytes` object. 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. 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. :meta private: """ if not isinstance(key, str): # NOTE: unreachable only by the ``str`` annotation, which a caller # at run time need not honour -- this is the path GitHub issue # #1824 fixed. return super().get(key) # type: ignore[unreachable] name = key.upper() if name not in cls._member_map_: # pylint: disable=no-member # NOTE: the value is ``default`` if the caller supplied one, or # else ``key`` exactly as given -- never ``name`` -- 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 # unchanged. Two calls naming the same command in different # case, e.g. ``get('xyzw')`` and ``get('XYZW')``, therefore build # results that are *not* equal -- each is exactly what its own # caller passed. Minting instead returned, through the # ``_member_map_`` cache, the *first* casing seen for every later # call regardless of case; that is the one observable behaviour # change in GitHub issue #860's conversion. 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] # type: ignore[return-value] return cls._unregistered_member(value, name)