fresco 3.3.4__py3-none-any.whl → 3.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fresco/__init__.py +55 -56
- fresco/core.py +39 -27
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +188 -70
- fresco/py.typed +0 -0
- fresco/request.py +157 -36
- fresco/requestcontext.py +3 -0
- fresco/response.py +66 -57
- fresco/routeargs.py +23 -9
- fresco/routing.py +152 -74
- fresco/static.py +1 -1
- fresco/subrequests.py +3 -5
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +59 -15
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_response.py +16 -0
- fresco/tests/test_routing.py +113 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/tests/util/test_urls.py +20 -0
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +44 -12
- fresco/util/wsgi.py +15 -14
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/METADATA +4 -4
- fresco-3.9.0.dist-info/RECORD +58 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.3.4.dist-info/RECORD +0 -57
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/top_level.txt +0 -0
fresco/routing.py
CHANGED
|
@@ -14,22 +14,21 @@
|
|
|
14
14
|
#
|
|
15
15
|
import re
|
|
16
16
|
import sys
|
|
17
|
+
import inspect
|
|
17
18
|
import warnings
|
|
18
19
|
from copy import copy
|
|
19
20
|
from collections import defaultdict
|
|
20
21
|
from collections import namedtuple
|
|
22
|
+
from collections.abc import Collection
|
|
21
23
|
from collections.abc import MutableSequence
|
|
24
|
+
from collections.abc import Sequence
|
|
22
25
|
from importlib import import_module
|
|
23
26
|
from functools import partial
|
|
24
27
|
from typing import Any
|
|
25
28
|
from typing import Callable
|
|
26
|
-
from typing import Dict
|
|
27
|
-
from typing import List
|
|
28
29
|
from typing import Mapping
|
|
29
30
|
from typing import Optional
|
|
30
31
|
from typing import Union
|
|
31
|
-
from typing import Set
|
|
32
|
-
from typing import Tuple
|
|
33
32
|
from weakref import WeakKeyDictionary
|
|
34
33
|
import typing as t
|
|
35
34
|
|
|
@@ -38,6 +37,8 @@ from fresco.response import Response
|
|
|
38
37
|
from fresco.request import Request
|
|
39
38
|
from fresco.requestcontext import context
|
|
40
39
|
from fresco.routeargs import RouteArg
|
|
40
|
+
from fresco.types import WSGIApplication
|
|
41
|
+
from fresco.types import ViewCallable
|
|
41
42
|
from fresco.util.cache import make_cache
|
|
42
43
|
from fresco.util.common import fq_path
|
|
43
44
|
from fresco.util.urls import join_path
|
|
@@ -107,6 +108,9 @@ ALL_METHODS = HTTP_METHODS = set(
|
|
|
107
108
|
+ RFC5789_METHODS
|
|
108
109
|
)
|
|
109
110
|
|
|
111
|
+
#: Shortcut for specifying all methods as a kwarg
|
|
112
|
+
ALL = "ALL"
|
|
113
|
+
|
|
110
114
|
__all__ = [
|
|
111
115
|
"ALL_METHODS",
|
|
112
116
|
"Pattern",
|
|
@@ -125,7 +129,7 @@ PathMatch = namedtuple(
|
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
class RouteTraversal(
|
|
128
|
-
namedtuple("RouteTraversal", "route args kwargs collections_traversed")
|
|
132
|
+
namedtuple("RouteTraversal", "route view args kwargs collections_traversed")
|
|
129
133
|
):
|
|
130
134
|
"""
|
|
131
135
|
Encapsulate a route traversal.
|
|
@@ -134,6 +138,7 @@ class RouteTraversal(
|
|
|
134
138
|
|
|
135
139
|
- ``route`` the final route traversed, allowing access to the view
|
|
136
140
|
associated with the path.
|
|
141
|
+
- ``view``, the resolved view callable
|
|
137
142
|
- ``args`` - positional args to be passed to the view. This is a
|
|
138
143
|
combination of args extracted from the path and any added when
|
|
139
144
|
constructing the route.
|
|
@@ -172,6 +177,7 @@ class RouteTraversal(
|
|
|
172
177
|
else:
|
|
173
178
|
viewspecs = [viewspec]
|
|
174
179
|
collections_traversed_iter = iter(self.collections_traversed)
|
|
180
|
+
route = None
|
|
175
181
|
for item in viewspecs:
|
|
176
182
|
while True:
|
|
177
183
|
ct = next(collections_traversed_iter, None)
|
|
@@ -203,7 +209,7 @@ class RouteTraversal(
|
|
|
203
209
|
#: An item of RouteTraversal.collections_traversed
|
|
204
210
|
TraversedCollection = namedtuple(
|
|
205
211
|
"TraversedCollection",
|
|
206
|
-
"collection path route args kwargs
|
|
212
|
+
"collection path route args kwargs traversal_args traversal_kwargs",
|
|
207
213
|
)
|
|
208
214
|
|
|
209
215
|
|
|
@@ -241,7 +247,7 @@ class Pattern(object):
|
|
|
241
247
|
URL path.
|
|
242
248
|
"""
|
|
243
249
|
|
|
244
|
-
segments:
|
|
250
|
+
segments: list["PatternSegment"]
|
|
245
251
|
|
|
246
252
|
def match(self, path):
|
|
247
253
|
"""
|
|
@@ -274,8 +280,8 @@ class Pattern(object):
|
|
|
274
280
|
raise NotImplementedError()
|
|
275
281
|
|
|
276
282
|
|
|
277
|
-
class Converter
|
|
278
|
-
"""
|
|
283
|
+
class Converter:
|
|
284
|
+
"""
|
|
279
285
|
Responsible for converting arguments to and from URL components.
|
|
280
286
|
|
|
281
287
|
A ``Converter`` class should provide two instance methods:
|
|
@@ -333,11 +339,11 @@ class StrConverter(Converter):
|
|
|
333
339
|
|
|
334
340
|
pattern = r"[^/]+"
|
|
335
341
|
|
|
336
|
-
def to_string(self,
|
|
342
|
+
def to_string(self, ob):
|
|
337
343
|
"""
|
|
338
344
|
Return ``s`` converted to an ``str`` object.
|
|
339
345
|
"""
|
|
340
|
-
return
|
|
346
|
+
return ob
|
|
341
347
|
|
|
342
348
|
def from_string(self, s):
|
|
343
349
|
"""
|
|
@@ -514,7 +520,15 @@ class ExtensiblePattern(Pattern):
|
|
|
514
520
|
|
|
515
521
|
self.segments = list(self._make_segments())
|
|
516
522
|
self.args = [item for item in self.segments if item.converter is not None]
|
|
517
|
-
|
|
523
|
+
self.segments_from_string = [
|
|
524
|
+
(s.name, s.converter.from_string)
|
|
525
|
+
for s in self.segments
|
|
526
|
+
if s.converter is not None
|
|
527
|
+
]
|
|
528
|
+
self.positional_args = tuple(a.converter for a in self.args if a.name is None)
|
|
529
|
+
self.keyword_args = {
|
|
530
|
+
a.name: a.converter for a in self.args if a.name is not None
|
|
531
|
+
}
|
|
518
532
|
regex = "".join(segment.regex for segment in self.segments)
|
|
519
533
|
if self.match_entire_path:
|
|
520
534
|
regex += "$"
|
|
@@ -525,9 +539,7 @@ class ExtensiblePattern(Pattern):
|
|
|
525
539
|
self.regex_match = self.regex.match
|
|
526
540
|
|
|
527
541
|
def path_argument_info(self):
|
|
528
|
-
|
|
529
|
-
keyword = {a.name: a.converter for a in self.args if a.name is not None}
|
|
530
|
-
return (positional, keyword)
|
|
542
|
+
return (self.positional_args, self.keyword_args)
|
|
531
543
|
|
|
532
544
|
def _make_segments(self):
|
|
533
545
|
r"""
|
|
@@ -590,8 +602,8 @@ class ExtensiblePattern(Pattern):
|
|
|
590
602
|
|
|
591
603
|
try:
|
|
592
604
|
group_items = [
|
|
593
|
-
(
|
|
594
|
-
for value,
|
|
605
|
+
(name, from_string(value))
|
|
606
|
+
for value, (name, from_string) in zip(groups, self.segments_from_string)
|
|
595
607
|
]
|
|
596
608
|
except ValueError:
|
|
597
609
|
return None
|
|
@@ -601,7 +613,9 @@ class ExtensiblePattern(Pattern):
|
|
|
601
613
|
kwargs = {name: value for name, value in group_items if name}
|
|
602
614
|
return PathMatch(matched, path[len(matched) :], args, kwargs)
|
|
603
615
|
|
|
604
|
-
def pathfor(
|
|
616
|
+
def pathfor(
|
|
617
|
+
self, *args, _strjoin="".join, **kwargs
|
|
618
|
+
) -> tuple[str, list[Any], dict[Any, Any]]:
|
|
605
619
|
"""
|
|
606
620
|
Example usage::
|
|
607
621
|
|
|
@@ -615,8 +629,7 @@ class ExtensiblePattern(Pattern):
|
|
|
615
629
|
"""
|
|
616
630
|
|
|
617
631
|
arg_list = list(args)
|
|
618
|
-
|
|
619
|
-
result: List[str] = []
|
|
632
|
+
result: list[str] = []
|
|
620
633
|
result_append = result.append
|
|
621
634
|
for seg in self.segments:
|
|
622
635
|
if not seg.converter:
|
|
@@ -625,7 +638,7 @@ class ExtensiblePattern(Pattern):
|
|
|
625
638
|
elif seg.name:
|
|
626
639
|
try:
|
|
627
640
|
value = kwargs.pop(seg.name)
|
|
628
|
-
except
|
|
641
|
+
except KeyError:
|
|
629
642
|
raise URLGenerationError(
|
|
630
643
|
"Argument %r not specified for url %r"
|
|
631
644
|
% (seg.name, self.pattern)
|
|
@@ -637,11 +650,11 @@ class ExtensiblePattern(Pattern):
|
|
|
637
650
|
value = arg_list.pop(0)
|
|
638
651
|
except IndexError:
|
|
639
652
|
raise URLGenerationError(
|
|
640
|
-
"Not enough positional arguments for url
|
|
653
|
+
f"Not enough positional arguments for url {self.pattern}"
|
|
641
654
|
)
|
|
642
655
|
result_append(seg.converter.to_string(value))
|
|
643
656
|
|
|
644
|
-
return
|
|
657
|
+
return _strjoin(result), arg_list, kwargs
|
|
645
658
|
|
|
646
659
|
def add_prefix(self, prefix):
|
|
647
660
|
return self.__class__(join_path(prefix, self.pattern), self.match_entire_path)
|
|
@@ -674,7 +687,7 @@ class ExtensiblePattern(Pattern):
|
|
|
674
687
|
return "%s" % (self.pattern,)
|
|
675
688
|
|
|
676
689
|
|
|
677
|
-
class PatternSegment
|
|
690
|
+
class PatternSegment:
|
|
678
691
|
"""
|
|
679
692
|
Represent a single segment of a URL pattern, storing information about the
|
|
680
693
|
``source``, ``regex`` used to pattern match the segment, ``name`` for
|
|
@@ -684,7 +697,13 @@ class PatternSegment(object):
|
|
|
684
697
|
|
|
685
698
|
__slots__ = ["source", "regex", "name", "converter"]
|
|
686
699
|
|
|
687
|
-
def __init__(
|
|
700
|
+
def __init__(
|
|
701
|
+
self,
|
|
702
|
+
source: str,
|
|
703
|
+
regex: str,
|
|
704
|
+
name: Optional[str],
|
|
705
|
+
converter: Optional[Converter],
|
|
706
|
+
):
|
|
688
707
|
self.source = source
|
|
689
708
|
self.regex = regex
|
|
690
709
|
self.name = name
|
|
@@ -699,35 +718,36 @@ class Route(object):
|
|
|
699
718
|
#: The default class to use for URL pattern matching
|
|
700
719
|
pattern_class = ExtensiblePattern
|
|
701
720
|
|
|
702
|
-
fallthrough_statuses: Optional[
|
|
721
|
+
fallthrough_statuses: Optional[set[int]]
|
|
703
722
|
|
|
704
|
-
_route_hints:
|
|
723
|
+
_route_hints: dict[Callable, dict[str, list[Callable]]] = defaultdict(
|
|
705
724
|
lambda: defaultdict(list)
|
|
706
725
|
)
|
|
707
726
|
|
|
708
|
-
|
|
709
|
-
provide_request = False
|
|
727
|
+
provide_request: Optional[bool] = None
|
|
710
728
|
|
|
711
729
|
def __init__(
|
|
712
730
|
self,
|
|
713
731
|
pattern: t.Union[str, Pattern],
|
|
714
|
-
methods=None,
|
|
715
|
-
view=None,
|
|
716
|
-
kwargs=None,
|
|
732
|
+
methods: Optional[Collection[str]] = None,
|
|
733
|
+
view: Optional[Union[ViewCallable, "RouteCollection", str]] = None,
|
|
734
|
+
kwargs: Optional[dict[str, Any]] = None,
|
|
717
735
|
args=None,
|
|
718
|
-
name=None,
|
|
736
|
+
name: Optional[str] = None,
|
|
719
737
|
predicate=None,
|
|
720
738
|
decorators=None,
|
|
721
739
|
filters=None,
|
|
722
|
-
fallthrough_on=None,
|
|
740
|
+
fallthrough_on: Optional[Sequence[int]] = None,
|
|
723
741
|
provide_request: Optional[bool] = None,
|
|
724
|
-
**_kwargs,
|
|
742
|
+
**_kwargs: ViewCallable,
|
|
725
743
|
):
|
|
726
744
|
"""
|
|
727
745
|
:param pattern: A string that can be compiled into a path pattern
|
|
728
746
|
:param methods: The list of HTTP methods the view is bound to
|
|
729
747
|
('GET', 'POST', etc)
|
|
730
|
-
:param view:
|
|
748
|
+
:param view:
|
|
749
|
+
The view function, or a string identifier which will later be resolved.
|
|
750
|
+
|
|
731
751
|
:param kwargs: A dictionary of default keyword arguments to pass
|
|
732
752
|
to the view callable
|
|
733
753
|
:param args: Positional arguments to pass to the view callable
|
|
@@ -740,16 +760,19 @@ class Route(object):
|
|
|
740
760
|
before invoking it
|
|
741
761
|
:param filters: Filter functions to apply to the view's return
|
|
742
762
|
value before returning the final response object
|
|
743
|
-
:param fallthrough_on: A
|
|
763
|
+
:param fallthrough_on: A list of http status codes which, if returned
|
|
744
764
|
by a view will cause the current response to be
|
|
745
765
|
discarded with routing continuing to the next
|
|
746
766
|
available route.
|
|
747
|
-
:param provide_request:
|
|
748
|
-
|
|
767
|
+
:param provide_request:
|
|
768
|
+
If True, provide the current request as the first argument to the
|
|
769
|
+
view callable. Defaults to ``None``, which will autodetect if the
|
|
770
|
+
view function has an initial parameter of type
|
|
771
|
+
:class:`~fresco.request.Request`
|
|
749
772
|
:param **_kwargs: Keyword arguments matching HTTP method names
|
|
750
773
|
(GET, POST etc) can used to specify views
|
|
751
774
|
associated with those methods.
|
|
752
|
-
Other keyword
|
|
775
|
+
Other keyword arguments are passed through to the
|
|
753
776
|
view callable.
|
|
754
777
|
|
|
755
778
|
Naming routes
|
|
@@ -792,8 +815,9 @@ class Route(object):
|
|
|
792
815
|
'http://localhost/thumbnail'
|
|
793
816
|
|
|
794
817
|
"""
|
|
795
|
-
method_view_map:
|
|
818
|
+
method_view_map: dict[str, t.Union[ViewCallable, RouteCollection, str]] = {}
|
|
796
819
|
if methods:
|
|
820
|
+
assert view is not None
|
|
797
821
|
if isinstance(methods, str):
|
|
798
822
|
methods = [methods]
|
|
799
823
|
else:
|
|
@@ -803,13 +827,25 @@ class Route(object):
|
|
|
803
827
|
"HTTP methods must be specified as a string or iterable"
|
|
804
828
|
)
|
|
805
829
|
for m in methods:
|
|
830
|
+
if m == ALL:
|
|
831
|
+
methods = ALL_METHODS
|
|
832
|
+
break
|
|
806
833
|
if m not in ALL_METHODS:
|
|
807
834
|
raise ValueError("{!r} is not a valid HTTP method".format(m))
|
|
808
835
|
method_view_map.update((m, view) for m in methods)
|
|
809
836
|
|
|
837
|
+
if view := _kwargs.pop("ALL", None):
|
|
838
|
+
method_view_map |= {m: view for m in ALL_METHODS}
|
|
839
|
+
|
|
810
840
|
for method in ALL_METHODS:
|
|
811
841
|
if method in _kwargs:
|
|
812
842
|
method_view_map[method] = _kwargs.pop(method)
|
|
843
|
+
if _kwargs:
|
|
844
|
+
for k in list(_kwargs):
|
|
845
|
+
trymethods = k.split("_")
|
|
846
|
+
if all(m in ALL_METHODS for m in trymethods):
|
|
847
|
+
method_view_map.update((m, _kwargs[k]) for m in trymethods)
|
|
848
|
+
_kwargs.pop(k)
|
|
813
849
|
|
|
814
850
|
if not isinstance(pattern, Pattern):
|
|
815
851
|
pattern = self.pattern_class(pattern)
|
|
@@ -821,7 +857,7 @@ class Route(object):
|
|
|
821
857
|
self.name = name
|
|
822
858
|
self.predicate = predicate
|
|
823
859
|
self.decorators = decorators or []
|
|
824
|
-
self.before_hooks:
|
|
860
|
+
self.before_hooks: list[Callable] = []
|
|
825
861
|
self.filters = filters or []
|
|
826
862
|
if fallthrough_on:
|
|
827
863
|
self.fallthrough_statuses = {int(i) for i in fallthrough_on}
|
|
@@ -832,14 +868,14 @@ class Route(object):
|
|
|
832
868
|
self.provide_request = provide_request
|
|
833
869
|
|
|
834
870
|
#: Default values to use for path generation
|
|
835
|
-
self.routed_args_default:
|
|
871
|
+
self.routed_args_default: dict[str, Any] = {}
|
|
836
872
|
|
|
837
873
|
self.pattern = pattern
|
|
838
874
|
self.methods = set(method_view_map)
|
|
839
875
|
self.instance = None
|
|
840
876
|
|
|
841
877
|
# Cached references to view functions
|
|
842
|
-
self._cached_views:
|
|
878
|
+
self._cached_views: dict[str, ViewCallable] = {}
|
|
843
879
|
|
|
844
880
|
# Cached references to decorated view function. We use weakrefs in case
|
|
845
881
|
# a process_view hook substitutes the view function used as a key
|
|
@@ -852,8 +888,12 @@ class Route(object):
|
|
|
852
888
|
if default is not _marker:
|
|
853
889
|
self.routed_args_default[k] = default
|
|
854
890
|
|
|
855
|
-
self.view_args
|
|
856
|
-
|
|
891
|
+
self.view_args: tuple[Any] = tuple(
|
|
892
|
+
args or _kwargs.pop("view_args", tuple()) # type: ignore
|
|
893
|
+
)
|
|
894
|
+
self.view_kwargs: dict[str, Any] = dict(
|
|
895
|
+
kwargs or _kwargs.pop("view_kwargs", {}), **_kwargs # type: ignore
|
|
896
|
+
)
|
|
857
897
|
|
|
858
898
|
for arg in self.view_args:
|
|
859
899
|
if isinstance(arg, RouteArg):
|
|
@@ -870,7 +910,9 @@ class Route(object):
|
|
|
870
910
|
self.viewspecs = method_view_map
|
|
871
911
|
|
|
872
912
|
def __repr__(self):
|
|
873
|
-
view_methods_map: Mapping[
|
|
913
|
+
view_methods_map: Mapping[
|
|
914
|
+
t.Union[RouteCollection, ViewCallable, str], set[str]
|
|
915
|
+
] = defaultdict(set)
|
|
874
916
|
for method, viewspec in self.viewspecs.items():
|
|
875
917
|
view_methods_map[viewspec].add(method)
|
|
876
918
|
|
|
@@ -895,14 +937,14 @@ class Route(object):
|
|
|
895
937
|
newroute.fallthrough_statuses = {int(s) for s in status_codes}
|
|
896
938
|
return newroute
|
|
897
939
|
|
|
898
|
-
def match(self, path, method):
|
|
940
|
+
def match(self, path: str, method: t.Optional[str]) -> t.Optional[PathMatch]:
|
|
899
941
|
if method and method not in self.methods:
|
|
900
942
|
return None
|
|
901
943
|
return self.pattern.match(path)
|
|
902
944
|
|
|
903
|
-
def getview(self, method: str) ->
|
|
904
|
-
"""
|
|
905
|
-
|
|
945
|
+
def getview(self, method: str) -> ViewCallable:
|
|
946
|
+
"""
|
|
947
|
+
Resolve and return the raw view callable.
|
|
906
948
|
"""
|
|
907
949
|
try:
|
|
908
950
|
return self._cached_views[method]
|
|
@@ -921,8 +963,14 @@ class Route(object):
|
|
|
921
963
|
raise RouteNotReady()
|
|
922
964
|
uview = getattr(self.instance, uview)
|
|
923
965
|
|
|
924
|
-
self._cached_views[method] = uview
|
|
925
|
-
|
|
966
|
+
self._cached_views[method] = uview # type: ignore
|
|
967
|
+
if self.provide_request is None:
|
|
968
|
+
if callable(uview):
|
|
969
|
+
self.provide_request = _has_request_parameter(uview)
|
|
970
|
+
else:
|
|
971
|
+
self.provide_request = False
|
|
972
|
+
|
|
973
|
+
return uview # type: ignore
|
|
926
974
|
|
|
927
975
|
@classmethod
|
|
928
976
|
def _add_route_hint(cls, viewfunc, hinttype, func):
|
|
@@ -1157,7 +1205,7 @@ class RRoute(Route):
|
|
|
1157
1205
|
|
|
1158
1206
|
def split_iter(pattern, string):
|
|
1159
1207
|
"""
|
|
1160
|
-
Generate alternate strings and match objects for all
|
|
1208
|
+
Generate alternate strings and match objects for all occurences of
|
|
1161
1209
|
``pattern`` in ``string``.
|
|
1162
1210
|
"""
|
|
1163
1211
|
matcher = pattern.finditer(string)
|
|
@@ -1197,10 +1245,8 @@ class RouteCollection(MutableSequence):
|
|
|
1197
1245
|
_route_cache = None
|
|
1198
1246
|
|
|
1199
1247
|
def __init__(self, routes=None, route_class=None, cache=True):
|
|
1200
|
-
self.__routes__:
|
|
1201
|
-
self.__routed_views__:
|
|
1202
|
-
Union[str, Callable], Union[Route, RouteNotFound]
|
|
1203
|
-
] = {}
|
|
1248
|
+
self.__routes__: list[Route] = []
|
|
1249
|
+
self.__routed_views__: dict[Any, Union[Route, RouteNotFound]] = {}
|
|
1204
1250
|
if cache:
|
|
1205
1251
|
self.reinit_route_cache()
|
|
1206
1252
|
self.route_class = route_class or self.route_class
|
|
@@ -1265,8 +1311,8 @@ class RouteCollection(MutableSequence):
|
|
|
1265
1311
|
)
|
|
1266
1312
|
self._route_cache = make_cache(self._get_routes, cache_size)
|
|
1267
1313
|
|
|
1268
|
-
def insert(self,
|
|
1269
|
-
self.__routes__.insert(
|
|
1314
|
+
def insert(self, index, value):
|
|
1315
|
+
self.__routes__.insert(index, value)
|
|
1270
1316
|
if self._route_cache is not None:
|
|
1271
1317
|
self.reinit_route_cache()
|
|
1272
1318
|
|
|
@@ -1385,8 +1431,8 @@ class RouteCollection(MutableSequence):
|
|
|
1385
1431
|
raise exc
|
|
1386
1432
|
|
|
1387
1433
|
def _get_routes(
|
|
1388
|
-
self, key:
|
|
1389
|
-
) -> t.Sequence[
|
|
1434
|
+
self, key: tuple[t.Optional[str], str]
|
|
1435
|
+
) -> t.Sequence[tuple[Route, PathMatch]]:
|
|
1390
1436
|
method, path = key
|
|
1391
1437
|
routes = ((r, r.match(path, method)) for r in self.__routes__)
|
|
1392
1438
|
return [(r, t) for (r, t) in routes if t is not None]
|
|
@@ -1428,6 +1474,13 @@ class RouteCollection(MutableSequence):
|
|
|
1428
1474
|
|
|
1429
1475
|
# View function arguments extracted while traversing the path
|
|
1430
1476
|
traversal_args, traversal_kwargs = result.args, result.kwargs
|
|
1477
|
+
if method is None:
|
|
1478
|
+
view = None
|
|
1479
|
+
for m in route.viewspecs:
|
|
1480
|
+
view = route.getview(m)
|
|
1481
|
+
break
|
|
1482
|
+
else:
|
|
1483
|
+
view = route.getview(method)
|
|
1431
1484
|
|
|
1432
1485
|
# Process any args/kwargs defined in the Route declaration.
|
|
1433
1486
|
if request:
|
|
@@ -1459,7 +1512,7 @@ class RouteCollection(MutableSequence):
|
|
|
1459
1512
|
raise exc
|
|
1460
1513
|
|
|
1461
1514
|
r = self.route_class("/", ALL_METHODS, raiser)
|
|
1462
|
-
yield RouteTraversal(r, (), {}, [(self, "", r)])
|
|
1515
|
+
yield RouteTraversal(r, r.getview("GET"), (), {}, [(self, "", r)])
|
|
1463
1516
|
continue
|
|
1464
1517
|
|
|
1465
1518
|
for sub in sub_routes.get_route_traversals(
|
|
@@ -1484,17 +1537,21 @@ class RouteCollection(MutableSequence):
|
|
|
1484
1537
|
# Dynamic routes consume their arguments when creating the
|
|
1485
1538
|
# sub RouteCollection.
|
|
1486
1539
|
if route.dynamic:
|
|
1487
|
-
yield RouteTraversal(
|
|
1540
|
+
yield RouteTraversal(
|
|
1541
|
+
sub.route, sub.view, sub.args, sub.kwargs, traversed
|
|
1542
|
+
)
|
|
1488
1543
|
else:
|
|
1489
1544
|
yield RouteTraversal(
|
|
1490
1545
|
sub.route,
|
|
1546
|
+
sub.view,
|
|
1491
1547
|
args + sub.args,
|
|
1492
|
-
|
|
1548
|
+
kwargs | sub.kwargs,
|
|
1493
1549
|
traversed,
|
|
1494
1550
|
)
|
|
1495
1551
|
else:
|
|
1496
1552
|
yield RouteTraversal(
|
|
1497
1553
|
route,
|
|
1554
|
+
view,
|
|
1498
1555
|
args,
|
|
1499
1556
|
kwargs,
|
|
1500
1557
|
[
|
|
@@ -1523,8 +1580,8 @@ class RouteCollection(MutableSequence):
|
|
|
1523
1580
|
def route(
|
|
1524
1581
|
self,
|
|
1525
1582
|
pattern: t.Union[str, Pattern],
|
|
1526
|
-
methods: t.Optional[t.Union[str, t.
|
|
1527
|
-
view: t.Optional[t.Union[str,
|
|
1583
|
+
methods: t.Optional[t.Union[str, t.Collection[str]]] = None,
|
|
1584
|
+
view: t.Optional[t.Union[str, ViewCallable]] = None,
|
|
1528
1585
|
*args,
|
|
1529
1586
|
route_class: t.Optional[t.Type[Route]] = None,
|
|
1530
1587
|
**kwargs,
|
|
@@ -1563,7 +1620,7 @@ class RouteCollection(MutableSequence):
|
|
|
1563
1620
|
def route_wsgi(
|
|
1564
1621
|
self,
|
|
1565
1622
|
path: str,
|
|
1566
|
-
wsgiapp: t.Union[
|
|
1623
|
+
wsgiapp: t.Union[WSGIApplication, str],
|
|
1567
1624
|
rewrite_script_name: bool = True,
|
|
1568
1625
|
*args,
|
|
1569
1626
|
**kwargs,
|
|
@@ -1591,7 +1648,7 @@ class RouteCollection(MutableSequence):
|
|
|
1591
1648
|
resolved_wsgi_app = None
|
|
1592
1649
|
|
|
1593
1650
|
def fresco_wsgi_view(
|
|
1594
|
-
request,
|
|
1651
|
+
request: Request,
|
|
1595
1652
|
path=path,
|
|
1596
1653
|
ws_path=ws_path,
|
|
1597
1654
|
rewrite_script_name=rewrite_script_name,
|
|
@@ -1699,7 +1756,7 @@ class RouteCollection(MutableSequence):
|
|
|
1699
1756
|
if r is route:
|
|
1700
1757
|
del self.__routed_views__[k]
|
|
1701
1758
|
|
|
1702
|
-
def remove(self,
|
|
1759
|
+
def remove(self, value):
|
|
1703
1760
|
"""
|
|
1704
1761
|
Remove the route(s) identified by ``viewspec``
|
|
1705
1762
|
|
|
@@ -1707,7 +1764,7 @@ class RouteCollection(MutableSequence):
|
|
|
1707
1764
|
('package.module.view_function'), or the name of a
|
|
1708
1765
|
named route
|
|
1709
1766
|
"""
|
|
1710
|
-
self.replace(
|
|
1767
|
+
self.replace(value, None)
|
|
1711
1768
|
|
|
1712
1769
|
|
|
1713
1770
|
class DelegateRoute(Route):
|
|
@@ -1730,7 +1787,7 @@ class DelegateRoute(Route):
|
|
|
1730
1787
|
view = RouteCollection(routes)
|
|
1731
1788
|
super(DelegateRoute, self).__init__(pattern, ALL_METHODS, view, *args, **kwargs)
|
|
1732
1789
|
|
|
1733
|
-
def _dynamic_routecollectionfactory(self, *args, **kwargs):
|
|
1790
|
+
def _dynamic_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
|
|
1734
1791
|
"""\
|
|
1735
1792
|
Return the RouteCollection responsible for paths under this route
|
|
1736
1793
|
"""
|
|
@@ -1744,8 +1801,8 @@ class DelegateRoute(Route):
|
|
|
1744
1801
|
(r.bindto(routes) for r in routes.__routes__), cache=False
|
|
1745
1802
|
)
|
|
1746
1803
|
|
|
1747
|
-
def _static_routecollectionfactory(self, *args, **kwargs):
|
|
1748
|
-
return self.getview(GET)
|
|
1804
|
+
def _static_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
|
|
1805
|
+
return self.getview(GET) # type: ignore
|
|
1749
1806
|
|
|
1750
1807
|
|
|
1751
1808
|
def register_converter(name, registry=ExtensiblePattern):
|
|
@@ -1766,3 +1823,24 @@ def register_converter(name, registry=ExtensiblePattern):
|
|
|
1766
1823
|
return cls
|
|
1767
1824
|
|
|
1768
1825
|
return register_converter
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def _has_request_parameter(func: Callable[..., Any]) -> bool:
|
|
1829
|
+
"""
|
|
1830
|
+
Return True if the given function has an initial parameter of type Request
|
|
1831
|
+
"""
|
|
1832
|
+
|
|
1833
|
+
def _is_request_annotation(a: Any) -> bool:
|
|
1834
|
+
if isinstance(a, type) and issubclass(a, Request):
|
|
1835
|
+
return True
|
|
1836
|
+
|
|
1837
|
+
origin = t.get_origin(a)
|
|
1838
|
+
if origin is t.Union and any(
|
|
1839
|
+
_is_request_annotation(arg) for arg in t.get_args(a)
|
|
1840
|
+
):
|
|
1841
|
+
return True
|
|
1842
|
+
return False
|
|
1843
|
+
|
|
1844
|
+
sig = inspect.signature(func)
|
|
1845
|
+
firstparam = next(iter(sig.parameters.values()), None)
|
|
1846
|
+
return bool(firstparam and _is_request_annotation(firstparam.annotation))
|
fresco/static.py
CHANGED
|
@@ -57,7 +57,7 @@ def serve_static_file(path, content_type=None, bufsize=8192, **kwargs):
|
|
|
57
57
|
file_wrapper = request.environ.get("wsgi.file_wrapper")
|
|
58
58
|
if file_wrapper is not None:
|
|
59
59
|
|
|
60
|
-
def content_iterator(f):
|
|
60
|
+
def content_iterator(f): # type: ignore
|
|
61
61
|
return file_wrapper(f, bufsize)
|
|
62
62
|
|
|
63
63
|
else:
|
fresco/subrequests.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from itertools import chain
|
|
2
2
|
from typing import Any
|
|
3
|
-
from typing import Dict
|
|
4
|
-
from typing import Tuple
|
|
5
3
|
|
|
6
4
|
from fresco.core import context
|
|
7
5
|
from fresco.request import Request
|
|
@@ -174,7 +172,7 @@ def subrequest_raw(view, *args, **kwargs) -> Response:
|
|
|
174
172
|
|
|
175
173
|
response = view(*args, **kwargs)
|
|
176
174
|
|
|
177
|
-
return response
|
|
175
|
+
return response # type: ignore
|
|
178
176
|
finally:
|
|
179
177
|
context.pop()
|
|
180
178
|
|
|
@@ -262,8 +260,8 @@ def resolve_viewspec(viewspec, *args, **kwargs):
|
|
|
262
260
|
|
|
263
261
|
|
|
264
262
|
def _get_args_for_route(
|
|
265
|
-
route: Route, request: Request, args:
|
|
266
|
-
) ->
|
|
263
|
+
route: Route, request: Request, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
264
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
267
265
|
"""
|
|
268
266
|
Return the args/kwargs required to pass to the view callable for ``route``.
|
|
269
267
|
"""
|
fresco/tests/test_core.py
CHANGED
|
@@ -178,10 +178,10 @@ class TestFrescoApp(object):
|
|
|
178
178
|
|
|
179
179
|
def test_get_methods_matches_on_path(self):
|
|
180
180
|
app = FrescoApp()
|
|
181
|
-
app.route("/1", POST,
|
|
182
|
-
app.route("/1", PUT,
|
|
183
|
-
app.route("/2", GET,
|
|
184
|
-
app.route("/2", DELETE,
|
|
181
|
+
app.route("/1", POST, Response)
|
|
182
|
+
app.route("/1", PUT, Response)
|
|
183
|
+
app.route("/2", GET, Response)
|
|
184
|
+
app.route("/2", DELETE, Response)
|
|
185
185
|
|
|
186
186
|
with app.requestcontext() as c:
|
|
187
187
|
assert app.get_methods(c.request, "/1") == set([POST, PUT])
|
fresco/tests/test_multidict.py
CHANGED
|
@@ -88,7 +88,7 @@ class TestMultDict(object):
|
|
|
88
88
|
|
|
89
89
|
def test_keys(self):
|
|
90
90
|
m = MultiDict([("a", 1), ("a", 2), ("b", 3)])
|
|
91
|
-
i = m.keys()
|
|
91
|
+
i = iter(m.keys())
|
|
92
92
|
assert next(i) == "a"
|
|
93
93
|
assert next(i) == "b"
|
|
94
94
|
with pytest.raises(StopIteration):
|
|
@@ -96,7 +96,7 @@ class TestMultDict(object):
|
|
|
96
96
|
|
|
97
97
|
def test_values(self):
|
|
98
98
|
m = MultiDict([("a", 1), ("a", 2), ("b", 3)])
|
|
99
|
-
i = m.values()
|
|
99
|
+
i = iter(m.values())
|
|
100
100
|
assert next(i) == 1
|
|
101
101
|
assert next(i) == 3
|
|
102
102
|
with pytest.raises(StopIteration):
|