fresco 3.4.0__py3-none-any.whl → 3.6.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.
Potentially problematic release.
This version of fresco might be problematic. Click here for more details.
- fresco/__init__.py +55 -56
- fresco/core.py +36 -26
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +182 -70
- fresco/py.typed +0 -0
- fresco/request.py +155 -34
- fresco/requestcontext.py +3 -0
- fresco/response.py +12 -9
- fresco/routeargs.py +23 -9
- fresco/routing.py +133 -71
- 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_routing.py +86 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +13 -11
- fresco/util/wsgi.py +15 -14
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info}/METADATA +3 -2
- fresco-3.6.0.dist-info/RECORD +58 -0
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.4.0.dist-info/RECORD +0 -57
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.4.0.dist-info → fresco-3.6.0.dist-info}/top_level.txt +0 -0
fresco/routing.py
CHANGED
|
@@ -14,22 +14,20 @@
|
|
|
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
|
|
22
24
|
from importlib import import_module
|
|
23
25
|
from functools import partial
|
|
24
26
|
from typing import Any
|
|
25
27
|
from typing import Callable
|
|
26
|
-
from typing import Dict
|
|
27
|
-
from typing import List
|
|
28
28
|
from typing import Mapping
|
|
29
29
|
from typing import Optional
|
|
30
30
|
from typing import Union
|
|
31
|
-
from typing import Set
|
|
32
|
-
from typing import Tuple
|
|
33
31
|
from weakref import WeakKeyDictionary
|
|
34
32
|
import typing as t
|
|
35
33
|
|
|
@@ -38,6 +36,8 @@ from fresco.response import Response
|
|
|
38
36
|
from fresco.request import Request
|
|
39
37
|
from fresco.requestcontext import context
|
|
40
38
|
from fresco.routeargs import RouteArg
|
|
39
|
+
from fresco.types import WSGIApplication
|
|
40
|
+
from fresco.types import ViewCallable
|
|
41
41
|
from fresco.util.cache import make_cache
|
|
42
42
|
from fresco.util.common import fq_path
|
|
43
43
|
from fresco.util.urls import join_path
|
|
@@ -128,7 +128,7 @@ PathMatch = namedtuple(
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
class RouteTraversal(
|
|
131
|
-
namedtuple("RouteTraversal", "route args kwargs collections_traversed")
|
|
131
|
+
namedtuple("RouteTraversal", "route view args kwargs collections_traversed")
|
|
132
132
|
):
|
|
133
133
|
"""
|
|
134
134
|
Encapsulate a route traversal.
|
|
@@ -137,6 +137,7 @@ class RouteTraversal(
|
|
|
137
137
|
|
|
138
138
|
- ``route`` the final route traversed, allowing access to the view
|
|
139
139
|
associated with the path.
|
|
140
|
+
- ``view``, the resolved view callable
|
|
140
141
|
- ``args`` - positional args to be passed to the view. This is a
|
|
141
142
|
combination of args extracted from the path and any added when
|
|
142
143
|
constructing the route.
|
|
@@ -175,6 +176,7 @@ class RouteTraversal(
|
|
|
175
176
|
else:
|
|
176
177
|
viewspecs = [viewspec]
|
|
177
178
|
collections_traversed_iter = iter(self.collections_traversed)
|
|
179
|
+
route = None
|
|
178
180
|
for item in viewspecs:
|
|
179
181
|
while True:
|
|
180
182
|
ct = next(collections_traversed_iter, None)
|
|
@@ -206,7 +208,7 @@ class RouteTraversal(
|
|
|
206
208
|
#: An item of RouteTraversal.collections_traversed
|
|
207
209
|
TraversedCollection = namedtuple(
|
|
208
210
|
"TraversedCollection",
|
|
209
|
-
"collection path route args kwargs
|
|
211
|
+
"collection path route args kwargs traversal_args traversal_kwargs",
|
|
210
212
|
)
|
|
211
213
|
|
|
212
214
|
|
|
@@ -244,7 +246,7 @@ class Pattern(object):
|
|
|
244
246
|
URL path.
|
|
245
247
|
"""
|
|
246
248
|
|
|
247
|
-
segments:
|
|
249
|
+
segments: list["PatternSegment"]
|
|
248
250
|
|
|
249
251
|
def match(self, path):
|
|
250
252
|
"""
|
|
@@ -277,8 +279,8 @@ class Pattern(object):
|
|
|
277
279
|
raise NotImplementedError()
|
|
278
280
|
|
|
279
281
|
|
|
280
|
-
class Converter
|
|
281
|
-
"""
|
|
282
|
+
class Converter:
|
|
283
|
+
"""
|
|
282
284
|
Responsible for converting arguments to and from URL components.
|
|
283
285
|
|
|
284
286
|
A ``Converter`` class should provide two instance methods:
|
|
@@ -336,11 +338,11 @@ class StrConverter(Converter):
|
|
|
336
338
|
|
|
337
339
|
pattern = r"[^/]+"
|
|
338
340
|
|
|
339
|
-
def to_string(self,
|
|
341
|
+
def to_string(self, ob):
|
|
340
342
|
"""
|
|
341
343
|
Return ``s`` converted to an ``str`` object.
|
|
342
344
|
"""
|
|
343
|
-
return
|
|
345
|
+
return ob
|
|
344
346
|
|
|
345
347
|
def from_string(self, s):
|
|
346
348
|
"""
|
|
@@ -517,7 +519,15 @@ class ExtensiblePattern(Pattern):
|
|
|
517
519
|
|
|
518
520
|
self.segments = list(self._make_segments())
|
|
519
521
|
self.args = [item for item in self.segments if item.converter is not None]
|
|
520
|
-
|
|
522
|
+
self.segments_from_string = [
|
|
523
|
+
(s.name, s.converter.from_string)
|
|
524
|
+
for s in self.segments
|
|
525
|
+
if s.converter is not None
|
|
526
|
+
]
|
|
527
|
+
self.positional_args = tuple(a.converter for a in self.args if a.name is None)
|
|
528
|
+
self.keyword_args = {
|
|
529
|
+
a.name: a.converter for a in self.args if a.name is not None
|
|
530
|
+
}
|
|
521
531
|
regex = "".join(segment.regex for segment in self.segments)
|
|
522
532
|
if self.match_entire_path:
|
|
523
533
|
regex += "$"
|
|
@@ -528,9 +538,7 @@ class ExtensiblePattern(Pattern):
|
|
|
528
538
|
self.regex_match = self.regex.match
|
|
529
539
|
|
|
530
540
|
def path_argument_info(self):
|
|
531
|
-
|
|
532
|
-
keyword = {a.name: a.converter for a in self.args if a.name is not None}
|
|
533
|
-
return (positional, keyword)
|
|
541
|
+
return (self.positional_args, self.keyword_args)
|
|
534
542
|
|
|
535
543
|
def _make_segments(self):
|
|
536
544
|
r"""
|
|
@@ -593,8 +601,8 @@ class ExtensiblePattern(Pattern):
|
|
|
593
601
|
|
|
594
602
|
try:
|
|
595
603
|
group_items = [
|
|
596
|
-
(
|
|
597
|
-
for value,
|
|
604
|
+
(name, from_string(value))
|
|
605
|
+
for value, (name, from_string) in zip(groups, self.segments_from_string)
|
|
598
606
|
]
|
|
599
607
|
except ValueError:
|
|
600
608
|
return None
|
|
@@ -604,7 +612,9 @@ class ExtensiblePattern(Pattern):
|
|
|
604
612
|
kwargs = {name: value for name, value in group_items if name}
|
|
605
613
|
return PathMatch(matched, path[len(matched) :], args, kwargs)
|
|
606
614
|
|
|
607
|
-
def pathfor(
|
|
615
|
+
def pathfor(
|
|
616
|
+
self, *args, _strjoin="".join, **kwargs
|
|
617
|
+
) -> tuple[str, list[Any], dict[Any, Any]]:
|
|
608
618
|
"""
|
|
609
619
|
Example usage::
|
|
610
620
|
|
|
@@ -618,8 +628,7 @@ class ExtensiblePattern(Pattern):
|
|
|
618
628
|
"""
|
|
619
629
|
|
|
620
630
|
arg_list = list(args)
|
|
621
|
-
|
|
622
|
-
result: List[str] = []
|
|
631
|
+
result: list[str] = []
|
|
623
632
|
result_append = result.append
|
|
624
633
|
for seg in self.segments:
|
|
625
634
|
if not seg.converter:
|
|
@@ -628,7 +637,7 @@ class ExtensiblePattern(Pattern):
|
|
|
628
637
|
elif seg.name:
|
|
629
638
|
try:
|
|
630
639
|
value = kwargs.pop(seg.name)
|
|
631
|
-
except
|
|
640
|
+
except KeyError:
|
|
632
641
|
raise URLGenerationError(
|
|
633
642
|
"Argument %r not specified for url %r"
|
|
634
643
|
% (seg.name, self.pattern)
|
|
@@ -640,11 +649,11 @@ class ExtensiblePattern(Pattern):
|
|
|
640
649
|
value = arg_list.pop(0)
|
|
641
650
|
except IndexError:
|
|
642
651
|
raise URLGenerationError(
|
|
643
|
-
"Not enough positional arguments for url
|
|
652
|
+
f"Not enough positional arguments for url {self.pattern}"
|
|
644
653
|
)
|
|
645
654
|
result_append(seg.converter.to_string(value))
|
|
646
655
|
|
|
647
|
-
return
|
|
656
|
+
return _strjoin(result), arg_list, kwargs
|
|
648
657
|
|
|
649
658
|
def add_prefix(self, prefix):
|
|
650
659
|
return self.__class__(join_path(prefix, self.pattern), self.match_entire_path)
|
|
@@ -677,7 +686,7 @@ class ExtensiblePattern(Pattern):
|
|
|
677
686
|
return "%s" % (self.pattern,)
|
|
678
687
|
|
|
679
688
|
|
|
680
|
-
class PatternSegment
|
|
689
|
+
class PatternSegment:
|
|
681
690
|
"""
|
|
682
691
|
Represent a single segment of a URL pattern, storing information about the
|
|
683
692
|
``source``, ``regex`` used to pattern match the segment, ``name`` for
|
|
@@ -687,7 +696,13 @@ class PatternSegment(object):
|
|
|
687
696
|
|
|
688
697
|
__slots__ = ["source", "regex", "name", "converter"]
|
|
689
698
|
|
|
690
|
-
def __init__(
|
|
699
|
+
def __init__(
|
|
700
|
+
self,
|
|
701
|
+
source: str,
|
|
702
|
+
regex: str,
|
|
703
|
+
name: Optional[str],
|
|
704
|
+
converter: Optional[Converter],
|
|
705
|
+
):
|
|
691
706
|
self.source = source
|
|
692
707
|
self.regex = regex
|
|
693
708
|
self.name = name
|
|
@@ -702,21 +717,20 @@ class Route(object):
|
|
|
702
717
|
#: The default class to use for URL pattern matching
|
|
703
718
|
pattern_class = ExtensiblePattern
|
|
704
719
|
|
|
705
|
-
fallthrough_statuses: Optional[
|
|
720
|
+
fallthrough_statuses: Optional[set[int]]
|
|
706
721
|
|
|
707
|
-
_route_hints:
|
|
722
|
+
_route_hints: dict[Callable, dict[str, list[Callable]]] = defaultdict(
|
|
708
723
|
lambda: defaultdict(list)
|
|
709
724
|
)
|
|
710
725
|
|
|
711
|
-
|
|
712
|
-
provide_request = False
|
|
726
|
+
provide_request: Optional[bool] = None
|
|
713
727
|
|
|
714
728
|
def __init__(
|
|
715
729
|
self,
|
|
716
730
|
pattern: t.Union[str, Pattern],
|
|
717
|
-
methods=None,
|
|
718
|
-
view=None,
|
|
719
|
-
kwargs=None,
|
|
731
|
+
methods: Optional[Collection[str]] = None,
|
|
732
|
+
view: Optional[Union[ViewCallable, "RouteCollection", str]] = None,
|
|
733
|
+
kwargs: Optional[dict[str, Any]] = None,
|
|
720
734
|
args=None,
|
|
721
735
|
name=None,
|
|
722
736
|
predicate=None,
|
|
@@ -724,13 +738,15 @@ class Route(object):
|
|
|
724
738
|
filters=None,
|
|
725
739
|
fallthrough_on=None,
|
|
726
740
|
provide_request: Optional[bool] = None,
|
|
727
|
-
**_kwargs,
|
|
741
|
+
**_kwargs: ViewCallable,
|
|
728
742
|
):
|
|
729
743
|
"""
|
|
730
744
|
:param pattern: A string that can be compiled into a path pattern
|
|
731
745
|
:param methods: The list of HTTP methods the view is bound to
|
|
732
746
|
('GET', 'POST', etc)
|
|
733
|
-
:param view:
|
|
747
|
+
:param view:
|
|
748
|
+
The view function, or a string identifier which will later be resolved.
|
|
749
|
+
|
|
734
750
|
:param kwargs: A dictionary of default keyword arguments to pass
|
|
735
751
|
to the view callable
|
|
736
752
|
:param args: Positional arguments to pass to the view callable
|
|
@@ -743,12 +759,15 @@ class Route(object):
|
|
|
743
759
|
before invoking it
|
|
744
760
|
:param filters: Filter functions to apply to the view's return
|
|
745
761
|
value before returning the final response object
|
|
746
|
-
:param fallthrough_on: A
|
|
762
|
+
:param fallthrough_on: A list of http status codes which, if returned
|
|
747
763
|
by a view will cause the current response to be
|
|
748
764
|
discarded with routing continuing to the next
|
|
749
765
|
available route.
|
|
750
|
-
:param provide_request:
|
|
751
|
-
|
|
766
|
+
:param provide_request:
|
|
767
|
+
If True, provide the current request as the first argument to the
|
|
768
|
+
view callable. Defaults to ``None``, which will autodetect if the
|
|
769
|
+
view function has an initial parameter of type
|
|
770
|
+
:class:`~fresco.request.Request`
|
|
752
771
|
:param **_kwargs: Keyword arguments matching HTTP method names
|
|
753
772
|
(GET, POST etc) can used to specify views
|
|
754
773
|
associated with those methods.
|
|
@@ -795,8 +814,9 @@ class Route(object):
|
|
|
795
814
|
'http://localhost/thumbnail'
|
|
796
815
|
|
|
797
816
|
"""
|
|
798
|
-
method_view_map:
|
|
817
|
+
method_view_map: dict[str, t.Union[ViewCallable, RouteCollection, str]] = {}
|
|
799
818
|
if methods:
|
|
819
|
+
assert view is not None
|
|
800
820
|
if isinstance(methods, str):
|
|
801
821
|
methods = [methods]
|
|
802
822
|
else:
|
|
@@ -830,7 +850,7 @@ class Route(object):
|
|
|
830
850
|
self.name = name
|
|
831
851
|
self.predicate = predicate
|
|
832
852
|
self.decorators = decorators or []
|
|
833
|
-
self.before_hooks:
|
|
853
|
+
self.before_hooks: list[Callable] = []
|
|
834
854
|
self.filters = filters or []
|
|
835
855
|
if fallthrough_on:
|
|
836
856
|
self.fallthrough_statuses = {int(i) for i in fallthrough_on}
|
|
@@ -841,14 +861,14 @@ class Route(object):
|
|
|
841
861
|
self.provide_request = provide_request
|
|
842
862
|
|
|
843
863
|
#: Default values to use for path generation
|
|
844
|
-
self.routed_args_default:
|
|
864
|
+
self.routed_args_default: dict[str, Any] = {}
|
|
845
865
|
|
|
846
866
|
self.pattern = pattern
|
|
847
867
|
self.methods = set(method_view_map)
|
|
848
868
|
self.instance = None
|
|
849
869
|
|
|
850
870
|
# Cached references to view functions
|
|
851
|
-
self._cached_views:
|
|
871
|
+
self._cached_views: dict[str, ViewCallable] = {}
|
|
852
872
|
|
|
853
873
|
# Cached references to decorated view function. We use weakrefs in case
|
|
854
874
|
# a process_view hook substitutes the view function used as a key
|
|
@@ -861,8 +881,12 @@ class Route(object):
|
|
|
861
881
|
if default is not _marker:
|
|
862
882
|
self.routed_args_default[k] = default
|
|
863
883
|
|
|
864
|
-
self.view_args
|
|
865
|
-
|
|
884
|
+
self.view_args: tuple[Any] = tuple(
|
|
885
|
+
args or _kwargs.pop("view_args", tuple()) # type: ignore
|
|
886
|
+
)
|
|
887
|
+
self.view_kwargs: dict[str, Any] = dict(
|
|
888
|
+
kwargs or _kwargs.pop("view_kwargs", {}), **_kwargs # type: ignore
|
|
889
|
+
)
|
|
866
890
|
|
|
867
891
|
for arg in self.view_args:
|
|
868
892
|
if isinstance(arg, RouteArg):
|
|
@@ -879,7 +903,9 @@ class Route(object):
|
|
|
879
903
|
self.viewspecs = method_view_map
|
|
880
904
|
|
|
881
905
|
def __repr__(self):
|
|
882
|
-
view_methods_map: Mapping[
|
|
906
|
+
view_methods_map: Mapping[
|
|
907
|
+
t.Union[RouteCollection, ViewCallable, str], set[str]
|
|
908
|
+
] = defaultdict(set)
|
|
883
909
|
for method, viewspec in self.viewspecs.items():
|
|
884
910
|
view_methods_map[viewspec].add(method)
|
|
885
911
|
|
|
@@ -904,14 +930,14 @@ class Route(object):
|
|
|
904
930
|
newroute.fallthrough_statuses = {int(s) for s in status_codes}
|
|
905
931
|
return newroute
|
|
906
932
|
|
|
907
|
-
def match(self, path, method):
|
|
933
|
+
def match(self, path: str, method: t.Optional[str]) -> t.Optional[PathMatch]:
|
|
908
934
|
if method and method not in self.methods:
|
|
909
935
|
return None
|
|
910
936
|
return self.pattern.match(path)
|
|
911
937
|
|
|
912
|
-
def getview(self, method: str) ->
|
|
913
|
-
"""
|
|
914
|
-
|
|
938
|
+
def getview(self, method: str) -> ViewCallable:
|
|
939
|
+
"""
|
|
940
|
+
Resolve and return the raw view callable.
|
|
915
941
|
"""
|
|
916
942
|
try:
|
|
917
943
|
return self._cached_views[method]
|
|
@@ -930,8 +956,14 @@ class Route(object):
|
|
|
930
956
|
raise RouteNotReady()
|
|
931
957
|
uview = getattr(self.instance, uview)
|
|
932
958
|
|
|
933
|
-
self._cached_views[method] = uview
|
|
934
|
-
|
|
959
|
+
self._cached_views[method] = uview # type: ignore
|
|
960
|
+
if self.provide_request is None:
|
|
961
|
+
if callable(uview):
|
|
962
|
+
self.provide_request = _has_request_parameter(uview)
|
|
963
|
+
else:
|
|
964
|
+
self.provide_request = False
|
|
965
|
+
|
|
966
|
+
return uview # type: ignore
|
|
935
967
|
|
|
936
968
|
@classmethod
|
|
937
969
|
def _add_route_hint(cls, viewfunc, hinttype, func):
|
|
@@ -1166,7 +1198,7 @@ class RRoute(Route):
|
|
|
1166
1198
|
|
|
1167
1199
|
def split_iter(pattern, string):
|
|
1168
1200
|
"""
|
|
1169
|
-
Generate alternate strings and match objects for all
|
|
1201
|
+
Generate alternate strings and match objects for all occurences of
|
|
1170
1202
|
``pattern`` in ``string``.
|
|
1171
1203
|
"""
|
|
1172
1204
|
matcher = pattern.finditer(string)
|
|
@@ -1206,10 +1238,8 @@ class RouteCollection(MutableSequence):
|
|
|
1206
1238
|
_route_cache = None
|
|
1207
1239
|
|
|
1208
1240
|
def __init__(self, routes=None, route_class=None, cache=True):
|
|
1209
|
-
self.__routes__:
|
|
1210
|
-
self.__routed_views__:
|
|
1211
|
-
Union[str, Callable], Union[Route, RouteNotFound]
|
|
1212
|
-
] = {}
|
|
1241
|
+
self.__routes__: list[Route] = []
|
|
1242
|
+
self.__routed_views__: dict[Any, Union[Route, RouteNotFound]] = {}
|
|
1213
1243
|
if cache:
|
|
1214
1244
|
self.reinit_route_cache()
|
|
1215
1245
|
self.route_class = route_class or self.route_class
|
|
@@ -1274,8 +1304,8 @@ class RouteCollection(MutableSequence):
|
|
|
1274
1304
|
)
|
|
1275
1305
|
self._route_cache = make_cache(self._get_routes, cache_size)
|
|
1276
1306
|
|
|
1277
|
-
def insert(self,
|
|
1278
|
-
self.__routes__.insert(
|
|
1307
|
+
def insert(self, index, value):
|
|
1308
|
+
self.__routes__.insert(index, value)
|
|
1279
1309
|
if self._route_cache is not None:
|
|
1280
1310
|
self.reinit_route_cache()
|
|
1281
1311
|
|
|
@@ -1394,8 +1424,8 @@ class RouteCollection(MutableSequence):
|
|
|
1394
1424
|
raise exc
|
|
1395
1425
|
|
|
1396
1426
|
def _get_routes(
|
|
1397
|
-
self, key:
|
|
1398
|
-
) -> t.Sequence[
|
|
1427
|
+
self, key: tuple[t.Optional[str], str]
|
|
1428
|
+
) -> t.Sequence[tuple[Route, PathMatch]]:
|
|
1399
1429
|
method, path = key
|
|
1400
1430
|
routes = ((r, r.match(path, method)) for r in self.__routes__)
|
|
1401
1431
|
return [(r, t) for (r, t) in routes if t is not None]
|
|
@@ -1437,6 +1467,13 @@ class RouteCollection(MutableSequence):
|
|
|
1437
1467
|
|
|
1438
1468
|
# View function arguments extracted while traversing the path
|
|
1439
1469
|
traversal_args, traversal_kwargs = result.args, result.kwargs
|
|
1470
|
+
if method is None:
|
|
1471
|
+
view = None
|
|
1472
|
+
for m in route.viewspecs:
|
|
1473
|
+
view = route.getview(m)
|
|
1474
|
+
break
|
|
1475
|
+
else:
|
|
1476
|
+
view = route.getview(method)
|
|
1440
1477
|
|
|
1441
1478
|
# Process any args/kwargs defined in the Route declaration.
|
|
1442
1479
|
if request:
|
|
@@ -1468,7 +1505,7 @@ class RouteCollection(MutableSequence):
|
|
|
1468
1505
|
raise exc
|
|
1469
1506
|
|
|
1470
1507
|
r = self.route_class("/", ALL_METHODS, raiser)
|
|
1471
|
-
yield RouteTraversal(r, (), {}, [(self, "", r)])
|
|
1508
|
+
yield RouteTraversal(r, r.getview("GET"), (), {}, [(self, "", r)])
|
|
1472
1509
|
continue
|
|
1473
1510
|
|
|
1474
1511
|
for sub in sub_routes.get_route_traversals(
|
|
@@ -1493,17 +1530,21 @@ class RouteCollection(MutableSequence):
|
|
|
1493
1530
|
# Dynamic routes consume their arguments when creating the
|
|
1494
1531
|
# sub RouteCollection.
|
|
1495
1532
|
if route.dynamic:
|
|
1496
|
-
yield RouteTraversal(
|
|
1533
|
+
yield RouteTraversal(
|
|
1534
|
+
sub.route, sub.view, sub.args, sub.kwargs, traversed
|
|
1535
|
+
)
|
|
1497
1536
|
else:
|
|
1498
1537
|
yield RouteTraversal(
|
|
1499
1538
|
sub.route,
|
|
1539
|
+
sub.view,
|
|
1500
1540
|
args + sub.args,
|
|
1501
|
-
|
|
1541
|
+
kwargs | sub.kwargs,
|
|
1502
1542
|
traversed,
|
|
1503
1543
|
)
|
|
1504
1544
|
else:
|
|
1505
1545
|
yield RouteTraversal(
|
|
1506
1546
|
route,
|
|
1547
|
+
view,
|
|
1507
1548
|
args,
|
|
1508
1549
|
kwargs,
|
|
1509
1550
|
[
|
|
@@ -1532,8 +1573,8 @@ class RouteCollection(MutableSequence):
|
|
|
1532
1573
|
def route(
|
|
1533
1574
|
self,
|
|
1534
1575
|
pattern: t.Union[str, Pattern],
|
|
1535
|
-
methods: t.Optional[t.Union[str, t.
|
|
1536
|
-
view: t.Optional[t.Union[str,
|
|
1576
|
+
methods: t.Optional[t.Union[str, t.Collection[str]]] = None,
|
|
1577
|
+
view: t.Optional[t.Union[str, ViewCallable]] = None,
|
|
1537
1578
|
*args,
|
|
1538
1579
|
route_class: t.Optional[t.Type[Route]] = None,
|
|
1539
1580
|
**kwargs,
|
|
@@ -1572,7 +1613,7 @@ class RouteCollection(MutableSequence):
|
|
|
1572
1613
|
def route_wsgi(
|
|
1573
1614
|
self,
|
|
1574
1615
|
path: str,
|
|
1575
|
-
wsgiapp: t.Union[
|
|
1616
|
+
wsgiapp: t.Union[WSGIApplication, str],
|
|
1576
1617
|
rewrite_script_name: bool = True,
|
|
1577
1618
|
*args,
|
|
1578
1619
|
**kwargs,
|
|
@@ -1600,7 +1641,7 @@ class RouteCollection(MutableSequence):
|
|
|
1600
1641
|
resolved_wsgi_app = None
|
|
1601
1642
|
|
|
1602
1643
|
def fresco_wsgi_view(
|
|
1603
|
-
request,
|
|
1644
|
+
request: Request,
|
|
1604
1645
|
path=path,
|
|
1605
1646
|
ws_path=ws_path,
|
|
1606
1647
|
rewrite_script_name=rewrite_script_name,
|
|
@@ -1708,7 +1749,7 @@ class RouteCollection(MutableSequence):
|
|
|
1708
1749
|
if r is route:
|
|
1709
1750
|
del self.__routed_views__[k]
|
|
1710
1751
|
|
|
1711
|
-
def remove(self,
|
|
1752
|
+
def remove(self, value):
|
|
1712
1753
|
"""
|
|
1713
1754
|
Remove the route(s) identified by ``viewspec``
|
|
1714
1755
|
|
|
@@ -1716,7 +1757,7 @@ class RouteCollection(MutableSequence):
|
|
|
1716
1757
|
('package.module.view_function'), or the name of a
|
|
1717
1758
|
named route
|
|
1718
1759
|
"""
|
|
1719
|
-
self.replace(
|
|
1760
|
+
self.replace(value, None)
|
|
1720
1761
|
|
|
1721
1762
|
|
|
1722
1763
|
class DelegateRoute(Route):
|
|
@@ -1739,7 +1780,7 @@ class DelegateRoute(Route):
|
|
|
1739
1780
|
view = RouteCollection(routes)
|
|
1740
1781
|
super(DelegateRoute, self).__init__(pattern, ALL_METHODS, view, *args, **kwargs)
|
|
1741
1782
|
|
|
1742
|
-
def _dynamic_routecollectionfactory(self, *args, **kwargs):
|
|
1783
|
+
def _dynamic_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
|
|
1743
1784
|
"""\
|
|
1744
1785
|
Return the RouteCollection responsible for paths under this route
|
|
1745
1786
|
"""
|
|
@@ -1753,8 +1794,8 @@ class DelegateRoute(Route):
|
|
|
1753
1794
|
(r.bindto(routes) for r in routes.__routes__), cache=False
|
|
1754
1795
|
)
|
|
1755
1796
|
|
|
1756
|
-
def _static_routecollectionfactory(self, *args, **kwargs):
|
|
1757
|
-
return self.getview(GET)
|
|
1797
|
+
def _static_routecollectionfactory(self, *args, **kwargs) -> RouteCollection:
|
|
1798
|
+
return self.getview(GET) # type: ignore
|
|
1758
1799
|
|
|
1759
1800
|
|
|
1760
1801
|
def register_converter(name, registry=ExtensiblePattern):
|
|
@@ -1775,3 +1816,24 @@ def register_converter(name, registry=ExtensiblePattern):
|
|
|
1775
1816
|
return cls
|
|
1776
1817
|
|
|
1777
1818
|
return register_converter
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
def _has_request_parameter(func: Callable[..., Any]) -> bool:
|
|
1822
|
+
"""
|
|
1823
|
+
Return True if the given function has an initial parameter of type Request
|
|
1824
|
+
"""
|
|
1825
|
+
|
|
1826
|
+
def _is_request_annotation(a: Any) -> bool:
|
|
1827
|
+
if isinstance(a, type) and issubclass(a, Request):
|
|
1828
|
+
return True
|
|
1829
|
+
|
|
1830
|
+
origin = t.get_origin(a)
|
|
1831
|
+
if origin is t.Union and any(
|
|
1832
|
+
_is_request_annotation(arg) for arg in t.get_args(a)
|
|
1833
|
+
):
|
|
1834
|
+
return True
|
|
1835
|
+
return False
|
|
1836
|
+
|
|
1837
|
+
sig = inspect.signature(func)
|
|
1838
|
+
firstparam = next(iter(sig.parameters.values()), None)
|
|
1839
|
+
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):
|