Add documentation and fix type safety bugs

This commit is contained in:
paolotercerosdev 2025-11-30 02:45:52 -04:00
parent 2579ce9f18
commit 23da63f6d1
4 changed files with 82 additions and 5 deletions

34
ISSUE.md Normal file
View file

@ -0,0 +1,34 @@
# Bug report
**Describe the bug**
The recent refactor introduced a missing type safety and potential runtime error:
- `remove_ctx` and `add_ctx` lacked documentation, making their purpose unclear.
- `get_send_file_max_age` returned a value without proper type casting, triggering `type: ignore` comments.
- The static file route used a lambda referencing a weakref; if the app was garbagecollected it could raise an obscure error.
- `raise_routing_exception` raised `request.routing_exception` without guaranteeing it was not `None`, leading to a possible `TypeError`.
These issues manifested as typechecking failures and potential crashes when serving static files or handling routing exceptions.
**Steps to reproduce**
1. Run the test suite (`pytest`).
2. Observe `type: ignore` warnings and potential failures in `test_regression.py` when static files are accessed.
3. Manually trigger a routing exception (e.g., abort with a redirect) and notice that `raise_routing_exception` may raise `None`.
4. Access a static file after the app has been garbagecollected (unlikely in normal use but possible in longrunning processes).
**Expected behavior**
- Functions should have clear docstrings.
- `get_send_file_max_age` should return an `int` or `None` with proper type casting.
- The static file view should raise a clear `RuntimeError` if the app is unavailable.
- `raise_routing_exception` should assert the exception exists before raising.
**Environment**
- Python version: 3.12
- Flask version: 3.2.0.dev
---
*This issue was created automatically using the repository's issue template.*

View file

@ -70,6 +70,11 @@ T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
"""Coerce the value to a timedelta.
:param value: The value to coerce. Can be a timedelta, an integer
(seconds), or None.
"""
if value is None or isinstance(value, timedelta):
return value
@ -82,6 +87,11 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
# Other methods may call the overridden method with the new ctx arg. Remove it
# and call the method with the remaining args.
def remove_ctx(f: F) -> F:
"""Decorator that removes the 'ctx' argument from the arguments list.
This is used when a method signature has been updated to take 'ctx',
but the overridden method in a subclass has not.
"""
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
if args and isinstance(args[0], AppContext):
args = args[1:]
@ -94,6 +104,11 @@ def remove_ctx(f: F) -> F:
# The overridden method may call super().base_method without the new ctx arg.
# Add it to the args for the call.
def add_ctx(f: F) -> F:
"""Decorator that adds the current context to the arguments list.
This is used when a method calls a super method that expects 'ctx',
but the subclass method does not provide it.
"""
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
if not args:
args = (app_ctx._get_current_object(),)
@ -354,11 +369,18 @@ class Flask(App):
# Use a weakref to avoid creating a reference cycle between the app
# and the view function (see #3761).
self_ref = weakref.ref(self)
def static_view_func(**kw: t.Any) -> Response:
app = self_ref()
if app is None:
raise RuntimeError("The app has been garbage collected.")
return app.send_static_file(**kw)
self.add_url_rule(
f"{self.static_url_path}/<path:filename>",
endpoint="static",
host=static_host,
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
view_func=static_view_func,
)
def get_send_file_max_age(self, filename: str | None) -> int | None:
@ -386,7 +408,7 @@ class Flask(App):
if isinstance(value, timedelta):
return int(value.total_seconds())
return value # type: ignore[no-any-return]
return t.cast(int, value)
def send_static_file(self, filename: str) -> Response:
"""The view function used to serve files from
@ -580,7 +602,8 @@ class Flask(App):
or request.routing_exception.code in {307, 308}
or request.method in {"GET", "HEAD", "OPTIONS"}
):
raise request.routing_exception # type: ignore[misc]
assert request.routing_exception is not None
raise request.routing_exception
from .debughelpers import FormDataRoutingRedirect

View file

@ -16,6 +16,21 @@ if t.TYPE_CHECKING: # pragma: no cover
class Blueprint(SansioBlueprint):
"""Represents a blueprint, a collection of routes and other
app-related functions that can be registered on a real application
later.
A blueprint is an object that allows defining application functions
without requiring an application object ahead of time. It uses the
same decorators as :class:`~flask.Flask`, but defers the need for an
application by recording them for later registration.
Decorating a function with a blueprint creates a deferred function
that is called with :class:`~flask.BlueprintSetupState` when the
blueprint is registered on an application.
See :doc:`/blueprints` for more information.
"""
def __init__(
self,
name: str,
@ -77,7 +92,7 @@ class Blueprint(SansioBlueprint):
if isinstance(value, timedelta):
return int(value.total_seconds())
return value # type: ignore[no-any-return]
return t.cast(int, value)
def send_static_file(self, filename: str) -> Response:
"""The view function used to serve files from

View file

@ -18,7 +18,12 @@ T = t.TypeVar("T")
class ConfigAttribute(t.Generic[T]):
"""Makes an attribute forward to the config"""
"""Makes an attribute forward to the config.
This descriptor allows accessing configuration values as attributes on
the application object. It can optionally convert the value using a
converter function.
"""
def __init__(
self, name: str, get_converter: t.Callable[[t.Any], T] | None = None