From a8e78133187875cb5a8af9829621f5286a2e9aa9 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 19 Apr 2024 13:05:08 -0400 Subject: [PATCH] include stubs for python-multipart --- basgi/py.typed | 0 basgi/request.py | 8 +- basgi/runner.py | 11 ++ basgi/template.py | 2 +- dev.py | 9 ++ docs/src/api/index.rst | 4 +- docs/src/api/runners.rst | 2 + pyproject.toml | 2 +- stubs/multipart/__init__.pyi | 3 + stubs/multipart/decoders.pyi | 20 ++++ stubs/multipart/exceptions.pyi | 9 ++ stubs/multipart/multipart.pyi | 184 +++++++++++++++++++++++++++++++++ 12 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 basgi/py.typed create mode 100644 stubs/multipart/__init__.pyi create mode 100644 stubs/multipart/decoders.pyi create mode 100644 stubs/multipart/exceptions.pyi create mode 100644 stubs/multipart/multipart.pyi diff --git a/basgi/py.typed b/basgi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/basgi/request.py b/basgi/request.py index f3fb39f..1f04a87 100644 --- a/basgi/request.py +++ b/basgi/request.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing from aputils import JsonBase +from blib import random_str from collections.abc import Iterable from multidict import CIMultiDict, CIMultiDictProxy from multipart.multipart import Field, File, create_form_parser @@ -122,7 +123,12 @@ class Request: else: field.file_object.seek(0) - fields[field.field_name.decode("utf-8")] = field + + if field.field_name is None: + fields[f"unnamed-file-{random_str(10)}"] = field + + else: + fields[field.field_name.decode("utf-8")] = field parser = create_form_parser(self.headers, handle_field, handle_field) diff --git a/basgi/runner.py b/basgi/runner.py index 61a7184..4a0b86e 100644 --- a/basgi/runner.py +++ b/basgi/runner.py @@ -44,25 +44,36 @@ class Runner(ABC): @abstractmethod def setup_module(self) -> None: + "Import all necessary modules here and add them as attributes to the runner" ... @abstractmethod def reload(self) -> None: + "Start the runner with reloading enabled" ... @abstractmethod def simple(self) -> None: + "Start the runner without workers" ... @abstractmethod def multiprocess(self) -> None: + "Start the runner with multiple process workers" ... class UvicornRunner(Runner): + """ + Start an ASGI application with Uvicorn + + .. note:: Do not start this runner in the same process as the application unless it is + started with the simple runner. + """ + def setup_module(self) -> None: self.uvicorn = import_module("uvicorn") self.supervisors = import_module("uvicorn.supervisors") diff --git a/basgi/template.py b/basgi/template.py index cdf334b..9bba282 100644 --- a/basgi/template.py +++ b/basgi/template.py @@ -55,7 +55,7 @@ class SassExtension(Extension): if (ext := splitext(tpl_name)[1]) not in self._exts: return source - return sass.compile( # type: ignore[no-any-return] + return sass.compile( string = source, output_style = self.output_style.value, indented = ext == ".sass" diff --git a/dev.py b/dev.py index 3ba7fba..9098e3f 100755 --- a/dev.py +++ b/dev.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import asyncio +import os import shlex import subprocess import sys @@ -90,6 +91,7 @@ def cli_lint(path: Path, watch: bool) -> None: run_python("-m", "flake8", str(path)) echo("\n----- mypy -----") + os.environ["MYPYPATH"] = str(REPO.joinpath("stubs")) run_python("-m", "mypy", str(path)) @@ -120,6 +122,13 @@ def cli_update_files() -> None: project_file.write(project) +@cli.command("generate-stubs") +def cli_generate_stubs(): + subprocess.run( + ["-m", "mypy.stubgen", "-o", "stubs", "-p", "multipart", "--export-less"] + ) + + def run_python(*arguments: str) -> subprocess.CompletedProcess[bytes]: return subprocess.run([sys.executable, *arguments]) diff --git a/docs/src/api/index.rst b/docs/src/api/index.rst index 887bea6..09a9aa4 100644 --- a/docs/src/api/index.rst +++ b/docs/src/api/index.rst @@ -27,9 +27,9 @@ HTTP Messages Application Runners ------------------- -:meth:`basgi.run_app` +:class:`basgi.GranianRunner` -:meth:`basgi.handle_run_app` +:class:`basgi.UvicornRunner` Templates diff --git a/docs/src/api/runners.rst b/docs/src/api/runners.rst index fc4ca48..b20541b 100644 --- a/docs/src/api/runners.rst +++ b/docs/src/api/runners.rst @@ -8,7 +8,9 @@ App Runners .. autoclass:: basgi.GranianRunner :members: :show-inheritance: + :exclude-members: setup_module .. autoclass:: basgi.UvicornRunner :members: :show-inheritance: + :exclude-members: setup_module diff --git a/pyproject.toml b/pyproject.toml index 0880426..f3ff522 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dev = [ "mypy == 1.9.0", "flake8 == 7.0.0", "pyinstaller == 6.5.0", - "types-jinja2 == 2.11.9", + "types-libsass == 0.23.0.20240311", "watchfiles == 0.21.0" ] diff --git a/stubs/multipart/__init__.pyi b/stubs/multipart/__init__.pyi new file mode 100644 index 0000000..b4e3b8e --- /dev/null +++ b/stubs/multipart/__init__.pyi @@ -0,0 +1,3 @@ +from .multipart import FormParser as FormParser, MultipartParser as MultipartParser, OctetStreamParser as OctetStreamParser, QuerystringParser as QuerystringParser, create_form_parser as create_form_parser, parse_form as parse_form + +__all__ = ['FormParser', 'MultipartParser', 'OctetStreamParser', 'QuerystringParser', 'create_form_parser', 'parse_form'] diff --git a/stubs/multipart/decoders.pyi b/stubs/multipart/decoders.pyi new file mode 100644 index 0000000..75e3279 --- /dev/null +++ b/stubs/multipart/decoders.pyi @@ -0,0 +1,20 @@ +import typing + +if typing.TYPE_CHECKING: + from .multipart import Field, File + +class Base64Decoder: + cache: bytearray + underlying: Field | File + def __init__(self, underlying: Field | File) -> None: ... + def write(self, data) -> int: ... + def close(self) -> None: ... + def finalize(self) -> None: ... + +class QuotedPrintableDecoder: + cache: bytes + underlying: Field | File + def __init__(self, underlying: Field | File) -> None: ... + def write(self, data) -> int: ... + def close(self) -> None: ... + def finalize(self) -> None: ... diff --git a/stubs/multipart/exceptions.pyi b/stubs/multipart/exceptions.pyi new file mode 100644 index 0000000..a9da5ec --- /dev/null +++ b/stubs/multipart/exceptions.pyi @@ -0,0 +1,9 @@ +class FormParserError(ValueError): ... + +class ParseError(FormParserError): + offset: int + +class MultipartParseError(ParseError): ... +class QuerystringParseError(ParseError): ... +class DecodeError(ParseError): ... +class FileError(FormParserError, OSError): ... diff --git a/stubs/multipart/multipart.pyi b/stubs/multipart/multipart.pyi new file mode 100644 index 0000000..77ba7c0 --- /dev/null +++ b/stubs/multipart/multipart.pyi @@ -0,0 +1,184 @@ +import io +import logging + +from collections.abc import Mapping, Sequence +from enum import IntEnum +from typing import Any, Callable, TypedDict + + +ParserCallback = Callable[[Any, int, int], None] | Callable[..., None] + +class QuerystringCallbacks(TypedDict, total=False): + on_field_start: Callable[[], None] + on_field_name: Callable[[bytes, int, int], None] + on_field_data: Callable[[bytes, int, int], None] + on_field_end: Callable[[], None] + on_end: Callable[[], None] + +class OctetStreamCallbacks(TypedDict, total=False): + on_start: Callable[[], None] + on_data: Callable[[bytes, int, int], None] + on_end: Callable[[], None] + +class MultipartCallbacks(TypedDict, total=False): + on_part_begin: Callable[[], None] + on_part_data: Callable[[bytes, int, int], None] + on_part_end: Callable[[], None] + on_headers_begin: Callable[[], None] + on_header_field: Callable[[bytes, int, int], None] + on_header_value: Callable[[bytes, int, int], None] + on_header_end: Callable[[], None] + on_headers_finished: Callable[[], None] + on_end: Callable[[], None] + +class FormParserConfig(TypedDict, total=False): + UPLOAD_DIR: str | None + UPLOAD_KEEP_FILENAME: bool + UPLOAD_KEEP_EXTENSIONS: bool + UPLOAD_ERROR_ON_BAD_CTE: bool + MAX_MEMORY_FILE_SIZE: int + MAX_BODY_SIZE: float + +class FileConfig(TypedDict, total=False): + UPLOAD_DIR: str | None + UPLOAD_DELETE_TMP: bool + UPLOAD_KEEP_FILENAME: bool + UPLOAD_KEEP_EXTENSIONS: bool + MAX_MEMORY_FILE_SIZE: int + +class QuerystringState(IntEnum): + BEFORE_FIELD: int + FIELD_NAME: int + FIELD_DATA: int + +class MultipartState(IntEnum): + START: int + START_BOUNDARY: int + HEADER_FIELD_START: int + HEADER_FIELD: int + HEADER_VALUE_START: int + HEADER_VALUE: int + HEADER_VALUE_ALMOST_DONE: int + HEADERS_ALMOST_DONE: int + PART_DATA_START: int + PART_DATA: int + PART_DATA_END: int + END: int + +FLAG_PART_BOUNDARY: int +FLAG_LAST_BOUNDARY: int +CR: bytes +LF: bytes +COLON: bytes +SPACE: bytes +HYPHEN: bytes +AMPERSAND: bytes +SEMICOLON: bytes +LOWER_A: bytes +LOWER_Z: bytes +NULL: bytes + +def lower_char(c: bytes) -> bytes:... +def ord_char(c: bytes) -> bytes: ... +def join_bytes(b: Sequence[bytes]) -> bytes: ... +def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: ... + +class Field: + def __init__(self, name: str) -> None: ... + @classmethod + def from_value(cls, name: str, value: bytes | None) -> Field: ... + def write(self, data: bytes) -> int: ... + def on_data(self, data: bytes) -> int: ... + def on_end(self) -> None: ... + def finalize(self) -> None: ... + def close(self) -> None: ... + def set_none(self) -> None: ... + @property + def field_name(self) -> bytes: ... + @property + def value(self) -> bytes: ... + def __eq__(self, other: object) -> bool: ... + +class File: + logger: logging.Logger + def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}) -> None: ... + @property + def field_name(self) -> bytes | None: ... + @property + def file_name(self) -> bytes | None: ... + @property + def actual_file_name(self) -> str | None: ... + @property + def file_object(self) -> io.BytesIO | io.TextIOWrapper | io.BufferedReader: ... + @property + def size(self) -> int: ... + @property + def in_memory(self) -> bool: ... + def flush_to_disk(self) -> None: ... + def write(self, data: bytes) -> int: ... + def on_data(self, data: bytes) -> int: ... + def on_end(self) -> None: ... + def finalize(self) -> None: ... + def close(self) -> None: ... + +class BaseParser: + logger: logging.Logger + def __init__(self) -> None: ... + def callback(self, name: str, data: Any | None = None, start: int | None = None, end: int | None = None) -> None: ... + def set_callback(self, name: str, new_func: ParserCallback) -> None: ... + def close(self) -> None: ... + def finalize(self) -> None: ... + +class OctetStreamParser(BaseParser): + callbacks: OctetStreamCallbacks + max_size: float + def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size: float = float("inf")) -> None: ... + def write(self, data: bytes) -> int: ... + def finalize(self) -> None: ... + +class QuerystringParser(BaseParser): + state: QuerystringState + callbacks: QuerystringCallbacks + max_size: float + strict_parsing: bool + def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing: bool = False, max_size: float = float("inf")) -> None: ... + def write(self, data: bytes) -> int: ... + def finalize(self) -> None: ... + +class MultipartParser(BaseParser): + state: MultipartState + index: int + callbacks: MultipartCallbacks + max_size: float + marks: dict[str, int] + boundary: bytes + boundary_chars: frozenset[bytes] + lookbehind: Sequence[bytes] + def __init__(self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size: float = float("inf")) -> None: ... + def write(self, data: bytes) -> int: ... + def finalize(self) -> None: ... + +OnFieldCallback = Callable[[Field], None] +OnFileCallback = Callable[[File], None] +OnEndCallback = Callable[..., Any | None] + +class FormParser: + DEFAULT_CONFIG: FormParserConfig + logger: logging.Logger + content_type: str + boundary: bytes | None + bytes_received: int + parser: BaseParser + on_field: OnFieldCallback + on_file: OnFileCallback + on_end: OnEndCallback | None + FileClass: File + FieldClass: Field + config: FormParserConfig | None + def __init__(self, content_type: str, on_field: OnFieldCallback, on_file: OnFileCallback, on_end: OnEndCallback | None = None, boundary: bytes | None = None, file_name: str | None = None, FileClass: type[File] = File, FieldClass: type[Field] = Field, config: FormParserConfig = {}) -> None: ... + def write(self, data: bytes) -> int: ... + def finalize(self) -> None: ... + def close(self) -> None: ... + +def create_form_parser(headers: Mapping[str, str], on_field: OnFieldCallback, on_file: OnFileCallback, trust_x_headers: bool = False, config: FormParserConfig = {}) -> FormParser: ... +def parse_form(headers: Mapping[str, str], input_stream: io.BufferedIOBase, on_field: OnFieldCallback, on_file: OnFileCallback, chunk_size: int = 1048576, **kwargs: Any) -> None: ...