2323 :class:`~pcapkit.protocols.application.httpv1.HTTPv1` or
2424 :class:`~pcapkit.protocols.application.httpv2.HTTPv2` rather than an instance
2525 of it, so the data sits under ``httpv1`` or ``httpv2`` rather than ``http``
26- (:issue:`2524`).
26+ (:issue:`2524`). Building it from fields takes ``version`` and is refused
27+ without it, where it built HTTP/1.*, and its
28+ :meth:`~pcapkit.protocols.application.http.HTTP.from_data` refuses a
29+ :obj:`dict`, where it read the ``receipt`` or ``sid`` key (:issue:`2628`).
2730
2831"""
2932import contextlib
30- import inspect
3133import re
3234import struct
3335from typing import TYPE_CHECKING , Generic , cast
3436
3537from pcapkit .corekit .protochain import ProtoChain
3638from pcapkit .protocols .application .application import Application
37- from pcapkit .protocols .data .data import Data
3839from pcapkit .protocols .misc .no_payload import NoPayload
3940from pcapkit .protocols .protocol import _PT , _ST
4041from pcapkit .utilities .exceptions import ProtocolError
@@ -146,21 +147,6 @@ def _test_http2_frame(data: 'bytes') -> 'bool':
146147_HTTP2_PREFACE_HEADER = _HTTP2_PREFACE .split (b'\r \n \r \n ' , maxsplit = 1 )[0 ]
147148
148149
149- def _make_fields (klass : 'Type[HTTP]' ) -> 'frozenset[str]' :
150- """Names of the fields ``klass.make`` takes, as keywords.
151-
152- Args:
153- klass: An HTTP member class.
154-
155- Returns:
156- Every named parameter of its ``make`` but ``self``.
157-
158- """
159- kinds = (inspect .Parameter .POSITIONAL_OR_KEYWORD , inspect .Parameter .KEYWORD_ONLY )
160- return frozenset (name for name , param in inspect .signature (klass .make ).parameters .items ()
161- if name != 'self' and param .kind in kinds )
162-
163-
164150def test_start_line (data : 'bytes' ) -> 'bool' :
165151 """Whether ``data`` opens with an HTTP/1.* start line.
166152
@@ -237,7 +223,11 @@ class HTTP(Application[_PT, _ST], Generic[_PT, _ST]):
237223 and :meth:`from_data` give
238224 :class:`~pcapkit.protocols.application.httpv1.HTTPv1` or
239225 :class:`~pcapkit.protocols.application.httpv2.HTTPv2`, never an instance of
240- this class (:issue:`2524`); see :meth:`__new__`.
226+ this class (:issue:`2524`). ``version`` names the member; without it, octets
227+ give the one :meth:`_dispatch_version` identifies, and fields are refused,
228+ since no field picks a member (:issue:`2574`). :meth:`from_data` gives the
229+ member whose data model it is handed, and refuses a :obj:`dict`. See
230+ :meth:`__new__`.
241231
242232 """
243233
@@ -383,13 +373,14 @@ def read(self, length: 'Optional[int]' = None, *,
383373 return info
384374
385375 def make (self ,
386- version : 'Literal[1, 2]' = 1 , * ,
376+ version : 'Optional[ Literal[1, 2]] ' = None , * ,
387377 preface : 'bool' = False ,
388378 ** kwargs : 'Any' ) -> '_ST' :
389379 """Make (construct) packet data.
390380
391381 Args:
392- version: Version of HTTP.
382+ version: Version of HTTP, ``1`` or ``2``. There is no default: no
383+ field picks a member (:issue:`2574`).
393384 preface: Whether the HTTP/2 connection preface goes ahead of the
394385 frame; :meth:`pack` writes it.
395386 **kwargs: Arbitrary keyword arguments.
@@ -398,20 +389,16 @@ def make(self,
398389 The header schema the versioned ``make`` constructs.
399390
400391 Raises:
401- ProtocolError: If ``version`` is unknown, or if ``preface`` is
402- requested for a version other than HTTP/2.
392+ ProtocolError: If ``version`` is missing or names neither member, or
393+ if ``preface`` is requested for a version other than HTTP/2.
403394
404395 """
396+ if version is None :
397+ raise ProtocolError ('HTTP: no version to build with; pass version=1 or version=2' )
398+ protocol = self ._member_of_version (version )
405399 if preface and version != 2 :
406400 raise ProtocolError (f'HTTP/{ version } : connection preface is HTTP/2 only' )
407401
408- if version == 1 :
409- from pcapkit .protocols .application .httpv1 import HTTPv1 as protocol # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
410- elif version == 2 :
411- from pcapkit .protocols .application .httpv2 import HTTPv2 as protocol # type: ignore[assignment] # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
412- else :
413- raise ProtocolError (f'HTTP: invalid version: { version !r} ' )
414-
415402 # NOTE: ``protocol.make`` is an ordinary instance method (the abstract
416403 # declaration at ``ProtocolBase.make`` takes ``self``, and the
417404 # versioned overrides use instance-bound helpers such as
@@ -447,56 +434,87 @@ def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self':
447434 """Create protocol instance from data.
448435
449436 Args:
450- data: Protocol data. A :obj:`dict`, as :meth:`Info.to_dict
451- <pcapkit.corekit.infoclass.Info.to_dict>` returns it, is rebuilt
452- into the versioned data model its keys belong to -- ``receipt``
453- is HTTP/1.*'s and ``sid`` HTTP/2's -- since this class's own data
454- model carries no version.
437+ data: Protocol data.
455438 **kwargs: As :meth:`Protocol.from_data
456439 <pcapkit.protocols.protocol.Protocol.from_data>`.
457440
458441 Returns:
459- Protocol instance; on this class, an instance of the member the data
460- model belongs to (:issue:`2524`).
442+ Protocol instance; on this class, an instance of the member whose data
443+ model ``data`` is (:issue:`2524`).
461444
462445 Raises:
463- ProtocolError: If ``data`` is neither HTTP/1.* nor HTTP/2 data, on
464- this class.
446+ ProtocolError: If ``data`` is neither member's data model, on this
447+ class. A :obj:`dict` is refused: it carries no version, and no
448+ key picks a member (:issue:`2574`).
465449
466450 """
451+ if cls is not HTTP :
452+ return super ().from_data (data , ** kwargs )
453+
467454 from pcapkit .protocols .application .httpv1 import HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
468455 from pcapkit .protocols .application .httpv2 import HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
469456 from pcapkit .protocols .data .application .httpv1 import HTTPv1 as Data_HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
470457 from pcapkit .protocols .data .application .httpv2 import HTTPv2 as Data_HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel
471458
472- if not isinstance (data , Data ) and not issubclass (cls .__data__ , (Data_HTTPv1 , Data_HTTPv2 )):
473- if 'receipt' in data :
474- data = cast ('_PT' , Data_HTTPv1 .from_dict (data ))
475- elif 'sid' in data :
476- data = cast ('_PT' , Data_HTTPv2 .from_dict (data ))
477- if cls is not HTTP :
478- return super ().from_data (data , ** kwargs )
479459 if isinstance (data , Data_HTTPv1 ):
480460 return cast ('Self' , HTTPv1 .from_data (data , ** kwargs ))
481461 if isinstance (data , Data_HTTPv2 ):
482462 return cast ('Self' , HTTPv2 .from_data (data , ** kwargs ))
483- raise ProtocolError (f'HTTP: invalid data: must be HTTP/1.* or HTTP/2 data, '
484- f'not { type (data ).__name__ } ' )
463+ raise ProtocolError ('HTTP: invalid data: must be an HTTPv1 or HTTPv2 data model' )
485464
486465 ##########################################################################
487466 # Data models.
488467 ##########################################################################
489468
469+ def __init__ (self , file : 'Optional[IO[bytes] | bytes]' = None ,
470+ length : 'Optional[int]' = None , * ,
471+ version : 'Optional[Literal[1, 2]]' = None , ** kwargs : 'Any' ) -> 'None' :
472+ """Initialisation.
473+
474+ Args:
475+ file: Source packet stream.
476+ length: Length of packet data.
477+ version: Version of HTTP, ``1`` or ``2``: the member :meth:`__new__`
478+ picks. Building from fields, it must be this class's own. Parsing,
479+ it is handed on unread.
480+ **kwargs: As :meth:`Protocol.__init__
481+ <pcapkit.protocols.protocol.Protocol.__init__>`.
482+
483+ Raises:
484+ ProtocolError: If, building from fields, ``version`` names neither
485+ member, or is not this class's own version: that of the member
486+ it is or derives from, which a subclass outside the members
487+ does not have; or if ``preface`` is asked of HTTP/1.
488+
489+ """
490+ if file is not None :
491+ if version is not None :
492+ kwargs ['version' ] = version
493+ else :
494+ from pcapkit .protocols .application .httpv1 import HTTPv1 # isort: skip # pylint: disable=import-outside-toplevel
495+ from pcapkit .protocols .application .httpv2 import HTTPv2 # isort: skip # pylint: disable=import-outside-toplevel
496+
497+ if version is not None and not isinstance (self , self ._member_of_version (version )):
498+ if not isinstance (self , (HTTPv1 , HTTPv2 )):
499+ raise ProtocolError (f'{ type (self ).__name__ } : no version of its own to match '
500+ f'version={ version !r} ' )
501+ raise ProtocolError (f'{ type (self ).__name__ } : version={ version !r} '
502+ 'names another member' )
503+ # NOTE: ``pack`` writes the preface for any member, so HTTP/1 refuses it here.
504+ if kwargs .get ('preface' ) and isinstance (self , HTTPv1 ):
505+ raise ProtocolError ('HTTP/1: connection preface is HTTP/2 only' )
506+ super ().__init__ (file , length , ** kwargs ) # type: ignore[arg-type]
507+
490508 def __new__ (cls , file : 'Optional[IO[bytes] | bytes]' = None , # type: ignore[misc]
491509 length : 'Optional[int]' = None , ** kwargs : 'Any' ) -> 'HTTP' :
492510 """Create the member a construction of this class describes.
493511
494- Parsing octets gives the member an explicit ``version`` names, or else
495- the one :meth:`_dispatch_version` identifies; building from fields gives
496- the one :meth:`_dispatch_fields` picks. That member's ``__init__`` then
497- runs with the same arguments, which it accepts, since a member inherits
498- this class's ``version `` and ``preface`` keywords . A subclass of this
499- class is created as itself.
512+ The ``version`` keyword names the member, on either path. Without it,
513+ parsing octets gives the member :meth:`_dispatch_version` identifies,
514+ since HTTP octets carry their own version, and building from fields is
515+ refused, since no field picks a member (:issue:`2574`). That member's
516+ ``__init__ `` then runs with the same arguments . A subclass of this class
517+ is created as itself.
500518
501519 Args:
502520 file: Source packet stream, read here without being consumed.
@@ -507,17 +525,17 @@ class is created as itself.
507525 An uninitialised instance of the member.
508526
509527 Raises:
510- ProtocolError: If the octets are neither version's, including an
511- HTTP/2 connection preface with no frame after it, or if
512- :meth:`_dispatch_fields` refuses the fields .
528+ ProtocolError: If ``version`` names neither member; or, without it,
529+ if building from fields, or if the octets are neither version's,
530+ including an HTTP/2 connection preface with no frame after it .
513531
514532 """
515533 if cls is not HTTP :
516534 return super ().__new__ (cls , file , length , ** kwargs ) # type: ignore[call-arg]
517- if file is None :
518- member = cls ._dispatch_fields (kwargs )
519- elif kwargs .get ('version' ) is not None :
535+ if kwargs .get ('version' ) is not None :
520536 member = cls ._member_of_version (kwargs ['version' ])
537+ elif file is None :
538+ raise ProtocolError ('HTTP: no version to build with; pass version=1 or version=2' )
521539 else :
522540 if isinstance (file , bytes ):
523541 data = file
@@ -815,53 +833,6 @@ def _dispatch_version(cls, data: 'bytes', length: 'int') -> 'Type[ProtocolBase]'
815833 return HTTPv2
816834 return Raw
817835
818- @classmethod
819- def _dispatch_fields (cls , fields : 'dict[str, Any]' ) -> 'Type[HTTP]' :
820- """Pick the member that builds a packet from the keyword ``fields``.
821-
822- An explicit ``version`` names the member. Otherwise a field that only
823- :meth:`HTTPv2.make <pcapkit.protocols.application.httpv2.HTTPv2.make>`
824- takes picks :class:`~pcapkit.protocols.application.httpv2.HTTPv2`, and a
825- field that only :meth:`HTTPv1.make
826- <pcapkit.protocols.application.httpv1.HTTPv1.make>` takes, or no field
827- of either, picks :class:`~pcapkit.protocols.application.httpv1.HTTPv1`,
828- the version :meth:`make` defaults to. ``preface`` counts only when true,
829- as in the ``version`` check, since its default is :data:`False`.
830-
831- Args:
832- fields: Construction keywords.
833-
834- Returns:
835- :class:`~pcapkit.protocols.application.httpv1.HTTPv1` or
836- :class:`~pcapkit.protocols.application.httpv2.HTTPv2`.
837-
838- Raises:
839- ProtocolError: If ``version`` is neither ``1`` nor ``2``, if
840- ``preface`` is asked of HTTP/1, or if ``fields`` holds fields
841- only HTTP/1 takes and fields only HTTP/2 takes.
842-
843- """
844- from pcapkit .protocols .application .httpv1 import HTTPv1 # isort: skip # pylint: disable=import-outside-toplevel
845- from pcapkit .protocols .application .httpv2 import HTTPv2 # isort: skip # pylint: disable=import-outside-toplevel
846-
847- version = fields .get ('version' )
848- if version is not None :
849- member = cls ._member_of_version (version )
850- if fields .get ('preface' ) and member is not HTTPv2 :
851- raise ProtocolError (f'HTTP/{ version } : connection preface is HTTP/2 only' )
852- return member
853-
854- # NOTE: ``preface=False`` is the default, not a request for HTTP/2.
855- names = {name for name , value in fields .items () if name != 'preface' or value }
856- own1 , own2 = _make_fields (HTTPv1 ), _make_fields (HTTPv2 )
857- only1 , only2 = names & (own1 - own2 ), names & (own2 - own1 )
858- if only1 and only2 :
859- if only2 == {'preface' }:
860- raise ProtocolError ('HTTP/1: connection preface is HTTP/2 only' )
861- raise ProtocolError (f'HTTP: fields of both HTTP/1 ({ ", " .join (sorted (only1 ))} ) '
862- f'and HTTP/2 ({ ", " .join (sorted (only2 ))} )' )
863- return HTTPv2 if only2 else HTTPv1
864-
865836 @staticmethod
866837 def _member_of_version (version : 'Any' ) -> 'Type[HTTP]' :
867838 """The member a ``version`` of ``1`` or ``2`` names.
@@ -874,14 +845,16 @@ def _member_of_version(version: 'Any') -> 'Type[HTTP]':
874845 :class:`~pcapkit.protocols.application.httpv2.HTTPv2`.
875846
876847 Raises:
877- ProtocolError: If ``version`` is neither.
848+ ProtocolError: If ``version`` is neither, or is no :obj:`int`: a
849+ :obj:`bool` or a :obj:`float` is refused even where it equals one.
878850
879851 """
880852 from pcapkit .protocols .application .httpv1 import HTTPv1 # isort: skip # pylint: disable=import-outside-toplevel
881853 from pcapkit .protocols .application .httpv2 import HTTPv2 # isort: skip # pylint: disable=import-outside-toplevel
882854
883- if version == 1 :
884- return HTTPv1
885- if version == 2 :
886- return HTTPv2
855+ if isinstance (version , int ) and not isinstance (version , bool ):
856+ if version == 1 :
857+ return HTTPv1
858+ if version == 2 :
859+ return HTTPv2
887860 raise ProtocolError (f'HTTP: invalid version: { version !r} ' )
0 commit comments