Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ResponseValue Typing issue #5322

Open
CoolCat467 opened this issue Nov 5, 2023 · 5 comments · May be fixed by pallets/quart#341
Open

ResponseValue Typing issue #5322

CoolCat467 opened this issue Nov 5, 2023 · 5 comments · May be fixed by pallets/quart#341
Labels

Comments

@CoolCat467
Copy link

This is not a runtime bug but a typing issue. In flask.typing, ResponseValue does not accept AsyncIterator[str].

I am using quart-trio, which itself uses quart, which uses flask.
quart.templating.stream_template returns AsyncIterator[str], and when running mypy on my project I get the following error:

error: Value of type variable "T_route" of function cannot be "Callable[[], Coroutine[Any, Any, AsyncIterator[str] | Response]]"  [type-var]

Example usage:

__title__ = "Example Server"
__author__ = "CoolCat467"

from quart.templating import stream_template
from quart_trio import QuartTrio
import trio
from os import path, makedirs
import functools
import logging
from quart import Response
from typing import Final
from logging.handlers import TimedRotatingFileHandler
from collections.abc import AsyncIterator
from hypercorn.config import Config
from hypercorn.trio import serve


DOMAIN: str | None = None#getenv("DOMAIN", None)

FORMAT = "[%(asctime)s] [%(levelname)s] %(message)s"

ROOT_FOLDER = trio.Path(path.dirname(__file__))
CURRENT_LOG = ROOT_FOLDER / "logs" / "current.log"

if not path.exists(path.dirname(CURRENT_LOG)):
    makedirs(path.dirname(CURRENT_LOG))

logging.basicConfig(format=FORMAT, level=logging.DEBUG, force=True)
logging.getLogger().addHandler(
    TimedRotatingFileHandler(
        CURRENT_LOG,
        when="D",
        backupCount=60,
        encoding="utf-8",
        utc=True,
        delay=True,
    ),
)


app: Final = QuartTrio(
    __name__,
    static_folder="static",
    template_folder="templates",
)

async def send_error(
    page_title: str,
    error_body: str,
    return_link: str | None = None,
) -> AsyncIterator[str]:
    """Stream error page."""
    return await stream_template(
        "error_page.html.jinja",
        page_title=page_title,
        error_body=error_body,
        return_link=return_link,
    )


async def get_exception_page(code: int, name: str, desc: str) -> Response:
    """Return Response for exception."""
    resp_body = await send_error(
        page_title=f"{code} {name}",
        error_body=desc,
    )
    return Response(resp_body, status=code)


@app.get("/")
async def root_get() -> Response:
    """Main page GET request."""
    return await get_exception_page(404, "Page not found", "Requested content does not exist.")


# Stolen from WOOF (Web Offer One File), Copyright (C) 2004-2009 Simon Budig,
# available at http://www.home.unix-ag.org/simon/woof
# with modifications

# Utility function to guess the IP (as a string) where the server can be
# reached from the outside. Quite nasty problem actually.


def find_ip() -> str:
    """Guess the IP where the server can be found from the network."""
    # we get a UDP-socket for the TEST-networks reserved by IANA.
    # It is highly unlikely, that there is special routing used
    # for these networks, hence the socket later should give us
    # the IP address of the default route.
    # We're doing multiple tests, to guard against the computer being
    # part of a test installation.

    candidates: list[str] = []
    for test_ip in ("192.0.2.0", "198.51.100.0", "203.0.113.0"):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.connect((test_ip, 80))
        ip_addr: str = sock.getsockname()[0]
        sock.close()
        if ip_addr in candidates:
            return ip_addr
        candidates.append(ip_addr)

    return candidates[0]


async def run_async(
    root_dir: str,
    port: int,
    *,
    ip_addr: str | None = None,
    localhost: bool = True,
) -> None:
    """Asynchronous Entry Point."""
    if ip_addr is None:
        ip_addr = "0.0.0.0"  # noqa: S104  # Binding to all interfaces
        if not localhost:
            ip_addr = find_ip()

    try:
        # Add more information about the address
        location = f"{ip_addr}:{port}"

        config = {
            "bind": [location],
            "worker_class": "trio",
        }
        if DOMAIN:
            config["certfile"] = f"/etc/letsencrypt/live/{DOMAIN}/fullchain.pem"
            config["keyfile"] = f"/etc/letsencrypt/live/{DOMAIN}/privkey.pem"
        app.config["SERVER_NAME"] = location

        app.jinja_options = {
            "trim_blocks": True,
            "lstrip_blocks": True,
        }

        app.add_url_rule("/<path:filename>", "static", app.send_static_file)

        config_obj = Config.from_mapping(config)

        proto = "http" if not DOMAIN else "https"
        print(f"Serving on {proto}://{location}\n(CTRL + C to quit)")

        await serve(app, config_obj)
    except OSError:
        logging.error(f"Cannot bind to IP address '{ip_addr}' port {port}")
        sys.exit(1)
    except KeyboardInterrupt:
        logging.info("Shutting down from keyboard interrupt")


