diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7617e1f..ff4ced2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,12 @@ jobs: max-parallel: 7 matrix: python-version: - - 3.8 - - 3.9 - "3.10" - "3.11" - "3.12" - "3.13" - - pypy-3.9 - - pypy-3.10 + - "3.14" + - pypy-3.11 steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 7ad0c44..bdcd02d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ __pycache__/ *.egg-info/ .installed.cfg *.egg +.pdm-python # Installer logs pip-log.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 38d4fcc..b09cdf6 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,11 +1,11 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 apt_packages: - graphviz tools: - python: "3.8" + python: "3.10" sphinx: configuration: docs/source/conf.py diff --git a/docs/source/_examples/myclient.py b/docs/source/_examples/myclient.py index 1baaf64..8100f45 100644 --- a/docs/source/_examples/myclient.py +++ b/docs/source/_examples/myclient.py @@ -1,6 +1,9 @@ -import socket, ssl +import socket +import ssl + import h11 + class MyHttpClient: def __init__(self, host, port): self.sock = socket.create_connection((host, port)) diff --git a/docs/source/conf.py b/docs/source/conf.py index b3627f5..eea682f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os ################################################################ @@ -30,31 +29,34 @@ # directory, so there's no single value of this that works for both.) # import os.path +import sys + sys._h11_hack_docs_source_path = os.path.dirname(__file__) ################################################################ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'IPython.sphinxext.ipython_directive', - 'IPython.sphinxext.ipython_console_highlighting', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "IPython.sphinxext.ipython_directive", + "IPython.sphinxext.ipython_console_highlighting", ] + # Undocumented trick: if we def setup here in conf.py, it gets called just # like an extension's setup function. def setup(app): @@ -62,24 +64,25 @@ def setup(app): app.add_javascript("facebox.js") app.add_stylesheet("facebox.css") + # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'h11' -copyright = '2016, Nathaniel J. Smith' -author = 'Nathaniel J. Smith' +project = "h11" +copyright = "2016, Nathaniel J. Smith" +author = "Nathaniel J. Smith" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -87,6 +90,7 @@ def setup(app): # # The short X.Y version. import h11 + version = h11.__version__ # The full version, including alpha/beta/rc tags. release = h11.__version__ @@ -100,9 +104,9 @@ def setup(app): # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -111,27 +115,27 @@ def setup(app): # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -146,153 +150,146 @@ def setup(app): # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. -#html_title = 'h11 v0.0.1' +# html_title = 'h11 v0.0.1' # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None +# html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'h11doc' +htmlhelp_basename = "h11doc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'h11.tex', 'h11 Documentation', - 'Nathaniel J. Smith', 'manual'), + (master_doc, "h11.tex", "h11 Documentation", "Nathaniel J. Smith", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'h11', 'h11 Documentation', - [author], 1) -] +man_pages = [(master_doc, "h11", "h11 Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -301,25 +298,31 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'h11', 'h11 Documentation', - author, 'h11', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "h11", + "h11 Documentation", + author, + "h11", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3.5', None), + "python": ("https://docs.python.org/3.5", None), } diff --git a/docs/source/make-state-diagrams.py b/docs/source/make-state-diagrams.py index 617efa5..130da8e 100644 --- a/docs/source/make-state-diagrams.py +++ b/docs/source/make-state-diagrams.py @@ -1,6 +1,7 @@ #!python import sys + sys.path.append("../..") import os.path @@ -9,8 +10,10 @@ from h11._events import * from h11._state import * from h11._state import ( - _SWITCH_UPGRADE, _SWITCH_CONNECT, - EVENT_TRIGGERED_TRANSITIONS, STATE_TRIGGERED_TRANSITIONS, + _SWITCH_CONNECT, + _SWITCH_UPGRADE, + EVENT_TRIGGERED_TRANSITIONS, + STATE_TRIGGERED_TRANSITIONS, ) _EVENT_COLOR = "#002092" @@ -24,13 +27,15 @@ edge [fontname = "Lato"] """ + def finish(machine_name): - return (""" + return f""" labelloc="t" labeljust="l" - label=<h11 state machine: {}> + label=<h11 state machine: {machine_name}> }} -""".format(machine_name)) +""" + class Edges: def __init__(self): @@ -40,19 +45,20 @@ def e(self, source, target, label, color, italicize=False, weight=1): if italicize: quoted_label = f"<{label}>" else: - quoted_label = f'<{label}>' + quoted_label = f"<{label}>" self.edges.append( - f'{source} -> {target} [\n' - f' label={quoted_label},\n' + f"{source} -> {target} [\n" + f" label={quoted_label},\n" f' color="{color}", fontcolor="{color}",\n' - f' weight={weight},\n' - f']\n' - ) + f" weight={weight},\n" + f"]\n" + ) def write(self, f): self.edges.sort() f.write("".join(self.edges)) + def make_dot_special_state(out_path): with open(out_path, "w") as f: f.write(HEADER) @@ -68,29 +74,35 @@ def make_dot_special_state(out_path): """) edges = Edges() for s in ["kaT", "kaF"]: - edges.e(s, "kaF", - "Request/response with
HTTP/1.0 or Connection: close", - color=_EVENT_COLOR, - italicize=True) - - edges.e("upF", "upT", - "Request with Upgrade:", - color=_EVENT_COLOR, italicize=True) - edges.e("upT", "upF", - "Response", - color=_EVENT_COLOR, italicize=True) - - edges.e("coF", "coT", - "Request with CONNECT", - color=_EVENT_COLOR, italicize=True) - edges.e("coT", "coF", - "Response without 2xx status", - color=_EVENT_COLOR, italicize=True) + edges.e( + s, + "kaF", + "Request/response with
HTTP/1.0 or Connection: close", + color=_EVENT_COLOR, + italicize=True, + ) + + edges.e( + "upF", "upT", "Request with Upgrade:", color=_EVENT_COLOR, italicize=True + ) + edges.e("upT", "upF", "Response", color=_EVENT_COLOR, italicize=True) + + edges.e( + "coF", "coT", "Request with CONNECT", color=_EVENT_COLOR, italicize=True + ) + edges.e( + "coT", + "coF", + "Response without 2xx status", + color=_EVENT_COLOR, + italicize=True, + ) edges.write(f) f.write(finish("special states")) + def make_dot(role, out_path): with open(out_path, "w") as f: f.write(HEADER) @@ -109,23 +121,21 @@ def make_dot(role, out_path): # of the file to fix them. edges = Edges() - CORE_EVENTS = {Request, InformationalResponse, - Response, Data, EndOfMessage} + CORE_EVENTS = {Request, InformationalResponse, Response, Data, EndOfMessage} - for (source_state, t) in EVENT_TRIGGERED_TRANSITIONS[role].items(): - for (event_type, target_state) in t.items(): + for source_state, t in EVENT_TRIGGERED_TRANSITIONS[role].items(): + for event_type, target_state in t.items(): weight = 1 color = _EVENT_COLOR italicize = False - if (event_type in CORE_EVENTS - and source_state is not target_state): + if event_type in CORE_EVENTS and source_state is not target_state: weight = 10 # exception - if (event_type is Response and source_state is IDLE): + if event_type is Response and source_state is IDLE: weight = 1 if isinstance(event_type, tuple): # The weird special cases - #color = _SPECIAL_COLOR + # color = _SPECIAL_COLOR if event_type == (Request, CLIENT): name = "client makes Request" weight = 10 @@ -139,8 +149,14 @@ def make_dot(role, out_path): assert False else: name = event_type.__name__ - edges.e(source_state, target_state, name, color, - weight=weight, italicize=italicize) + edges.e( + source_state, + target_state, + name, + color, + weight=weight, + italicize=italicize, + ) for state_pair, updates in STATE_TRIGGERED_TRANSITIONS.items(): if role not in updates: @@ -149,22 +165,32 @@ def make_dot(role, out_path): (our_state, their_state) = state_pair else: (their_state, our_state) = state_pair - edges.e(our_state, updates[role], - f"peer in
{their_state}", - color=_STATE_COLOR) + edges.e( + our_state, + updates[role], + f"peer in
{their_state}", + color=_STATE_COLOR, + ) if role is CLIENT: - edges.e(DONE, MIGHT_SWITCH_PROTOCOL, - "Potential Upgrade:
or CONNECT pending", - _STATE_COLOR, - italicize=True) - edges.e(MIGHT_SWITCH_PROTOCOL, DONE, - "No potential Upgrade:
or CONNECT pending", - _STATE_COLOR, - italicize=True) - - edges.e(DONE, MUST_CLOSE, "keep-alive
is disabled", _STATE_COLOR, - italicize=True) + edges.e( + DONE, + MIGHT_SWITCH_PROTOCOL, + "Potential Upgrade:
or CONNECT pending", + _STATE_COLOR, + italicize=True, + ) + edges.e( + MIGHT_SWITCH_PROTOCOL, + DONE, + "No potential Upgrade:
or CONNECT pending", + _STATE_COLOR, + italicize=True, + ) + + edges.e( + DONE, MUST_CLOSE, "keep-alive
is disabled", _STATE_COLOR, italicize=True + ) edges.e(DONE, IDLE, "start_next_cycle()", _SPECIAL_COLOR) edges.write(f) @@ -173,6 +199,7 @@ def make_dot(role, out_path): # works f.write(finish(role)) + my_dir = os.path.dirname(__file__) out_dir = os.path.join(my_dir, "_static") if not os.path.exists(out_dir): diff --git a/examples/trio-server.py b/examples/trio-server.py index 996afb6..81ad829 100644 --- a/examples/trio-server.py +++ b/examples/trio-server.py @@ -260,18 +260,17 @@ async def http_serve(stream): wrapper.info("connection is not reusable, so shutting down") await wrapper.shutdown_and_clean_up() return - else: - try: - wrapper.info("trying to re-use connection") - wrapper.conn.start_next_cycle() - except h11.ProtocolError: - states = wrapper.conn.states - wrapper.info("unexpected state", states, "-- bailing out") - await maybe_send_error_response( - wrapper, RuntimeError(f"unexpected state {states}") - ) - await wrapper.shutdown_and_clean_up() - return + try: + wrapper.info("trying to re-use connection") + wrapper.conn.start_next_cycle() + except h11.ProtocolError: + states = wrapper.conn.states + wrapper.info("unexpected state", states, "-- bailing out") + await maybe_send_error_response( + wrapper, RuntimeError(f"unexpected state {states}") + ) + await wrapper.shutdown_and_clean_up() + return ################################################################ diff --git a/format-requirements.txt b/format-requirements.txt deleted file mode 100644 index a45e8c9..0000000 --- a/format-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -black==23.3.0 -isort==5.12.0 \ No newline at end of file diff --git a/h11/__init__.py b/h11/__init__.py index 989e92c..34167db 100644 --- a/h11/__init__.py +++ b/h11/__init__.py @@ -6,28 +6,30 @@ # semantics to check that what you're asking to write to the wire is sensible, # but at least it gets you out of dealing with the wire itself. -from h11._connection import Connection, NEED_DATA, PAUSED +# ruff: noqa: I001 + +from h11._connection import NEED_DATA, PAUSED, Connection from h11._events import ( - ConnectionClosed, - Data, - EndOfMessage, - Event, - InformationalResponse, - Request, - Response, + ConnectionClosed as ConnectionClosed, + Data as Data, + EndOfMessage as EndOfMessage, + Event as Event, + InformationalResponse as InformationalResponse, + Request as Request, + Response as Response, ) from h11._state import ( - CLIENT, - CLOSED, - DONE, - ERROR, - IDLE, - MIGHT_SWITCH_PROTOCOL, - MUST_CLOSE, - SEND_BODY, - SEND_RESPONSE, - SERVER, - SWITCHED_PROTOCOL, + CLIENT as CLIENT, + CLOSED as CLOSED, + DONE as DONE, + ERROR as ERROR, + IDLE as IDLE, + MIGHT_SWITCH_PROTOCOL as MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE as MUST_CLOSE, + SEND_BODY as SEND_BODY, + SEND_RESPONSE as SEND_RESPONSE, + SERVER as SERVER, + SWITCHED_PROTOCOL as SWITCHED_PROTOCOL, ) from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError from h11._version import __version__ @@ -36,27 +38,28 @@ __all__ = ( - "Connection", - "NEED_DATA", - "PAUSED", - "ConnectionClosed", - "Data", - "EndOfMessage", - "Event", - "InformationalResponse", - "Request", - "Response", "CLIENT", "CLOSED", "DONE", "ERROR", "IDLE", + "MIGHT_SWITCH_PROTOCOL", "MUST_CLOSE", + "NEED_DATA", + "PAUSED", "SEND_BODY", "SEND_RESPONSE", "SERVER", "SWITCHED_PROTOCOL", - "ProtocolError", + "Connection", + "ConnectionClosed", + "Data", + "EndOfMessage", + "Event", + "InformationalResponse", "LocalProtocolError", + "ProtocolError", "RemoteProtocolError", + "Request", + "Response", ) diff --git a/h11/_connection.py b/h11/_connection.py index e37d82a..77031e4 100644 --- a/h11/_connection.py +++ b/h11/_connection.py @@ -1,16 +1,10 @@ # This contains the main Connection class. Everything in h11 revolves around # this. +from collections.abc import Callable from typing import ( Any, - Callable, cast, - Dict, - List, - Optional, overload, - Tuple, - Type, - Union, ) from ._events import ( @@ -29,13 +23,13 @@ _SWITCH_CONNECT, _SWITCH_UPGRADE, CLIENT, - ConnectionState, DONE, ERROR, MIGHT_SWITCH_PROTOCOL, SEND_BODY, SERVER, SWITCHED_PROTOCOL, + ConnectionState, ) from ._util import ( # Import the internal things we need LocalProtocolError, @@ -45,10 +39,10 @@ from ._writers import WRITERS, WritersType # Everything in __all__ gets re-exported as part of the h11 public API. -__all__ = ["Connection", "NEED_DATA", "PAUSED"] +__all__ = ["NEED_DATA", "PAUSED", "Connection"] -class NEED_DATA(Sentinel, metaclass=Sentinel): +class NEED_DATA(Sentinel, metaclass=Sentinel): # noqa: N801 pass @@ -81,18 +75,17 @@ class PAUSED(Sentinel, metaclass=Sentinel): # our rule is: # - If someone says Connection: close, we will close # - If someone uses HTTP/1.0, we will close. -def _keep_alive(event: Union[Request, Response]) -> bool: +def _keep_alive(event: Request | Response) -> bool: connection = get_comma_header(event.headers, b"connection") if b"close" in connection: return False - if getattr(event, "http_version", b"1.1") < b"1.1": - return False - return True + + return not getattr(event, "http_version", b"1.1") < b"1.1" def _body_framing( - request_method: bytes, event: Union[Request, Response] -) -> Tuple[str, Union[Tuple[()], Tuple[int]]]: + request_method: bytes | None, event: Request | Response +) -> tuple[str, tuple[()] | tuple[int]]: # Called when we enter SEND_BODY to figure out framing information for # this body. # @@ -137,8 +130,7 @@ def _body_framing( # Step 4: no applicable headers; fallback/default depends on type if type(event) is Request: return ("content-length", (0,)) - else: - return ("http/1.0", ()) + return ("http/1.0", ()) ################################################################ @@ -166,7 +158,7 @@ class Connection: def __init__( self, - our_role: Type[Sentinel], + our_role: type[Sentinel], max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, ) -> None: self._max_incomplete_event_size = max_incomplete_event_size @@ -174,7 +166,7 @@ def __init__( if our_role not in (CLIENT, SERVER): raise ValueError(f"expected CLIENT or SERVER, not {our_role!r}") self.our_role = our_role - self.their_role: Type[Sentinel] + self.their_role: type[Sentinel] if our_role is CLIENT: self.their_role = SERVER else: @@ -197,14 +189,14 @@ def __init__( # These two are only used to interpret framing headers for figuring # out how to read/write response bodies. their_http_version is also # made available as a convenient public API. - self.their_http_version: Optional[bytes] = None - self._request_method: Optional[bytes] = None + self.their_http_version: bytes | None = None + self._request_method: bytes | None = None # This is pure flow-control and doesn't at all affect the set of legal # transitions, so no need to bother ConnectionState with it: self.client_is_waiting_for_100_continue = False @property - def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]: + def states(self) -> dict[type[Sentinel], type[Sentinel]]: """A dictionary like:: {CLIENT: , SERVER: } @@ -215,14 +207,14 @@ def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]: return dict(self._cstate.states) @property - def our_state(self) -> Type[Sentinel]: + def our_state(self) -> type[Sentinel]: """The current state of whichever role we are playing. See :ref:`state-machine` for details. """ return self._cstate.states[self.our_role] @property - def their_state(self) -> Type[Sentinel]: + def their_state(self) -> type[Sentinel]: """The current state of whichever role we are NOT playing. See :ref:`state-machine` for details. """ @@ -252,24 +244,25 @@ def start_next_cycle(self) -> None: assert not self.client_is_waiting_for_100_continue self._respond_to_state_changes(old_states) - def _process_error(self, role: Type[Sentinel]) -> None: + def _process_error(self, role: type[Sentinel]) -> None: old_states = dict(self._cstate.states) self._cstate.process_error(role) self._respond_to_state_changes(old_states) - def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]: + def _server_switch_event(self, event: Event) -> type[Sentinel] | None: if type(event) is InformationalResponse and event.status_code == 101: return _SWITCH_UPGRADE - if type(event) is Response: - if ( - _SWITCH_CONNECT in self._cstate.pending_switch_proposals - and 200 <= event.status_code < 300 - ): - return _SWITCH_CONNECT + + if type(event) is Response and ( + _SWITCH_CONNECT in self._cstate.pending_switch_proposals + and 200 <= event.status_code < 300 + ): + return _SWITCH_CONNECT + return None # All events go through here - def _process_event(self, role: Type[Sentinel], event: Event) -> None: + def _process_event(self, role: type[Sentinel], event: Event) -> None: # First, pass the event through the state machine to make sure it # succeeds. old_states = dict(self._cstate.states) @@ -293,7 +286,7 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: Response, InformationalResponse, ): - event = cast(Union[Request, Response, InformationalResponse], event) + event = cast(Request | Response | InformationalResponse, event) self.their_http_version = event.http_version # Keep alive handling @@ -303,7 +296,7 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: # this is not supposed to happen. In any case, if it does happen, we # ignore it. if type(event) in (Request, Response) and not _keep_alive( - cast(Union[Request, Response], event) + cast(Request | Response, event) ): self._cstate.process_keep_alive_disabled() @@ -319,30 +312,29 @@ def _process_event(self, role: Type[Sentinel], event: Event) -> None: def _get_io_object( self, - role: Type[Sentinel], - event: Optional[Event], - io_dict: Union[ReadersType, WritersType], - ) -> Optional[Callable[..., Any]]: + role: type[Sentinel], + event: Event | None, + io_dict: ReadersType | WritersType, + ) -> Callable[..., Any] | None: # event may be None; it's only used when entering SEND_BODY state = self._cstate.states[role] if state is SEND_BODY: # Special case: the io_dict has a dict of reader/writer factories # that depend on the request/response framing. framing_type, args = _body_framing( - cast(bytes, self._request_method), cast(Union[Request, Response], event) + cast(bytes, self._request_method), cast(Request | Response, event) ) return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index] - else: - # General case: the io_dict just has the appropriate reader/writer - # for this state - return io_dict.get((role, state)) # type: ignore[return-value] + # General case: the io_dict just has the appropriate reader/writer + # for this state + return io_dict.get((role, state)) # type: ignore[return-value] # This must be called after any action that might have caused # self._cstate.states to change. def _respond_to_state_changes( self, - old_states: Dict[Type[Sentinel], Type[Sentinel]], - event: Optional[Event] = None, + old_states: dict[type[Sentinel], type[Sentinel]], + event: Event | None = None, ) -> None: # Update reader/writer if self.our_state != old_states[self.our_role]: @@ -351,7 +343,7 @@ def _respond_to_state_changes( self._reader = self._get_io_object(self.their_role, event, READERS) @property - def trailing_data(self) -> Tuple[bytes, bool]: + def trailing_data(self) -> tuple[bytes, bool]: """Data that has been received, but not yet processed, represented as a tuple with two elements, where the first is a byte-string containing the unprocessed data itself, and the second is a bool that is True if @@ -409,7 +401,7 @@ def receive_data(self, data: bytes) -> None: def _extract_next_receive_event( self, - ) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + ) -> Event | type[NEED_DATA] | type[PAUSED]: state = self.their_state # We don't pause immediately when they enter DONE, because even in # DONE state we can still process a ConnectionClosed() event. But @@ -421,21 +413,20 @@ def _extract_next_receive_event( return PAUSED assert self._reader is not None event = self._reader(self._receive_buffer) - if event is None: - if not self._receive_buffer and self._receive_buffer_closed: - # In some unusual cases (basically just HTTP/1.0 bodies), EOF - # triggers an actual protocol event; in that case, we want to - # return that event, and then the state will change and we'll - # get called again to generate the actual ConnectionClosed(). - if hasattr(self._reader, "read_eof"): - event = self._reader.read_eof() - else: - event = ConnectionClosed() + if event is None and (not self._receive_buffer and self._receive_buffer_closed): + # In some unusual cases (basically just HTTP/1.0 bodies), EOF + # triggers an actual protocol event; in that case, we want to + # return that event, and then the state will change and we'll + # get called again to generate the actual ConnectionClosed(). + if hasattr(self._reader, "read_eof"): + event = self._reader.read_eof() + else: + event = ConnectionClosed() if event is None: event = NEED_DATA return event # type: ignore[no-any-return] - def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + def next_event(self) -> Event | type[NEED_DATA] | type[PAUSED]: """Parse the next event out of our receive buffer, update our internal state, and return it. @@ -501,20 +492,17 @@ def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: raise @overload - def send(self, event: ConnectionClosed) -> None: - ... + def send(self, event: ConnectionClosed) -> None: ... @overload def send( - self, event: Union[Request, InformationalResponse, Response, Data, EndOfMessage] - ) -> bytes: - ... + self, event: Request | InformationalResponse | Response | Data | EndOfMessage + ) -> bytes: ... @overload - def send(self, event: Event) -> Optional[bytes]: - ... + def send(self, event: Event) -> bytes | None: ... - def send(self, event: Event) -> Optional[bytes]: + def send(self, event: Event) -> bytes | None: """Convert a high-level event into bytes that can be sent to the peer, while updating our internal state machine. @@ -538,10 +526,9 @@ def send(self, event: Event) -> Optional[bytes]: data_list = self.send_with_data_passthrough(event) if data_list is None: return None - else: - return b"".join(data_list) + return b"".join(data_list) - def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: + def send_with_data_passthrough(self, event: Event) -> list[bytes] | None: """Identical to :meth:`send`, except that in situations where :meth:`send` returns a single :term:`bytes-like object`, this instead returns a list of them -- and when sending a :class:`Data` event, this @@ -563,13 +550,12 @@ def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: self._process_event(self.our_role, event) if type(event) is ConnectionClosed: return None - else: - # In any situation where writer is None, process_event should - # have raised ProtocolError - assert writer is not None - data_list: List[bytes] = [] - writer(event, data_list.append) - return data_list + # In any situation where writer is None, process_event should + # have raised ProtocolError + assert writer is not None + data_list: list[bytes] = [] + writer(event, data_list.append) + return data_list except: self._process_error(self.our_role) raise diff --git a/h11/_events.py b/h11/_events.py index ca1c3ad..09fa115 100644 --- a/h11/_events.py +++ b/h11/_events.py @@ -8,28 +8,27 @@ import re from abc import ABC from dataclasses import dataclass -from typing import List, Tuple, Union from ._abnf import method, request_target from ._headers import Headers, normalize_and_validate -from ._util import bytesify, LocalProtocolError, validate +from ._util import LocalProtocolError, bytesify, validate # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = [ + "ConnectionClosed", + "Data", + "EndOfMessage", "Event", - "Request", "InformationalResponse", + "Request", "Response", - "Data", - "EndOfMessage", - "ConnectionClosed", ] method_re = re.compile(method.encode("ascii")) request_target_re = re.compile(request_target.encode("ascii")) -class Event(ABC): +class Event(ABC): # noqa: B024 """ Base class for h11 events. """ @@ -72,7 +71,7 @@ class Request(Event): """ - __slots__ = ("method", "headers", "target", "http_version") + __slots__ = ("headers", "http_version", "method", "target") method: bytes headers: Headers @@ -82,10 +81,10 @@ class Request(Event): def __init__( self, *, - method: Union[bytes, str], - headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], - target: Union[bytes, str], - http_version: Union[bytes, str] = b"1.1", + method: bytes | str, + headers: Headers | list[tuple[bytes, bytes]] | list[tuple[str, str]], + target: bytes | str, + http_version: bytes | str = b"1.1", _parsed: bool = False, ) -> None: super().__init__() @@ -110,9 +109,10 @@ def __init__( # Host header field with an invalid field-value." # -- https://tools.ietf.org/html/rfc7230#section-5.4 host_count = 0 - for name, value in self.headers: + for name, _ in self.headers: if name == b"host": host_count += 1 + if self.http_version == b"1.1" and host_count == 0: raise LocalProtocolError("Missing mandatory Host: header") if host_count > 1: @@ -137,10 +137,10 @@ class _ResponseBase(Event): def __init__( self, *, - headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], + headers: Headers | list[tuple[bytes, bytes]] | list[tuple[str, str]], status_code: int, - http_version: Union[bytes, str] = b"1.1", - reason: Union[bytes, str] = b"", + http_version: bytes | str = b"1.1", + reason: bytes | str = b"", _parsed: bool = False, ) -> None: super().__init__() @@ -207,7 +207,7 @@ def __post_init__(self) -> None: if not (100 <= self.status_code < 200): raise LocalProtocolError( "InformationalResponse status_code should be in range " - "[100, 200), not {}".format(self.status_code) + f"[100, 200), not {self.status_code}" ) # This is an unhashable type. @@ -247,9 +247,8 @@ class Response(_ResponseBase): def __post_init__(self) -> None: if not (200 <= self.status_code < 1000): raise LocalProtocolError( - "Response status_code should be in range [200, 1000), not {}".format( - self.status_code - ) + "Response status_code should be in range [200, 1000), " + f"not {self.status_code}" ) # This is an unhashable type. @@ -290,7 +289,7 @@ class Data(Event): """ - __slots__ = ("data", "chunk_start", "chunk_end") + __slots__ = ("chunk_end", "chunk_start", "data") data: bytes chunk_start: bool @@ -304,7 +303,7 @@ def __init__( object.__setattr__(self, "chunk_end", chunk_end) # This is an unhashable type. - __hash__ = None # type: ignore + __hash__ = None # XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that @@ -337,9 +336,10 @@ class EndOfMessage(Event): def __init__( self, *, - headers: Union[ - Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None - ] = None, + headers: Headers + | list[tuple[bytes, bytes]] + | list[tuple[str, str]] + | None = None, _parsed: bool = False, ) -> None: super().__init__() @@ -365,5 +365,3 @@ class ConnectionClosed(Event): No fields. """ - - pass diff --git a/h11/_headers.py b/h11/_headers.py index 31da3e2..2d6e4c6 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -1,8 +1,11 @@ +# ruff: noqa: PLW2901 + import re -from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union +from collections.abc import Sequence +from typing import TYPE_CHECKING, overload from ._abnf import field_name, field_value -from ._util import bytesify, LocalProtocolError, validate +from ._util import LocalProtocolError, bytesify, validate if TYPE_CHECKING: from ._events import Request @@ -10,7 +13,7 @@ try: from typing import Literal except ImportError: - from typing_extensions import Literal # type: ignore + from typing import Literal # type: ignore CONTENT_LENGTH_MAX_DIGITS = 20 # allow up to 1 billion TB - 1 @@ -74,7 +77,7 @@ _field_value_re = re.compile(field_value.encode("ascii")) -class Headers(Sequence[Tuple[bytes, bytes]]): +class Headers(Sequence[tuple[bytes, bytes]]): """ A list-like interface that allows iterating over headers as byte-pairs of (lowercased-name, value). @@ -99,9 +102,9 @@ class Headers(Sequence[Tuple[bytes, bytes]]): ] """ - __slots__ = "_full_items" + __slots__ = ("_full_items",) - def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None: + def __init__(self, full_items: list[tuple[bytes, bytes, bytes]]) -> None: self._full_items = full_items def __bool__(self) -> bool: @@ -110,52 +113,54 @@ def __bool__(self) -> bool: def __eq__(self, other: object) -> bool: return list(self) == list(other) # type: ignore + def __hash__(self) -> int: + return hash(tuple(self._full_items)) + def __len__(self) -> int: return len(self._full_items) def __repr__(self) -> str: - return "" % repr(list(self)) + return f"" - def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override] + def __getitem__(self, idx: int) -> tuple[bytes, bytes]: # type: ignore[override] _, name, value = self._full_items[idx] return (name, value) - def raw_items(self) -> List[Tuple[bytes, bytes]]: + def raw_items(self) -> list[tuple[bytes, bytes]]: return [(raw_name, value) for raw_name, _, value in self._full_items] -HeaderTypes = Union[ - List[Tuple[bytes, bytes]], - List[Tuple[bytes, str]], - List[Tuple[str, bytes]], - List[Tuple[str, str]], -] +HeaderTypes = ( + list[tuple[bytes, bytes]] + | list[tuple[bytes, str]] + | list[tuple[str, bytes]] + | list[tuple[str, str]] +) @overload -def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: - ... +def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: ... @overload -def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers: - ... +def normalize_and_validate( + headers: HeaderTypes, _parsed: Literal[False] +) -> Headers: ... @overload def normalize_and_validate( - headers: Union[Headers, HeaderTypes], _parsed: bool = False -) -> Headers: - ... + headers: Headers | HeaderTypes, _parsed: bool = False +) -> Headers: ... def normalize_and_validate( - headers: Union[Headers, HeaderTypes], _parsed: bool = False + headers: Headers | HeaderTypes, _parsed: bool = False ) -> Headers: new_headers = [] seen_content_length = None saw_transfer_encoding = False - for name, value in headers: + for name, value in headers: # pyrefly: ignore[bad-assignment] # For headers coming out of the parser, we can safely skip some steps, # because it always returns bytes and has already run these regexes # over the data: @@ -206,7 +211,7 @@ def normalize_and_validate( return Headers(new_headers) -def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: +def get_comma_header(headers: Headers, name: bytes) -> list[bytes]: # Should only be used for headers whose value is a list of # comma-separated, case-insensitive values. # @@ -242,7 +247,7 @@ def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: # Expect: the only legal value is the literal string # "100-continue". Splitting on commas is harmless. Case insensitive. # - out: List[bytes] = [] + out: list[bytes] = [] for _, found_name, found_raw_value in headers._full_items: if found_name == name: found_raw_value = found_raw_value.lower() @@ -253,7 +258,7 @@ def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: return out -def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers: +def set_comma_header(headers: Headers, name: bytes, new_values: list[bytes]) -> Headers: # The header name `name` is expected to be lower-case bytes. # # Note that when we store the header we use title casing for the header @@ -263,12 +268,12 @@ def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> # here given the cases where we're using `set_comma_header`... # # Connection, Content-Length, Transfer-Encoding. - new_headers: List[Tuple[bytes, bytes]] = [] + new_headers: list[tuple[bytes, bytes]] = [] for found_raw_name, found_name, found_raw_value in headers._full_items: if found_name != name: new_headers.append((found_raw_name, found_raw_value)) - for new_value in new_values: - new_headers.append((name.title(), new_value)) + + new_headers.extend((name.title(), new_value) for new_value in new_values) return normalize_and_validate(new_headers) diff --git a/h11/_readers.py b/h11/_readers.py index 576804c..37d71da 100644 --- a/h11/_readers.py +++ b/h11/_readers.py @@ -17,7 +17,8 @@ # - or, for body readers, a dict of per-framing reader factories import re -from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union +from collections.abc import Callable, Iterable +from typing import Any, NoReturn from ._abnf import chunk_header, header_field, request_line, status_line from ._events import Data, EndOfMessage, InformationalResponse, Request, Response @@ -40,9 +41,9 @@ obs_fold_re = re.compile(rb"[ \t]+") -def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: +def _obsolete_line_fold(lines: Iterable[bytes | bytearray]) -> Iterable[bytes]: it = iter(lines) - last: Optional[bytes] = None + last: bytearray | None = None for line in it: match = obs_fold_re.match(line) if match: @@ -55,15 +56,15 @@ def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: last += line[match.end() :] else: if last is not None: - yield last - last = line + yield bytes(last) + last = bytearray(line) if last is not None: - yield last + yield bytes(last) def _decode_header_lines( - lines: Iterable[bytes], -) -> Iterable[Tuple[bytes, bytes]]: + lines: Iterable[bytes | bytearray], +) -> Iterable[tuple[bytes, bytes]]: for line in _obsolete_line_fold(lines): matches = validate(header_field_re, line, "illegal header line: {!r}", line) yield (matches["field_name"], matches["field_value"]) @@ -72,7 +73,7 @@ def _decode_header_lines( request_line_re = re.compile(request_line.encode("ascii")) -def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: +def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Request | None: # noqa: N802 lines = buf.maybe_extract_lines() if lines is None: if buf.is_next_line_obviously_invalid_request_line(): @@ -91,9 +92,9 @@ def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: status_line_re = re.compile(status_line.encode("ascii")) -def maybe_read_from_SEND_RESPONSE_server( +def maybe_read_from_SEND_RESPONSE_server( # noqa: N802 buf: ReceiveBuffer, -) -> Union[InformationalResponse, Response, None]: +) -> InformationalResponse | Response | None: lines = buf.maybe_extract_lines() if lines is None: if buf.is_next_line_obviously_invalid_request_line(): @@ -107,7 +108,7 @@ def maybe_read_from_SEND_RESPONSE_server( ) reason = b"" if matches["reason"] is None else matches["reason"] status_code = int(matches["status_code"]) - class_: Union[Type[InformationalResponse], Type[Response]] = ( + class_: type[InformationalResponse | Response] = ( InformationalResponse if status_code < 200 else Response ) return class_( @@ -124,21 +125,20 @@ def __init__(self, length: int) -> None: self._length = length self._remaining = length - def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + def __call__(self, buf: ReceiveBuffer) -> Data | EndOfMessage | None: if self._remaining == 0: return EndOfMessage() data = buf.maybe_extract_at_most(self._remaining) if data is None: return None self._remaining -= len(data) - return Data(data=data) + return Data(data=bytes(data)) def read_eof(self) -> NoReturn: raise RemoteProtocolError( "peer closed connection without sending complete message body " - "(received {} bytes, expected {})".format( - self._length - self._remaining, self._length - ) + f"(received {self._length - self._remaining} bytes, " + f"expected {self._length})" ) @@ -153,7 +153,7 @@ def __init__(self) -> None: self._bytes_to_discard = b"" self._reading_trailer = False - def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + def __call__(self, buf: ReceiveBuffer) -> Data | EndOfMessage | None: if self._reading_trailer: lines = buf.maybe_extract_lines() if lines is None: @@ -165,7 +165,8 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: return None if data != self._bytes_to_discard[: len(data)]: raise LocalProtocolError( - f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})" + f"malformed chunk footer: {data!r} " + "(expected {self._bytes_to_discard!r})" ) self._bytes_to_discard = self._bytes_to_discard[len(data) :] if self._bytes_to_discard: @@ -201,7 +202,7 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: chunk_end = True else: chunk_end = False - return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end) + return Data(data=bytes(data), chunk_start=chunk_start, chunk_end=chunk_end) def read_eof(self) -> NoReturn: raise RemoteProtocolError( @@ -211,11 +212,11 @@ def read_eof(self) -> NoReturn: class Http10Reader: - def __call__(self, buf: ReceiveBuffer) -> Optional[Data]: + def __call__(self, buf: ReceiveBuffer) -> Data | None: data = buf.maybe_extract_at_most(999999999) if data is None: return None - return Data(data=data) + return Data(data=bytes(data)) def read_eof(self) -> EndOfMessage: return EndOfMessage() @@ -224,12 +225,11 @@ def read_eof(self) -> EndOfMessage: def expect_nothing(buf: ReceiveBuffer) -> None: if buf: raise LocalProtocolError("Got data when expecting EOF") - return None -ReadersType = Dict[ - Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]], - Union[Callable[..., Any], Dict[str, Callable[..., Any]]], +ReadersType = dict[ + type[Sentinel] | tuple[type[Sentinel], type[Sentinel]], + Callable[..., Any] | dict[str, Callable[..., Any]], ] READERS: ReadersType = { diff --git a/h11/_receivebuffer.py b/h11/_receivebuffer.py index e5c4e08..20b8586 100644 --- a/h11/_receivebuffer.py +++ b/h11/_receivebuffer.py @@ -1,6 +1,9 @@ import re -import sys -from typing import List, Optional, Union + +try: + from typing import Self # pyrefly: ignore +except ImportError: + from typing_extensions import Self __all__ = ["ReceiveBuffer"] @@ -50,7 +53,7 @@ def __init__(self) -> None: self._next_line_search = 0 self._multiple_lines_search = 0 - def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer": + def __iadd__(self, byteslike: bytes | bytearray) -> Self: self._data += byteslike return self @@ -74,7 +77,7 @@ def _extract(self, count: int) -> bytearray: return out - def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: + def maybe_extract_at_most(self, count: int) -> bytearray | None: """ Extract a fixed number of bytes from the buffer. """ @@ -84,7 +87,7 @@ def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: return self._extract(count) - def maybe_extract_next_line(self) -> Optional[bytearray]: + def maybe_extract_next_line(self) -> bytearray | None: """ Extract the first line, if it is completed in the buffer. """ @@ -101,7 +104,7 @@ def maybe_extract_next_line(self) -> Optional[bytearray]: return self._extract(idx) - def maybe_extract_lines(self) -> Optional[List[bytearray]]: + def maybe_extract_lines(self) -> list[bytearray] | None: """ Extract everything up to the first blank line, and return a list of lines. """ diff --git a/h11/_state.py b/h11/_state.py index 3ad444b..f80dc89 100644 --- a/h11/_state.py +++ b/h11/_state.py @@ -110,24 +110,35 @@ # tables. But it can't automatically read the transitions that are written # directly in Python code. So if you touch those, you need to also update the # script to keep it in sync! -from typing import cast, Dict, Optional, Set, Tuple, Type, Union -from ._events import * +# ruff: noqa: N801 + +from typing import cast + +from ._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) from ._util import LocalProtocolError, Sentinel # Everything in __all__ gets re-exported as part of the h11 public API. __all__ = [ "CLIENT", - "SERVER", - "IDLE", - "SEND_RESPONSE", - "SEND_BODY", - "DONE", - "MUST_CLOSE", "CLOSED", + "DONE", + "ERROR", + "IDLE", "MIGHT_SWITCH_PROTOCOL", + "MUST_CLOSE", + "SEND_BODY", + "SEND_RESPONSE", + "SERVER", "SWITCHED_PROTOCOL", - "ERROR", ] @@ -185,11 +196,11 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): pass -EventTransitionType = Dict[ - Type[Sentinel], - Dict[ - Type[Sentinel], - Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]], +EventTransitionType = dict[ + type[Sentinel], + dict[ + type[Sentinel], + dict[type[Event] | tuple[type[Event], type[Sentinel]], type[Sentinel]], ], ] @@ -226,8 +237,8 @@ class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): }, } -StateTransitionType = Dict[ - Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]] +StateTransitionType = dict[ + tuple[type[Sentinel], type[Sentinel]], dict[type[Sentinel], type[Sentinel]] ] # NB: there are also some special-case state-triggered transitions hard-coded @@ -256,11 +267,11 @@ def __init__(self) -> None: # This is a subset of {UPGRADE, CONNECT}, containing the proposals # made by the client for switching protocols. - self.pending_switch_proposals: Set[Type[Sentinel]] = set() + self.pending_switch_proposals: set[type[Sentinel]] = set() - self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} + self.states: dict[type[Sentinel], type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} - def process_error(self, role: Type[Sentinel]) -> None: + def process_error(self, role: type[Sentinel]) -> None: self.states[role] = ERROR self._fire_state_triggered_transitions() @@ -268,17 +279,17 @@ def process_keep_alive_disabled(self) -> None: self.keep_alive = False self._fire_state_triggered_transitions() - def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None: + def process_client_switch_proposal(self, switch_event: type[Sentinel]) -> None: self.pending_switch_proposals.add(switch_event) self._fire_state_triggered_transitions() def process_event( self, - role: Type[Sentinel], - event_type: Type[Event], - server_switch_event: Optional[Type[Sentinel]] = None, + role: type[Sentinel], + event_type: type[Event], + server_switch_event: type[Sentinel] | None = None, ) -> None: - _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type + _event_type: type[Event] | tuple[type[Event], type[Sentinel]] = event_type if server_switch_event is not None: assert role is SERVER if server_switch_event not in self.pending_switch_proposals: @@ -298,18 +309,17 @@ def process_event( def _fire_event_triggered_transitions( self, - role: Type[Sentinel], - event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], + role: type[Sentinel], + event_type: type[Event] | tuple[type[Event], type[Sentinel]], ) -> None: state = self.states[role] try: new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type] except KeyError: - event_type = cast(Type[Event], event_type) + event_type = cast(type[Event], event_type) raise LocalProtocolError( - "can't handle event type {} when role={} and state={}".format( - event_type.__name__, role, self.states[role] - ) + f"can't handle event type {event_type.__name__} when role={role} " + "and state={self.states[role]}" ) from None self.states[role] = new_state @@ -331,13 +341,14 @@ def _fire_state_triggered_transitions(self) -> None: # they close the connection, or else the server will deny the # request, in which case the client will go back to DONE and then # from there to MUST_CLOSE. - if self.pending_switch_proposals: - if self.states[CLIENT] is DONE: - self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL - - if not self.pending_switch_proposals: - if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL: - self.states[CLIENT] = DONE + if self.pending_switch_proposals and self.states[CLIENT] is DONE: + self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL + + if ( + not self.pending_switch_proposals + and self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL + ): + self.states[CLIENT] = DONE if not self.keep_alive: for role in (CLIENT, SERVER): diff --git a/h11/_util.py b/h11/_util.py index 6718445..85d272a 100644 --- a/h11/_util.py +++ b/h11/_util.py @@ -1,11 +1,12 @@ -from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union +from re import Pattern +from typing import Any, NoReturn, TypeVar __all__ = [ - "ProtocolError", "LocalProtocolError", + "ProtocolError", "RemoteProtocolError", - "validate", "bytesify", + "validate", ] @@ -82,8 +83,11 @@ class RemoteProtocolError(ProtocolError): def validate( - regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any -) -> Dict[str, bytes]: + regex: Pattern[bytes], + data: bytes | bytearray, + msg: str = "malformed data", + *format_args: Any, +) -> dict[str, bytes]: match = regex.fullmatch(data) if not match: if format_args: @@ -106,25 +110,25 @@ def validate( class Sentinel(type): def __new__( - cls: Type[_T_Sentinel], + cls: type[_T_Sentinel], name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - **kwds: Any + bases: tuple[type, ...], + namespace: dict[str, Any], + **kwds: Any, ) -> _T_Sentinel: assert bases == (Sentinel,) v = super().__new__(cls, name, bases, namespace, **kwds) v.__class__ = v # type: ignore return v - def __repr__(self) -> str: - return self.__name__ + def __repr__(cls) -> str: + return cls.__name__ # Used for methods, request targets, HTTP versions, header names, and header # values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always # returns bytes. -def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes: +def bytesify(s: bytes | bytearray | memoryview | int | str) -> bytes: # Fast-path: if type(s) is bytes: return s diff --git a/h11/_writers.py b/h11/_writers.py index 939cdb9..ac28f54 100644 --- a/h11/_writers.py +++ b/h11/_writers.py @@ -7,7 +7,8 @@ # - a writer # - or, for body writers, a dict of framin-dependent writer factories -from typing import Any, Callable, Dict, List, Tuple, Type, Union +from collections.abc import Callable +from typing import Any from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response from ._headers import Headers @@ -42,7 +43,7 @@ def write_request(request: Request, write: Writer) -> None: # Shared between InformationalResponse and Response def write_any_response( - response: Union[InformationalResponse, Response], write: Writer + response: InformationalResponse | Response, write: Writer ) -> None: if response.http_version != b"1.1": raise LocalProtocolError("I only send HTTP/1.1") @@ -66,7 +67,7 @@ def __call__(self, event: Event, write: Writer) -> None: elif type(event) is EndOfMessage: self.send_eom(event.headers, write) else: # pragma: no cover - assert False + raise AssertionError def send_data(self, data: bytes, write: Writer) -> None: pass @@ -91,7 +92,7 @@ def send_data(self, data: bytes, write: Writer) -> None: raise LocalProtocolError("Too much data for declared Content-Length") write(data) - def send_eom(self, headers: Headers, write: Writer) -> None: + def send_eom(self, headers: Headers, write: Writer) -> None: # noqa: ARG002 if self._length != 0: raise LocalProtocolError("Too little data for declared Content-Length") if headers: @@ -117,20 +118,18 @@ class Http10Writer(BodyWriter): def send_data(self, data: bytes, write: Writer) -> None: write(data) - def send_eom(self, headers: Headers, write: Writer) -> None: + def send_eom(self, headers: Headers, write: Writer) -> None: # noqa: ARG002 if headers: raise LocalProtocolError("can't send trailers to HTTP/1.0 client") # no need to close the socket ourselves, that will be taken care of by # Connection: close machinery -WritersType = Dict[ - Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]], - Union[ - Dict[str, Type[BodyWriter]], - Callable[[Union[InformationalResponse, Response], Writer], None], - Callable[[Request, Writer], None], - ], +WritersType = dict[ + tuple[type[Sentinel], type[Sentinel]] | type[Sentinel], + dict[str, type[BodyWriter]] + | Callable[[InformationalResponse | Response, Writer], None] + | Callable[[Request, Writer], None], ] WRITERS: WritersType = { diff --git a/h11/tests/helpers.py b/h11/tests/helpers.py index 571be44..04c80ac 100644 --- a/h11/tests/helpers.py +++ b/h11/tests/helpers.py @@ -1,25 +1,21 @@ -from typing import cast, List, Type, Union, ValuesView +from collections.abc import ValuesView +from typing import cast -from .._connection import Connection, NEED_DATA, PAUSED +from .._connection import NEED_DATA, PAUSED, Connection from .._events import ( ConnectionClosed, Data, - EndOfMessage, Event, - InformationalResponse, - Request, - Response, ) -from .._state import CLIENT, CLOSED, DONE, MUST_CLOSE, SERVER -from .._util import Sentinel +from .._state import CLIENT, SERVER try: from typing import Literal except ImportError: - from typing_extensions import Literal # type: ignore + from typing import Literal # type: ignore -def get_all_events(conn: Connection) -> List[Event]: +def get_all_events(conn: Connection) -> list[Event]: got_events = [] while True: event = conn.next_event() @@ -32,23 +28,23 @@ def get_all_events(conn: Connection) -> List[Event]: return got_events -def receive_and_get(conn: Connection, data: bytes) -> List[Event]: +def receive_and_get(conn: Connection, data: bytes) -> list[Event]: conn.receive_data(data) return get_all_events(conn) # Merges adjacent Data events, converts payloads to bytestrings, and removes # chunk boundaries. -def normalize_data_events(in_events: List[Event]) -> List[Event]: - out_events: List[Event] = [] +def normalize_data_events(in_events: list[Event]) -> list[Event]: + out_events: list[Event] = [] for event in in_events: if type(event) is Data: - event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False) + event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False) # noqa: PLW2901 if out_events and type(out_events[-1]) is type(event) is Data: out_events[-1] = Data( - data=out_events[-1].data + event.data, - chunk_start=out_events[-1].chunk_start, - chunk_end=out_events[-1].chunk_end, + data=out_events[-1].data + event.data, # type: ignore + chunk_start=out_events[-1].chunk_start, # type: ignore + chunk_end=out_events[-1].chunk_end, # type: ignore ) else: out_events.append(event) @@ -71,9 +67,9 @@ def conns(self) -> ValuesView[Connection]: # expect="match" if expect=send_events; expect=[...] to say what expected def send( self, - role: Type[Sentinel], - send_events: Union[List[Event], Event], - expect: Union[List[Event], Event, Literal["match"]] = "match", + role: type[CLIENT] | type[SERVER], + send_events: list[Event] | Event, + expect: list[Event] | Event | Literal["match"] = "match", ) -> bytes: if not isinstance(send_events, list): send_events = [send_events] diff --git a/h11/tests/test_against_stdlib_http.py b/h11/tests/test_against_stdlib_http.py index 3f66a10..5c7e438 100644 --- a/h11/tests/test_against_stdlib_http.py +++ b/h11/tests/test_against_stdlib_http.py @@ -1,11 +1,13 @@ +# ruff: noqa: T201, S310 + import json import os.path import socket import socketserver import threading +from collections.abc import Callable, Generator from contextlib import closing, contextmanager from http.server import SimpleHTTPRequestHandler -from typing import Callable, Generator from urllib.request import urlopen import h11 @@ -33,40 +35,42 @@ def socket_server( class SingleMindedRequestHandler(SimpleHTTPRequestHandler): - def translate_path(self, path: str) -> str: + def translate_path(self, path: str) -> str: # noqa: ARG002 return test_file_path def test_h11_as_client() -> None: - with socket_server(SingleMindedRequestHandler) as httpd: - with closing(socket.create_connection(httpd.server_address)) as s: # type: ignore[arg-type] - c = h11.Connection(h11.CLIENT) - - s.sendall( - c.send( - h11.Request( - method="GET", target="/foo", headers=[("Host", "localhost")] - ) + with ( + socket_server(SingleMindedRequestHandler) as httpd, + closing(socket.create_connection(httpd.server_address)) as s, # type: ignore + ): + c = h11.Connection(h11.CLIENT) + + s.sendall( + c.send( + h11.Request( + method="GET", target="/foo", headers=[("Host", "localhost")] ) ) - s.sendall(c.send(h11.EndOfMessage())) + ) + s.sendall(c.send(h11.EndOfMessage())) - data = bytearray() - while True: - event = c.next_event() - print(event) - if event is h11.NEED_DATA: - # Use a small read buffer to make things more challenging - # and exercise more paths :-) - c.receive_data(s.recv(10)) - continue - if type(event) is h11.Response: - assert event.status_code == 200 - if type(event) is h11.Data: - data += event.data - if type(event) is h11.EndOfMessage: - break - assert bytes(data) == test_file_data + data = bytearray() + while True: + event = c.next_event() + print(event) + if event is h11.NEED_DATA: + # Use a small read buffer to make things more challenging + # and exercise more paths :-) + c.receive_data(s.recv(10)) + continue + if type(event) is h11.Response: + assert event.status_code == 200 + if type(event) is h11.Data: + data += event.data + if type(event) is h11.EndOfMessage: + break + assert bytes(data) == test_file_data class H11RequestHandler(socketserver.BaseRequestHandler): @@ -103,7 +107,7 @@ def handle(self) -> None: def test_h11_as_server() -> None: with socket_server(H11RequestHandler) as httpd: - host, port = httpd.server_address + host, port = httpd.server_address # type: ignore url = f"http://{host}:{port}/some-path" # type: ignore[str-bytes-safe] with closing(urlopen(url)) as f: assert f.getcode() == 200 diff --git a/h11/tests/test_connection.py b/h11/tests/test_connection.py index 01260dc..0171218 100644 --- a/h11/tests/test_connection.py +++ b/h11/tests/test_connection.py @@ -1,12 +1,15 @@ -from typing import Any, cast, Dict, List, Optional, Tuple, Type +# ruff: noqa: N802, T201 + +from typing import Any, cast import pytest -from .._connection import _body_framing, _keep_alive, Connection, NEED_DATA, PAUSED +from .._connection import NEED_DATA, PAUSED, Connection, _body_framing, _keep_alive from .._events import ( ConnectionClosed, Data, EndOfMessage, + Event, InformationalResponse, Request, Response, @@ -58,27 +61,27 @@ def test__keep_alive() -> None: def test__body_framing() -> None: - def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]: + def headers(cl: int | None, te: bool) -> list[tuple[str, str]]: headers = [] if cl is not None: headers.append(("Content-Length", str(cl))) if te: headers.append(("Transfer-Encoding", "chunked")) - return headers + return headers # type: ignore def resp( - status_code: int = 200, cl: Optional[int] = None, te: bool = False + status_code: int = 200, cl: int | None = None, te: bool = False ) -> Response: return Response(status_code=status_code, headers=headers(cl, te)) - def req(cl: Optional[int] = None, te: bool = False) -> Request: + def req(cl: int | None = None, te: bool = False) -> Request: h = headers(cl, te) h += [("Host", "example.com")] return Request(method="GET", target="/", headers=h) # Special cases where the headers are ignored: for kwargs in [{}, {"cl": 100}, {"te": True}, {"cl": 100, "te": True}]: - kwargs = cast(Dict[str, Any], kwargs) + kwargs = cast(dict[str, Any], kwargs) for meth, r in [ (b"HEAD", resp(**kwargs)), (b"GET", resp(status_code=204, **kwargs)), @@ -88,7 +91,7 @@ def req(cl: Optional[int] = None, te: bool = False) -> Request: # Transfer-encoding for kwargs in [{"te": True}, {"cl": 100, "te": True}]: - kwargs = cast(Dict[str, Any], kwargs) + kwargs = cast(dict[str, Any], kwargs) for meth, r in [(None, req(**kwargs)), (b"GET", resp(**kwargs))]: # type: ignore assert _body_framing(meth, r) == ("chunked", ()) @@ -102,7 +105,7 @@ def req(cl: Optional[int] = None, te: bool = False) -> Request: def test_Connection_basics_and_content_length() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 Connection("CLIENT") # type: ignore p = ConnectionPair() @@ -120,7 +123,7 @@ def test_Connection_basics_and_content_length() -> None: ), ) assert data == ( - b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 10\r\n\r\n" + b"GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\n" ) for conn in p.conns: @@ -203,10 +206,7 @@ def test_chunk_boundaries() -> None: conn = Connection(our_role=SERVER) request = ( - b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n" - b"\r\n" + b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n" ) conn.receive_data(request) assert conn.next_event() == Request( @@ -303,7 +303,7 @@ def test_automatic_transfer_encoding_in_response() -> None: # because if both are set then Transfer-Encoding wins [("Transfer-Encoding", "chunked"), ("Content-Length", "100")], ]: - user_headers = cast(List[Tuple[str, str]], user_headers) + user_headers = cast(list[tuple[str, str]], user_headers) p = ConnectionPair() p.send( CLIENT, @@ -415,7 +415,7 @@ def test_max_incomplete_event_size_countermeasure() -> None: c = Connection(SERVER) c.receive_data(b"GET / HTTP/1.0\r\nEndless: ") assert c.next_event() is NEED_DATA - with pytest.raises(RemoteProtocolError): + with pytest.raises(RemoteProtocolError): # noqa: PT012 while True: c.receive_data(b"a" * 1024) c.next_event() @@ -635,7 +635,7 @@ def test_protocol_switch() -> None: def setup() -> ConnectionPair: p = ConnectionPair() - p.send(CLIENT, req) + p.send(CLIENT, req) # noqa: B023 # No switch-related state change stuff yet; the client has to # finish the request before that kicks in for conn in p.conns: @@ -720,14 +720,14 @@ def test_close_simple() -> None: # Just immediately closing a new connection without anything having # happened yet. for who_shot_first, who_shot_second in [(CLIENT, SERVER), (SERVER, CLIENT)]: - + # all these noqas are fine bc the function is immediately called def setup() -> ConnectionPair: p = ConnectionPair() - p.send(who_shot_first, ConnectionClosed()) + p.send(who_shot_first, ConnectionClosed()) # noqa: B023 for conn in p.conns: assert conn.states == { - who_shot_first: CLOSED, - who_shot_second: MUST_CLOSE, + who_shot_first: CLOSED, # noqa: B023 + who_shot_second: MUST_CLOSE, # noqa: B023 } return p @@ -758,11 +758,11 @@ def setup() -> ConnectionPair: def test_close_different_states() -> None: - req = [ + req: list[Event] = [ Request(method="GET", target="/foo", headers=[("Host", "a")]), EndOfMessage(), ] - resp = [ + resp: list[Event] = [ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), EndOfMessage(), ] @@ -873,8 +873,8 @@ def __len__(self) -> int: placeholder = SendfilePlaceholder() def setup( - header: Tuple[str, str], http_version: str - ) -> Tuple[Connection, Optional[List[bytes]]]: + header: tuple[str, str], http_version: str + ) -> tuple[Connection, list[bytes] | None]: c = Connection(SERVER) receive_and_get( c, f"GET / HTTP/{http_version}\r\nHost: a\r\n\r\n".encode("ascii") @@ -924,7 +924,7 @@ def test_errors() -> None: # After an error sending, you can no longer send # (This is especially important for things like content-length errors, # where there's complex internal state being modified) - def conn(role: Type[Sentinel]) -> Connection: + def conn(role: type[Sentinel]) -> Connection: c = Connection(our_role=role) if role is SERVER: # Put it into the state where it *could* send a response... @@ -948,17 +948,17 @@ def conn(role: Type[Sentinel]) -> Connection: bad = Response(status_code=200, headers=[], http_version="1.0") # type: ignore[assignment] # Make sure 'good' actually is good c = conn(role) - c.send(good) + c.send(good) # pyrefly: ignore[unbound-name] assert c.our_state is not ERROR # Do that again, but this time sending 'bad' first c = conn(role) with pytest.raises(LocalProtocolError): - c.send(bad) + c.send(bad) # pyrefly: ignore[unbound-name] assert c.our_state is ERROR assert c.their_state is not ERROR # Now 'good' is not so good with pytest.raises(LocalProtocolError): - c.send(good) + c.send(good) # pyrefly: ignore[unbound-name] # And check send_failed() too c = conn(role) @@ -1092,7 +1092,7 @@ def setup(method: bytes, http_version: bytes) -> Connection: def test_special_exceptions_for_lost_connection_in_message_body() -> None: c = Connection(SERVER) c.receive_data( - b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 100\r\n\r\n" + b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 100\r\n\r\n" ) assert type(c.next_event()) is Request assert c.next_event() is NEED_DATA @@ -1106,9 +1106,7 @@ def test_special_exceptions_for_lost_connection_in_message_body() -> None: c = Connection(SERVER) c.receive_data( - b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" + b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n" ) assert type(c.next_event()) is Request assert c.next_event() is NEED_DATA diff --git a/h11/tests/test_events.py b/h11/tests/test_events.py index d691545..368e7a8 100644 --- a/h11/tests/test_events.py +++ b/h11/tests/test_events.py @@ -79,14 +79,17 @@ def test_events() -> None: target.append(bad_byte) with pytest.raises(LocalProtocolError): Request( - method="GET", target=target, headers=[("Host", "a")], http_version="1.1" + method="GET", + target=bytes(target), + headers=[("Host", "a")], + http_version="1.1", ) # Request method is validated with pytest.raises(LocalProtocolError): Request( method="GET / HTTP/1.1", - target=target, + target=bytes(target), # type: ignore[unbound-name] headers=[("Host", "a")], http_version="1.1", ) diff --git a/h11/tests/test_headers.py b/h11/tests/test_headers.py index b57274c..f2884d2 100644 --- a/h11/tests/test_headers.py +++ b/h11/tests/test_headers.py @@ -4,7 +4,6 @@ from .._headers import ( get_comma_header, has_expect_100_continue, - Headers, normalize_and_validate, set_comma_header, ) diff --git a/h11/tests/test_helpers.py b/h11/tests/test_helpers.py index 9a30dc6..fa18624 100644 --- a/h11/tests/test_helpers.py +++ b/h11/tests/test_helpers.py @@ -5,7 +5,7 @@ def test_normalize_data_events() -> None: assert normalize_data_events( [ - Data(data=bytearray(b"1")), + Data(data=bytearray(b"1")), # type: ignore[bad-argument-type] Data(data=b"2"), Response(status_code=200, headers=[]), Data(data=b"3"), diff --git a/h11/tests/test_io.py b/h11/tests/test_io.py index 407e044..2849ee4 100644 --- a/h11/tests/test_io.py +++ b/h11/tests/test_io.py @@ -1,4 +1,7 @@ -from typing import Any, Callable, Generator, List +# ruff: noqa: T201, N802 + +from collections.abc import Callable, Generator +from typing import Any import pytest @@ -12,23 +15,23 @@ ) from .._headers import Headers, normalize_and_validate from .._readers import ( - _obsolete_line_fold, + READERS, ChunkedReader, ContentLengthReader, Http10Reader, - READERS, + _obsolete_line_fold, ) from .._receivebuffer import ReceiveBuffer from .._state import CLIENT, IDLE, SEND_RESPONSE, SERVER from .._util import LocalProtocolError from .._writers import ( + WRITERS, ChunkedWriter, ContentLengthWriter, Http10Writer, write_any_response, write_headers, write_request, - WRITERS, ) from .helpers import normalize_data_events @@ -68,7 +71,7 @@ def dowrite(writer: Callable[..., None], obj: Any) -> bytes: - got_list: List[bytes] = [] + got_list: list[bytes] = [] writer(obj, got_list.append) return b"".join(got_list) @@ -188,7 +191,7 @@ def test_readers_unusual() -> None: # 7230 -- this is a bug in the standard that we originally copied...) tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n", + b"HTTP/1.0 200 OK\r\nFoo: a a a a a \r\n\r\n", Response( status_code=200, headers=[("Foo", "a a a a a")], @@ -200,7 +203,7 @@ def test_readers_unusual() -> None: # Empty headers -- also legal tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n", + b"HTTP/1.0 200 OK\r\nFoo:\r\n\r\n", Response( status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" ), @@ -208,7 +211,7 @@ def test_readers_unusual() -> None: tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n", + b"HTTP/1.0 200 OK\r\nFoo: \t \t \r\n\r\n", Response( status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK" ), @@ -217,7 +220,7 @@ def test_readers_unusual() -> None: # Tolerate broken servers that leave off the response code tr( READERS[SERVER, SEND_RESPONSE], - b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n", + b"HTTP/1.0 200\r\nFoo: bar\r\n\r\n", Response( status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b"" ), @@ -287,30 +290,30 @@ def test_readers_unusual() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\n folded: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo : line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo\t: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nfoo\t: line\r\n\r\n", None, ) with pytest.raises(LocalProtocolError): - tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None) + tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n: line\r\n\r\n", None) def test__obsolete_line_fold_bytes() -> None: @@ -343,7 +346,7 @@ def _run_reader_iter( yield reader.read_eof() -def _run_reader(*args: Any) -> List[Event]: +def _run_reader(*args: Any) -> list[Event]: events = list(_run_reader_iter(*args)) return normalize_data_events(events) @@ -412,10 +415,7 @@ def test_ChunkedReader() -> None: t_body_reader( ChunkedReader, - b"5\r\n01234\r\n" - + b"10\r\n0123456789abcdef\r\n" - + b"0\r\n" - + b"Some: header\r\n\r\n", + b"5\r\n01234\r\n10\r\n0123456789abcdef\r\n0\r\nSome: header\r\n\r\n", [ Data(data=b"012340123456789abcdef"), EndOfMessage(headers=[("Some", "header")]), @@ -447,9 +447,9 @@ def test_ChunkedReader() -> None: t_body_reader( ChunkedReader, b"5; hello=there\r\n" - + b"xxxxx" - + b"\r\n" - + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n', + b"xxxxx" + b"\r\n" + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n', [Data(data=b"xxxxx"), EndOfMessage()], ) @@ -533,7 +533,7 @@ def test_reject_garbage_after_response_line() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n", + b"HEAD /foo HTTP/1.1 xxxxxx\r\nHost: a\r\n\r\n", None, ) @@ -542,7 +542,7 @@ def test_reject_garbage_in_header_line() -> None: with pytest.raises(LocalProtocolError): tr( READERS[CLIENT, IDLE], - b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n", + b"HEAD /foo HTTP/1.1\r\nHost: foo\x00bar\r\n\r\n", None, ) @@ -553,7 +553,7 @@ def test_reject_non_vchar_in_path() -> None: message.append(bad_char) message.extend(b" HTTP/1.1\r\nHost: foobar\r\n\r\n") with pytest.raises(LocalProtocolError): - tr(READERS[CLIENT, IDLE], message, None) + tr(READERS[CLIENT, IDLE], bytes(message), None) # https://github.com/python-hyper/h11/issues/57 diff --git a/h11/tests/test_receivebuffer.py b/h11/tests/test_receivebuffer.py index 21a3870..8bd948d 100644 --- a/h11/tests/test_receivebuffer.py +++ b/h11/tests/test_receivebuffer.py @@ -1,6 +1,3 @@ -import re -from typing import Tuple - import pytest from .._receivebuffer import ReceiveBuffer @@ -119,7 +116,7 @@ def test_receivebuffer() -> None: ), ], ) -def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None: +def test_receivebuffer_for_invalid_delimiter(data: tuple[bytes]) -> None: b = ReceiveBuffer() for line in data: diff --git a/h11/tests/test_state.py b/h11/tests/test_state.py index bc974e6..6100a84 100644 --- a/h11/tests/test_state.py +++ b/h11/tests/test_state.py @@ -1,10 +1,11 @@ +# ruff: noqa: N802 + import pytest from .._events import ( ConnectionClosed, Data, EndOfMessage, - Event, InformationalResponse, Request, Response, @@ -14,7 +15,6 @@ _SWITCH_UPGRADE, CLIENT, CLOSED, - ConnectionState, DONE, IDLE, MIGHT_SWITCH_PROTOCOL, @@ -23,6 +23,7 @@ SEND_RESPONSE, SERVER, SWITCHED_PROTOCOL, + ConnectionState, ) from .._util import LocalProtocolError diff --git a/h11/tests/test_util.py b/h11/tests/test_util.py index 79bc095..a5a5323 100644 --- a/h11/tests/test_util.py +++ b/h11/tests/test_util.py @@ -1,3 +1,5 @@ +# ruff: noqa: N802, PT017 + import re import sys import traceback @@ -6,11 +8,11 @@ import pytest from .._util import ( - bytesify, LocalProtocolError, ProtocolError, RemoteProtocolError, Sentinel, + bytesify, validate, ) @@ -36,6 +38,7 @@ def test_LocalProtocolError() -> None: def thunk() -> NoReturn: raise LocalProtocolError("a", error_status_hint=420) + orig_traceback = "" try: try: thunk() @@ -86,7 +89,7 @@ class S(Sentinel, metaclass=Sentinel): pass assert repr(S) == "S" - assert S == S + assert S == S # noqa: PLR0124, expected test assert type(S).__name__ == "S" assert S in {S} assert type(S) is S diff --git a/pylock.toml b/pylock.toml new file mode 100644 index 0000000..d1cf5d5 --- /dev/null +++ b/pylock.toml @@ -0,0 +1,479 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. +lock-version = "1.0" +requires-python = ">=3.10" +environments = [ + "python_version >= \"3.10\"", +] +extras = [] +dependency-groups = ["default", "dev"] +default-groups = ["default"] +created-by = "pdm" + +[[packages]] +name = "pytest" +version = "8.4.2" +requires-python = ">=3.9" +sdist = {name = "pytest-8.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hashes = {sha256 = "86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}} +wheels = [ + {name = "pytest-8.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl",hashes = {sha256 = "872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=1", + "packaging>=20", + "pluggy<2,>=1.5", + "pygments>=2.7.2", + "tomli>=1; python_version < \"3.11\"", +] + +[[packages]] +name = "pytest-cov" +version = "7.0.0" +requires-python = ">=3.9" +sdist = {name = "pytest_cov-7.0.0.tar.gz", url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hashes = {sha256 = "33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}} +wheels = [ + {name = "pytest_cov-7.0.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl",hashes = {sha256 = "3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "coverage[toml]>=7.10.6", + "pluggy>=1.2", + "pytest>=7", +] + +[[packages]] +name = "ruff" +version = "0.14.2" +requires-python = ">=3.7" +sdist = {name = "ruff-0.14.2.tar.gz", url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hashes = {sha256 = "98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96"}} +wheels = [ + {name = "ruff-0.14.2-py3-none-linux_armv6l.whl",url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl",hashes = {sha256 = "7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1"}}, + {name = "ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl",hashes = {sha256 = "8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11"}}, + {name = "ruff-0.14.2-py3-none-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl",hashes = {sha256 = "5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl",url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl",hashes = {sha256 = "1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096"}}, + {name = "ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl",url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl",hashes = {sha256 = "2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df"}}, + {name = "ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05"}}, + {name = "ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl",url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl",hashes = {sha256 = "e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5"}}, + {name = "ruff-0.14.2-py3-none-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl",hashes = {sha256 = "b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e"}}, + {name = "ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770"}}, + {name = "ruff-0.14.2-py3-none-win32.whl",url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl",hashes = {sha256 = "41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9"}}, + {name = "ruff-0.14.2-py3-none-win_amd64.whl",url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl",hashes = {sha256 = "0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af"}}, + {name = "ruff-0.14.2-py3-none-win_arm64.whl",url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl",hashes = {sha256 = "ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "tox" +version = "4.32.0" +requires-python = ">=3.10" +sdist = {name = "tox-4.32.0.tar.gz", url = "https://files.pythonhosted.org/packages/59/bf/0e4dbd42724cbae25959f0e34c95d0c730df03ab03f54d52accd9abfc614/tox-4.32.0.tar.gz", hashes = {sha256 = "1ad476b5f4d3679455b89a992849ffc3367560bbc7e9495ee8a3963542e7c8ff"}} +wheels = [ + {name = "tox-4.32.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/fc/cc/e09c0d663a004945f82beecd4f147053567910479314e8d01ba71e5d5dea/tox-4.32.0-py3-none-any.whl",hashes = {sha256 = "451e81dc02ba8d1ed20efd52ee409641ae4b5d5830e008af10fe8823ef1bd551"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "cachetools>=6.2", + "chardet>=5.2", + "colorama>=0.4.6", + "filelock>=3.20", + "packaging>=25", + "platformdirs>=4.5", + "pluggy>=1.6", + "pyproject-api>=1.9.1", + "tomli>=2.3; python_version < \"3.11\"", + "typing-extensions>=4.15; python_version < \"3.11\"", + "virtualenv>=20.34", +] + +[[packages]] +name = "typing-extensions" +version = "4.15.0" +requires-python = ">=3.9" +sdist = {name = "typing_extensions-4.15.0.tar.gz", url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hashes = {sha256 = "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}} +wheels = [ + {name = "typing_extensions-4.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl",hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}}, +] +marker = "\"default\" in dependency_groups and python_version < \"3.12\" or \"dev\" in dependency_groups and python_version < \"3.12\"" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pyrefly" +version = "0.39.2" +requires-python = ">=3.8" +sdist = {name = "pyrefly-0.39.2.tar.gz", url = "https://files.pythonhosted.org/packages/d2/a0/2edbf98dd17a2f1209d7ee5424599222bc9438105f0398a37e9adf7e333e/pyrefly-0.39.2.tar.gz", hashes = {sha256 = "21669b53bcd831eea8f6b38ebb7da062e3060b416eb8ffccbebf4ae35be75fac"}} +wheels = [ + {name = "pyrefly-0.39.2-py3-none-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/ba/66/cb9937d4cb828c78ecba35b074510fb149405b4fafd641a5128109934242/pyrefly-0.39.2-py3-none-macosx_10_12_x86_64.whl",hashes = {sha256 = "a82bcc2d4b677e54110f9b7242db10791a5e8ada510a56352daeecdfeaaf5ba8"}}, + {name = "pyrefly-0.39.2-py3-none-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/d3/74/c32977401fd37e862ed02559d7da1f68e1f2ae4d50e8820c826104837a26/pyrefly-0.39.2-py3-none-macosx_11_0_arm64.whl",hashes = {sha256 = "f56265fea205d6c9f06d3ed8df194e6310940b4e2fa72ac78bcf3e7509f07f59"}}, + {name = "pyrefly-0.39.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/14/a8/c68a4ea5a8ab43a7d762cde89691d13eb75811ddbd81be70771010198d6d/pyrefly-0.39.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "c06e048153d84cc7f6eb3e731601e3d4834295478239a3016bcb973f01ea5505"}}, + {name = "pyrefly-0.39.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",url = "https://files.pythonhosted.org/packages/d6/44/75f696bc9a6059142ab407e4ce5539ef27db3299b3a9f451753c67c2f95e/pyrefly-0.39.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",hashes = {sha256 = "99af4063dbdce75a336cc5c72fea8ba411fce805eb94e3e1ec77ce3d202eb43c"}}, + {name = "pyrefly-0.39.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/41/a5/c2b72c3b1f0fc98215f7716ebf127fca2e714a21211cd6f4b9f47a3aae9f/pyrefly-0.39.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "19785bccc541872173ac41c7abf66236489490689df7be9c9daa56bdc6c206d6"}}, + {name = "pyrefly-0.39.2-py3-none-win32.whl",url = "https://files.pythonhosted.org/packages/a7/35/f429a501c54f108cbcbedc37d05a9cd29179b6f577208ff145abf0dd2b9d/pyrefly-0.39.2-py3-none-win32.whl",hashes = {sha256 = "4c247e615d1f846d9f0959831b2890db7615c6d9ff309014f8e2ecdc06f12de1"}}, + {name = "pyrefly-0.39.2-py3-none-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b4/cb/b5457410ea6745c35c4e7a254149c1cdce8aa03098052fbc438d1a538d6c/pyrefly-0.39.2-py3-none-win_amd64.whl",hashes = {sha256 = "54b15dbb6a9bb1c6fe63322eafa305e56242846467d07f86bdfb73403a1e6c72"}}, + {name = "pyrefly-0.39.2-py3-none-win_arm64.whl",url = "https://files.pythonhosted.org/packages/8d/a4/acbc52be49039f1d0a27473ec2b951a8ead1bd476c6fe6d1497ddbe2c948/pyrefly-0.39.2-py3-none-win_arm64.whl",hashes = {sha256 = "50226586994df6a1ac8d9cb653d2c0e506f6b3efd6b2719cffcb4d246b758363"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pluggy" +version = "1.6.0" +requires-python = ">=3.9" +sdist = {name = "pluggy-1.6.0.tar.gz", url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hashes = {sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}} +wheels = [ + {name = "pluggy-1.6.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl",hashes = {sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "colorama" +version = "0.4.6" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}} +wheels = [ + {name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "packaging" +version = "25.0" +requires-python = ">=3.8" +sdist = {name = "packaging-25.0.tar.gz", url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hashes = {sha256 = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}} +wheels = [ + {name = "packaging-25.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl",hashes = {sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "tomli" +version = "2.3.0" +requires-python = ">=3.8" +sdist = {name = "tomli-2.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hashes = {sha256 = "64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}} +wheels = [ + {name = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}}, + {name = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}}, + {name = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}}, + {name = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}}, + {name = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}}, + {name = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}}, + {name = "tomli-2.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}}, + {name = "tomli-2.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}}, + {name = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}}, + {name = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}}, + {name = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}}, + {name = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}}, + {name = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}}, + {name = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}}, + {name = "tomli-2.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}}, + {name = "tomli-2.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}}, + {name = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}}, + {name = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}}, + {name = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}}, + {name = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}}, + {name = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}}, + {name = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}}, + {name = "tomli-2.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}}, + {name = "tomli-2.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}}, + {name = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}}, + {name = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}}, + {name = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}}, + {name = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}}, + {name = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}}, + {name = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}}, + {name = "tomli-2.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}}, + {name = "tomli-2.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}}, + {name = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}}, + {name = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}}, + {name = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}}, + {name = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}}, + {name = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}}, + {name = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}}, + {name = "tomli-2.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}}, + {name = "tomli-2.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}}, + {name = "tomli-2.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl",hashes = {sha256 = "e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}}, +] +marker = "python_version < \"3.11\" and \"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "cachetools" +version = "6.2.1" +requires-python = ">=3.9" +sdist = {name = "cachetools-6.2.1.tar.gz", url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hashes = {sha256 = "3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201"}} +wheels = [ + {name = "cachetools-6.2.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl",hashes = {sha256 = "09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "chardet" +version = "5.2.0" +requires-python = ">=3.7" +sdist = {name = "chardet-5.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hashes = {sha256 = "1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}} +wheels = [ + {name = "chardet-5.2.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl",hashes = {sha256 = "e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "coverage" +version = "7.11.0" +requires-python = ">=3.10" +sdist = {name = "coverage-7.11.0.tar.gz", url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hashes = {sha256 = "167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}} +wheels = [ + {name = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}}, + {name = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}}, + {name = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl",hashes = {sha256 = "5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl",hashes = {sha256 = "f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}}, + {name = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}}, + {name = "coverage-7.11.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl",hashes = {sha256 = "bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}}, + {name = "coverage-7.11.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}}, + {name = "coverage-7.11.0-cp314-cp314-win_arm64.whl",url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl",hashes = {sha256 = "ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}}, + {name = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}}, + {name = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl",hashes = {sha256 = "ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}}, + {name = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}}, + {name = "coverage-7.11.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl",hashes = {sha256 = "4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}}, + {name = "coverage-7.11.0-cp314-cp314t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl",hashes = {sha256 = "b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}}, + {name = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}}, + {name = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl",hashes = {sha256 = "df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl",hashes = {sha256 = "8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}}, + {name = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}}, + {name = "coverage-7.11.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl",hashes = {sha256 = "695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}}, + {name = "coverage-7.11.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}}, + {name = "coverage-7.11.0-cp313-cp313-win_arm64.whl",url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl",hashes = {sha256 = "0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl",hashes = {sha256 = "587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}}, + {name = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl",hashes = {sha256 = "b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}}, + {name = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl",hashes = {sha256 = "d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl",hashes = {sha256 = "6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}}, + {name = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}}, + {name = "coverage-7.11.0-cp313-cp313t-win32.whl",url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl",hashes = {sha256 = "cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl",hashes = {sha256 = "a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}}, + {name = "coverage-7.11.0-cp313-cp313t-win_arm64.whl",url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl",hashes = {sha256 = "f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}}, + {name = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}}, + {name = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl",hashes = {sha256 = "fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl",hashes = {sha256 = "dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}}, + {name = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}}, + {name = "coverage-7.11.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl",hashes = {sha256 = "037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}}, + {name = "coverage-7.11.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}}, + {name = "coverage-7.11.0-cp312-cp312-win_arm64.whl",url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl",hashes = {sha256 = "d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}}, + {name = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}}, + {name = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}}, + {name = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}}, + {name = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}}, + {name = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}}, + {name = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}}, + {name = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}}, + {name = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl",hashes = {sha256 = "214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}}, + {name = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl",hashes = {sha256 = "258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}}, + {name = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}}, + {name = "coverage-7.11.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl",hashes = {sha256 = "fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}}, + {name = "coverage-7.11.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}}, + {name = "coverage-7.11.0-cp311-cp311-win_arm64.whl",url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl",hashes = {sha256 = "5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}}, + {name = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl",hashes = {sha256 = "eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}}, + {name = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl",hashes = {sha256 = "d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}}, + {name = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl",hashes = {sha256 = "0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}}, + {name = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",hashes = {sha256 = "e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}}, + {name = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}}, + {name = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",hashes = {sha256 = "ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}}, + {name = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl",hashes = {sha256 = "9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}}, + {name = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl",url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl",hashes = {sha256 = "e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}}, + {name = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl",url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl",hashes = {sha256 = "7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}}, + {name = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl",hashes = {sha256 = "3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}}, + {name = "coverage-7.11.0-cp310-cp310-win32.whl",url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl",hashes = {sha256 = "765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}}, + {name = "coverage-7.11.0-cp310-cp310-win_amd64.whl",url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl",hashes = {sha256 = "24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}}, + {name = "coverage-7.11.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl",hashes = {sha256 = "4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "exceptiongroup" +version = "1.3.0" +requires-python = ">=3.7" +sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hashes = {sha256 = "b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}} +wheels = [ + {name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}}, +] +marker = "python_version < \"3.11\" and \"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] + +[[packages]] +name = "filelock" +version = "3.20.0" +requires-python = ">=3.10" +sdist = {name = "filelock-3.20.0.tar.gz", url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hashes = {sha256 = "711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"}} +wheels = [ + {name = "filelock-3.20.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",hashes = {sha256 = "339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "iniconfig" +version = "2.3.0" +requires-python = ">=3.10" +sdist = {name = "iniconfig-2.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hashes = {sha256 = "c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}} +wheels = [ + {name = "iniconfig-2.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl",hashes = {sha256 = "f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "platformdirs" +version = "4.5.0" +requires-python = ">=3.10" +sdist = {name = "platformdirs-4.5.0.tar.gz", url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hashes = {sha256 = "70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}} +wheels = [ + {name = "platformdirs-4.5.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl",hashes = {sha256 = "e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pygments" +version = "2.19.2" +requires-python = ">=3.8" +sdist = {name = "pygments-2.19.2.tar.gz", url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hashes = {sha256 = "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}} +wheels = [ + {name = "pygments-2.19.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl",hashes = {sha256 = "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[[packages]] +name = "pyproject-api" +version = "1.10.0" +requires-python = ">=3.10" +sdist = {name = "pyproject_api-1.10.0.tar.gz", url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hashes = {sha256 = "40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}} +wheels = [ + {name = "pyproject_api-1.10.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl",hashes = {sha256 = "8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "packaging>=25", + "tomli>=2.3; python_version < \"3.11\"", +] + +[[packages]] +name = "virtualenv" +version = "20.35.3" +requires-python = ">=3.8" +sdist = {name = "virtualenv-20.35.3.tar.gz", url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hashes = {sha256 = "4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44"}} +wheels = [ + {name = "virtualenv-20.35.3-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl",hashes = {sha256 = "63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", + "typing-extensions>=4.13.2; python_version < \"3.11\"", +] + +[[packages]] +name = "distlib" +version = "0.4.0" +sdist = {name = "distlib-0.4.0.tar.gz", url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hashes = {sha256 = "feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}} +wheels = [ + {name = "distlib-0.4.0-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl",hashes = {sha256 = "9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}}, +] +marker = "\"dev\" in dependency_groups" + +[packages.tool.pdm] +dependencies = [] + +[tool.pdm] +hashes = {sha256 = "42afc75737c8509c983671b1f9bbe58ff57676dc73f453b3f9453563b4d55665"} +strategy = ["inherit_metadata", "static_urls"] + +[[tool.pdm.targets]] +requires_python = ">=3.10" diff --git a/pyproject.toml b/pyproject.toml index 64a6883..280a70d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,97 @@ +[build-system] +requires = [ + "setuptools >= 77", + "setuptools_scm >= 6.4" +] +build-backend = "setuptools.build_meta" + +[project] +name = "h11" +authors = [ + { name = "Nathaniel J. Smith", email = "njs@pobox.com" } +] +license = { file = "MIT" } +requires-python = ">=3.10" +dependencies = [ + "typing_extensions; python_version < '3.12'" +] +dynamic = ["version"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", + "Topic :: System :: Networking", +] + +[project.urls] +source = "https://github.com/python-hyper/h11" + +[tool.setuptools] +packages = ["h11"] + +[tool.setuptools.dynamic] +version = { attr = "h11._version.__version__" } + +[dependency-groups] +dev = [ + "ruff>=0.14.2", + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "tox>=4.32.0", + "pyrefly>=0.39.2", +] + +[tool.ruff] +target-version = "py310" +line-length = 88 +respect-gitignore = true +src = ["h11"] +exclude = ["fuzz", "examples", "docs/source", "bench"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "TID252", + "ERA001", + "PLR2004", # HTTP library has magic numbers, shocking! + + # Existing project code style + "D", + "SLF001", + "PGH003", + + # Bad lints + "TRY", + "EM", + "S101", # Allow asserts + "TC006", + "COM812", + "PTH", + "ANN401", + "FBT", + "PLR0915", + "PLR0911", + "PLR0912", + "S105", # Hardcoded password? what? + "C90", +] + +[tool.pyrefly] +project-includes = [ + "h11", +] +python-version = "3.10" + [tool.towncrier] # Usage: # - PRs should drop a file like "issuenumber.feature" in newsfragments @@ -44,3 +138,7 @@ strict = true warn_unused_configs = true warn_unused_ignores = true show_error_codes = true + +[tool.tox] +requires = ["tox>=4.32"] +envlist = ["format", "py310", "py311", "py312", "py313", "py314", "pypy3"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 73713e2..0000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -from setuptools import setup, find_packages - -# defines __version__ -exec(open("h11/_version.py").read()) - -setup( - name="h11", - version=__version__, - description= - "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1", - long_description=open("README.rst").read(), - author="Nathaniel J. Smith", - author_email="njs@pobox.com", - license="MIT", - packages=find_packages(exclude=["h11.tests"]), - package_data={'h11': ['py.typed']}, - url="https://github.com/python-hyper/h11", - python_requires=">=3.8", - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Internet :: WWW/HTTP", - "Topic :: System :: Networking", - ], -) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 9955dec..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -pytest-cov diff --git a/tox.ini b/tox.ini index 6614ecf..3077c85 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,28 @@ [tox] -envlist = format, py{38, 39, 310, 311, 312, py3}, mypy +envlist = format, py{310, 311, 312, 313, 314, py3}, typecheck [gh-actions] python = - 3.8: py38, format, mypy - 3.9: py39 - 3.10: py310 + 3.10: py310 format typecheck 3.11: py311 3.12: py312 3.13: py313 - pypy-3.9: pypy3 + 3.14: py314 pypy-3.10: pypy3 [testenv] -deps = -r{toxinidir}/test-requirements.txt +dependency_groups = dev commands = pytest --cov=h11 --cov-config=.coveragerc h11 [testenv:format] -basepython = python3.8 -deps = -r{toxinidir}/format-requirements.txt +basepython = python3.11 +dependency_groups = dev commands = - black --check --diff h11/ bench/ examples/ fuzz/ - isort --check --diff --profile black --dt h11 bench examples fuzz + ruff check --diff + ruff format --check --diff -[testenv:mypy] -basepython = python3.8 -deps = - mypy==1.8.0 - pytest +[testenv:typecheck] +basepython = python3.11 +dependency_groups = dev commands = - mypy h11/ + pyrefly check h11/