def run() -> None:
    """Synchronous Entry Point."""
    root_dir = path.dirname(__file__)
    port = 6002

    hostname: Final = "None"#os.getenv("HOSTNAME", "None")

    ip_address = None
    if hostname != "None":
        ip_address = hostname

    local = True#"--nonlocal" not in sys.argv[1:]

    trio.run(
        functools.partial(
            run_async,
            root_dir,
            port,
            ip_addr=ip_address,
            localhost=local,
        ),
        restrict_keyboard_interrupt_to_checkpoints=True,
    )


def main() -> None:
    """Call run after setup."""
    print(f"{__title__}\nProgrammed by {__author__}.\n")
    try:
        logging.captureWarnings(True)
        run()
    finally:
        logging.shutdown()


if __name__ == "__main__":
    main()

templates/error_page.html.jinja

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ page_title }}</title>
    <!--<link rel="stylesheet" type="text/css" href="/style.css">-->
  </head>
  <body>
    <div class="content">
      <h1>{{ page_title }}</h1>
      <div class="box">
        <p>
          {{ error_body }}
        </p>
        <br>
        {% if return_link %}
        <a href="{{ return_link }}">Return to previous page</a>
        <br>
        {% endif %}
        <a href="/">Return to main page</a>
      </div>
    </div>
    <footer>
      <i>If you're reading this, the web server was installed correctly.™</i>
      <hr>
      <p>Example Web Server v0.0.0 © CoolCat467</p>
    </footer>
  </body>
</html>

Environment:

  • Python version: 3.12
  • Flask version: 3.0.0
@davidism
Copy link
Member

davidism commented Nov 5, 2023

Flask doesn't accept that though. It would be Quart's response type that should allow that type. If it doesn't, that should be reported to Quart.

@davidism davidism closed this as not planned Won't fix, can't repro, duplicate, stale Nov 5, 2023
@pgjones
Copy link
Member

pgjones commented Nov 5, 2023

This one is a Flask issue now that Quart is based on Flask. I think the solution is to make the Flask sansio classes Generic over the response type. (Or support async iterators in Flask which could be nice).

@CoolCat467 Something to type: ignore for a while - until I find a nice solution.

@davidism davidism reopened this Nov 5, 2023
@davidism
Copy link
Member

davidism commented Nov 5, 2023

Ok, wasn't clear that Quart was passing through Flask's type here.

@pallets pallets deleted a comment from Obinna-Nwankwo Nov 8, 2023
@pgjones

This comment was marked as off-topic.

@pallets pallets deleted a comment from georgruetsche Nov 13, 2023
@davidism davidism added this to the 3.0.1 milestone Nov 15, 2023
@davidism davidism removed this from the 3.0.1 milestone Jan 15, 2024
mjsir911 added a commit to mjsir911/quart that referenced this issue May 20, 2024
@mjsir911 mjsir911 linked a pull request May 20, 2024 that will close this issue
6 tasks
@mjsir911
Copy link

mjsir911 commented May 20, 2024

Yeah looking at this again during the pycon 2024 sprints, the naive fix for this reported issue seems to be just adding AsyncIterator to the initializer within quart. I can't find how quart is "passing through Flask's type".

If anything, quart's Response object looks a lot like the superclass to flask's Response, werkzeug's Response, and if consolidation is desired than yeah modifying werkzeug's response makes sense. But for the time being I've filed pallets/quart#341 which does fix the provided example using the naive approach, I'm not sure if additional consolidation work is desired in the future but in that case that looks like a tracked issue would be better placed in werkzeug's repo.

mjsir911 added a commit to mjsir911/quart that referenced this issue May 20, 2024
mjsir911 added a commit to mjsir911/quart that referenced this issue May 20, 2024
mjsir911 added a commit to mjsir911/quart that referenced this issue May 20, 2024
mjsir911 added a commit to mjsir911/quart that referenced this issue May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants
@davidism @pgjones @mjsir911 @CoolCat467 and others