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.
@@ -24,6 +24,7 @@ import sys
24
24
  import pytest
25
25
 
26
26
  from fresco.options import Options
27
+ from fresco.options import override_options
27
28
  from fresco.options import parse_key_value_pairs
28
29
  from fresco.options import dict_from_options
29
30
 
@@ -104,6 +105,24 @@ class TestOptions(object):
104
105
  assert isinstance(Options().copy(), Options)
105
106
 
106
107
 
108
+ class TestOverrideOptions:
109
+ def test_override_options_with_object(self):
110
+ options = Options(foo=1)
111
+ with override_options(options, {"foo": 2, "bar": "a"}):
112
+ assert options["foo"] == 2
113
+ assert options["bar"] == "a"
114
+ assert options["foo"] == 1
115
+ assert "bar" not in options
116
+
117
+ def test_override_options_with_kwargs(self):
118
+ options = Options(foo=1)
119
+ with override_options(options, foo=2, bar="a"):
120
+ assert options["foo"] == 2
121
+ assert options["bar"] == "a"
122
+ assert options["foo"] == 1
123
+ assert "bar" not in options
124
+
125
+
107
126
  class TestLoadKeyValuePairs:
108
127
  def test_it_loads_strings(self):
109
128
  assert parse_key_value_pairs({}, ["a=b"]) == {"a": "b"}
@@ -143,7 +162,6 @@ class TestLoadKeyValuePairs:
143
162
 
144
163
 
145
164
  class TestLoadOptions:
146
-
147
165
  def check_loadoptions(self, tmpdir, files, sources="*", tags=[], expected={}):
148
166
  """
149
167
  Write the files indicated in ``sources`` to the given temporary directory,
@@ -192,6 +210,11 @@ class TestLoadOptions:
192
210
  def test_it_loads_py_files(self, tmpdir):
193
211
  self.check_loadoptions(tmpdir, {"a.py": "x = 2 * 2"}, expected={"x": 4})
194
212
 
213
+ def test_py_files_have_options_in_namespace(self, tmpdir):
214
+ self.check_loadoptions(
215
+ tmpdir, {"a.py": "options['foo'] = 'bar'"}, expected={"foo": "bar"}
216
+ )
217
+
195
218
  def test_it_selects_by_tag(self, tmpdir):
196
219
  with self.check_loadoptions(
197
220
  tmpdir,
@@ -217,14 +240,38 @@ class TestLoadOptions:
217
240
  with self.check_loadoptions(
218
241
  tmpdir,
219
242
  {
220
- "a": "a = 0",
221
- "a.dev.txt": "a = ${a}-1",
222
- "a.local.txt": "a = ${a}-2",
223
- "b.dev.txt": "a = ${a}-3",
243
+ "a": "a = 'a'",
244
+ "a.0-dev.txt": "a = ${a} a.dev",
245
+ "a.local.txt": "a = ${a} a.local",
246
+ "b.dev.txt": "a = ${a} b.dev",
247
+ },
248
+ ) as loadopts:
249
+ assert loadopts("*", ["dev", "local"]) == {"a": "a a.dev b.dev a.local"}
250
+ assert loadopts("*", ["local", "dev"]) == {"a": "a a.local a.dev b.dev"}
251
+
252
+ def test_it_loads_in_priority_order(self, tmpdir):
253
+ with self.check_loadoptions(
254
+ tmpdir,
255
+ {
256
+ "a": "a = 'a'",
257
+ "a.100-dev.txt": "a = ${a} a.100-dev",
258
+ "a.local.txt": "a = ${a} a.local",
259
+ "b.dev.txt": "a = ${a} b.dev",
224
260
  },
225
261
  ) as loadopts:
226
- assert loadopts("*", ["dev", "local"]) == {"a": "0-1-3-2"}
227
- assert loadopts("*", ["local", "dev"]) == {"a": "0-2-1-3"}
262
+ assert loadopts("*", ["dev", "local"]) == {"a": "a b.dev a.local a.100-dev"}
263
+ assert loadopts("*", ["local", "dev"]) == {"a": "a a.local b.dev a.100-dev"}
264
+
265
+ def test_it_loads_in_priority_order_without_tags(self, tmpdir):
266
+ with self.check_loadoptions(
267
+ tmpdir,
268
+ {
269
+ "a": "a = a",
270
+ "b.100": "a = ${a} b",
271
+ "a.200.txt": "a = ${a} 100",
272
+ },
273
+ ) as loadopts:
274
+ assert loadopts("*") == {"a": "a b 100"}
228
275
 
229
276
  def test_it_loads_from_os_environ(self, tmpdir):
230
277
  with setenv(a="2"):
@@ -252,7 +299,7 @@ class TestLoadOptions:
252
299
  tmpdir,
253
300
  {"a.txt": "a=1", "b.txt": "b=1"},
254
301
  sources=["a.*", "b.*"],
255
- expected={"a": 1, "b": 1}
302
+ expected={"a": 1, "b": 1},
256
303
  )
257
304
 
258
305
  def test_it_substitutes_from_environment_variables(self, tmpdir):
@@ -261,7 +308,7 @@ class TestLoadOptions:
261
308
  tmpdir,
262
309
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
263
310
  tags=["{FOO}"],
264
- expected={"a": 2}
311
+ expected={"a": 2},
265
312
  )
266
313
 
267
314
  with setenv(FOO="baz"):
@@ -269,7 +316,7 @@ class TestLoadOptions:
269
316
  tmpdir,
270
317
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
271
318
  tags=["{FOO}"],
272
- expected={"a": 1}
319
+ expected={"a": 1},
273
320
  )
274
321
 
275
322
  def test_it_allows_missing_environment_variables(self, tmpdir):
@@ -278,19 +325,16 @@ class TestLoadOptions:
278
325
  tmpdir,
279
326
  {"a.txt": "a=1", "a.bar.txt": "a=2"},
280
327
  tags=["{FOO}"],
281
- expected={"a": 1}
328
+ expected={"a": 1},
282
329
  )
283
330
 
284
331
 
285
332
  class TestDictFromOptions:
286
-
287
333
  def test_it_splits_on_prefix(self):
288
-
289
334
  options = Options(FOO_BAR=1, FOO_BAZ=2, FOO_BAR_BAZ=3, BAR=4)
290
335
  assert dict_from_options("FOO_", options) == {"BAR": 1, "BAZ": 2, "BAR_BAZ": 3}
291
336
 
292
337
  def test_it_splits_recursively(self):
293
-
294
338
  options = Options(
295
339
  A_A=1,
296
340
  A_B_C_D=2,
@@ -303,7 +347,7 @@ class TestDictFromOptions:
303
347
  "A": 1,
304
348
  "B": {"C": {"D": 2}, "E": 3},
305
349
  "F": {"G": {"H": 4}},
306
- "I": 5
350
+ "I": 5,
307
351
  }
308
352
 
309
353
 
@@ -99,7 +99,7 @@ class TestRequestProperties(object):
99
99
  CONTENT_LENGTH="2",
100
100
  CONTENT_TYPE="text/plain; charset=UTF-8",
101
101
  ) as c:
102
- c.request.body_bytes == b"\xc3a"
102
+ assert c.request.body_bytes == b"\xc3a"
103
103
 
104
104
  def test_get_json_decodes_json(self):
105
105
  with context(
@@ -107,7 +107,7 @@ class TestRequestProperties(object):
107
107
  CONTENT_LENGTH="14",
108
108
  CONTENT_TYPE="application/json",
109
109
  ) as c:
110
- c.request.get_json() == {"foo": "bar"}
110
+ assert c.request.get_json() == {"foo": "bar"}
111
111
 
112
112
  def test_get_json_ignores_mime_type(self):
113
113
  with context(
@@ -115,7 +115,7 @@ class TestRequestProperties(object):
115
115
  CONTENT_LENGTH="14",
116
116
  CONTENT_TYPE="application/broken",
117
117
  ) as c:
118
- c.request.get_json() == {"foo": "bar"}
118
+ assert c.request.get_json() == {"foo": "bar"}
119
119
 
120
120
  def test_get_json_passes_args_to_decoder(self):
121
121
  with context(
@@ -123,19 +123,30 @@ class TestRequestProperties(object):
123
123
  CONTENT_LENGTH="10",
124
124
  CONTENT_TYPE="application/broken",
125
125
  ) as c:
126
- c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
126
+ assert c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
127
127
 
128
- def test_get_does_type_conversion(self):
128
+ def test_get_required_raises_badrequest(self):
129
129
  with context(
130
130
  QUERY_STRING="x=10",
131
131
  CONTENT_LENGTH="10",
132
132
  CONTENT_TYPE="application/broken",
133
133
  ) as c:
134
- assert c.request.get("x") == "10"
135
- assert c.request.get("x", type=int) == 10
134
+ req = c.request
135
+ assert req.get("x", required=True) == "10"
136
136
  with pytest.raises(exceptions.BadRequest):
137
- c.request.get("y", type=int)
138
- assert c.request.get("y", None, type=int) is None
137
+ req.get("y", required=True)
138
+
139
+ def test_get_does_type_conversion(self):
140
+ with context(
141
+ QUERY_STRING="x=10",
142
+ CONTENT_LENGTH="10",
143
+ CONTENT_TYPE="application/broken",
144
+ ) as c:
145
+ req = c.request
146
+ assert req.get("x") == "10"
147
+ assert req.get("x", type=int) == 10
148
+ assert req.get("y", type=int) is None
149
+ assert req.get("y", "20", type=int) == "20"
139
150
 
140
151
  def test_is_secure_returns_correct_value(self):
141
152
  with context("https://example.org/") as c:
@@ -150,7 +161,7 @@ class TestRequestProperties(object):
150
161
  def test_getint_raises_badrequest(self):
151
162
  with pytest.raises(exceptions.BadRequest):
152
163
  with context("http://example.org/") as c:
153
- c.request.getint("a")
164
+ c.request.getint("a", required=True)
154
165
 
155
166
  with pytest.raises(exceptions.BadRequest):
156
167
  with context("http://example.org/?a=four") as c:
@@ -87,6 +87,12 @@ class TestCannedResponses(object):
87
87
  r = Response.redirect("http://bad.example.com/", "gloop", barfle=23)
88
88
  assert r.get_header("Location") == "http://localhost/arfle/23"
89
89
 
90
+ def test_redirect_uses_fallback_for_empty_url(self):
91
+ app = FrescoApp()
92
+ with app.requestcontext():
93
+ r = Response.redirect("", "http://localhost/foo")
94
+ assert r.get_header("Location") == "http://localhost/foo"
95
+
90
96
  def test_not_found(self):
91
97
  with FrescoApp().requestcontext():
92
98
  r = Response.not_found()
@@ -130,6 +136,16 @@ class TestCannedResponses(object):
130
136
  assert r.status == "200 OK"
131
137
  assert r.get_header("Content-Type") == "text/html"
132
138
 
139
+ def test_meta_refresh_to_view(self):
140
+
141
+ app = FrescoApp()
142
+ view = Mock(return_value=Response())
143
+ app.route("/arfle/<barfle:int>", GET, view)
144
+ with app.requestcontext():
145
+ r = Response.meta_refresh(view, barfle=42)
146
+ content = b"".join(r.content_iterator)
147
+ assert b'url=http://localhost/arfle/42"' in content
148
+
133
149
  def test_json(self):
134
150
  with FrescoApp().requestcontext():
135
151
  r = Response.json(
@@ -24,11 +24,15 @@ import tms
24
24
 
25
25
  from fresco import FrescoApp
26
26
  from fresco.core import urlfor
27
+ from fresco.request import Request
27
28
  from fresco.exceptions import NotFound
28
29
  from fresco.response import Response
29
30
  from fresco.routing import ALL_METHODS
31
+ from fresco.routing import ALL
30
32
  from fresco.routing import GET
33
+ from fresco.routing import OPTIONS
31
34
  from fresco.routing import POST
35
+ from fresco.routing import _has_request_parameter
32
36
  from fresco.routing import (
33
37
  Route,
34
38
  DelegateRoute,
@@ -43,6 +47,10 @@ from fresco.routing import (
43
47
  from . import fixtures
44
48
 
45
49
 
50
+ def dummyview() -> Response:
51
+ return Response()
52
+
53
+
46
54
  def assert_method_bound_to(method, ob):
47
55
  try:
48
56
  assert method.__self__ is ob
@@ -114,6 +122,31 @@ class TestRouteConstructor(object):
114
122
  with pytest.raises(ValueError):
115
123
  Route("/", ["GET", "FOO"], lambda: None)
116
124
 
125
+ def test_ALL_kwarg_routes_all_methods(self):
126
+ def view():
127
+ return Response()
128
+
129
+ app = FrescoApp()
130
+ app.route("/x", ALL, view)
131
+ assert len(list(app.get_route_traversals("/x", GET))) == 1
132
+ assert len(list(app.get_route_traversals("/x", POST))) == 1
133
+ assert len(list(app.get_route_traversals("/x", OPTIONS))) == 1
134
+
135
+ app.route("/y", ALL=view)
136
+ assert len(list(app.get_route_traversals("/y", GET))) == 1
137
+ assert len(list(app.get_route_traversals("/y", POST))) == 1
138
+ assert len(list(app.get_route_traversals("/y", OPTIONS))) == 1
139
+
140
+ def test_multiple_methods_can_be_expressed_using_kwargs(self):
141
+ def view():
142
+ return Response()
143
+
144
+ app = FrescoApp()
145
+ app.route("/x", GET_POST=view)
146
+ assert len(list(app.get_route_traversals("/x", GET))) == 1
147
+ assert len(list(app.get_route_traversals("/x", POST))) == 1
148
+ assert len(list(app.get_route_traversals("/x", OPTIONS))) == 0
149
+
117
150
 
118
151
  class TestRouteBeforeHooks(object):
119
152
  def test_hook_is_called(self):
@@ -330,36 +363,36 @@ class TestPredicates(object):
330
363
 
331
364
  class TestRouteNames(object):
332
365
  def test_name_present_in_route_keys(self):
333
- r = Route("/", GET, None, name="foo")
366
+ r = Route("/", GET, lambda: None, name="foo")
334
367
  assert "foo" in list(r.route_keys())
335
368
 
336
369
  def test_name_with_other_kwargs(self):
337
- r = Route("/", GET, None, name="foo", x="bar")
370
+ r = Route("/", GET, lambda: None, name="foo", x="bar")
338
371
  assert "foo" in list(r.route_keys())
339
372
 
340
373
  def test_name_cannot_contain_colon(self):
341
374
  with pytest.raises(ValueError):
342
- Route("/", GET, None, name="foo:bar")
375
+ Route("/", GET, lambda: None, name="foo:bar")
343
376
 
344
377
 
345
378
  class TestRouteCollection(object):
346
379
  def test_it_adds_routes_from_constructor(self):
347
- r1 = Route("/1", GET, None, name="1")
348
- r2 = Route("/2", POST, None, name="2")
380
+ r1 = Route("/1", GET, dummyview, name="1")
381
+ r2 = Route("/2", POST, dummyview, name="2")
349
382
  rc = RouteCollection([r1, r2])
350
383
  assert [r.name for r in rc] == ["1", "2"]
351
384
 
352
385
  def test_it_adds_routecollections_from_constructor(self):
353
- r1 = Route("/", GET, None, name="1")
354
- r2 = Route("/", POST, None, name="2")
355
- r3 = Route("/", POST, None, name="3")
386
+ r1 = Route("/", GET, dummyview, name="1")
387
+ r2 = Route("/", POST, dummyview, name="2")
388
+ r3 = Route("/", POST, dummyview, name="3")
356
389
  rc = RouteCollection([r1, RouteCollection([r2, r3])])
357
390
  assert [r.name for r in rc] == ["1", "2", "3"]
358
391
 
359
392
  def test_it_adds_dunderroutes_from_constructor(self):
360
- r1 = Route("/", GET, None, name="1")
361
- r2 = Route("/", POST, None, name="2")
362
- r3 = Route("/", POST, None, name="3")
393
+ r1 = Route("/", GET, dummyview, name="1")
394
+ r2 = Route("/", POST, dummyview, name="2")
395
+ r3 = Route("/", POST, dummyview, name="3")
363
396
 
364
397
  class A:
365
398
  __routes__ = [r2, r3]
@@ -368,8 +401,8 @@ class TestRouteCollection(object):
368
401
  assert [r.name for r in rc] == ["1", "2", "3"]
369
402
 
370
403
  def test_get_routes_matches_on_method(self):
371
- r_get = Route("/", GET, None)
372
- r_post = Route("/", POST, None)
404
+ r_get = Route("/", GET, dummyview)
405
+ r_post = Route("/", POST, dummyview)
373
406
 
374
407
  rc = RouteCollection([r_post, r_get])
375
408
 
@@ -377,8 +410,8 @@ class TestRouteCollection(object):
377
410
  assert [r.route for r in rc.get_route_traversals("/", POST)] == [r_post]
378
411
 
379
412
  def test_get_routes_matches_on_path(self):
380
- r1 = Route("/1", GET, None)
381
- r2 = Route("/2", GET, None)
413
+ r1 = Route("/1", GET, dummyview)
414
+ r2 = Route("/2", GET, dummyview)
382
415
 
383
416
  rc = RouteCollection([r1, r2])
384
417
 
@@ -386,8 +419,8 @@ class TestRouteCollection(object):
386
419
  assert [r.route for r in rc.get_route_traversals("/2", GET)] == [r2]
387
420
 
388
421
  def test_get_routes_can_match_all_methods(self):
389
- r1 = Route("/1", GET, None)
390
- r2 = Route("/1", POST, None)
422
+ r1 = Route("/1", GET, dummyview)
423
+ r2 = Route("/1", POST, dummyview)
391
424
 
392
425
  rc = RouteCollection([r1, r2])
393
426
  assert [r.route for r in rc.get_route_traversals("/1", None)] == [
@@ -399,8 +432,8 @@ class TestRouteCollection(object):
399
432
  a = RouteCollection()
400
433
  b = RouteCollection()
401
434
 
402
- a_route = Route("/harvey", GET, lambda: None)
403
- b_route = Route("/harvey", GET, lambda: None)
435
+ a_route = Route("/harvey", GET, dummyview)
436
+ b_route = Route("/harvey", GET, dummyview)
404
437
 
405
438
  a.add_route(a_route)
406
439
  b.add_route(b_route)
@@ -411,7 +444,7 @@ class TestRouteCollection(object):
411
444
  a.add_route(a_delegate_route)
412
445
  b.add_route(b_delegate_route)
413
446
 
414
- r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", None))
447
+ r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", "GET"))
415
448
 
416
449
  assert r.collections_traversed == [
417
450
  (a, "", a_delegate_route, (), {}, (), {}),
@@ -508,7 +541,9 @@ class TestRouteCollection(object):
508
541
  ]
509
542
 
510
543
  def test_add_prefix_returns_prefixed_collection(self):
511
- rc = RouteCollection([Route("/fish", GET, None), Route("/beans", GET, None)])
544
+ rc = RouteCollection(
545
+ [Route("/fish", GET, dummyview), Route("/beans", GET, dummyview)]
546
+ )
512
547
  prefixed = rc.add_prefix("/jelly")
513
548
  assert [str(r.pattern) for r in prefixed] == [
514
549
  "/jelly/fish",
@@ -611,15 +646,14 @@ class TestRouteCollection(object):
611
646
  assert list(response.content_iterator) == [b"ok"]
612
647
 
613
648
  def test_it_isolates_request_for_wsgi_app(self):
614
-
615
649
  params: t.Dict[str, t.Any] = {}
616
650
  outerapp = FrescoApp()
617
651
  innerapp = FrescoApp()
618
652
  innerapp.route("/y", GET=Response)
619
653
  outerapp.route_wsgi("/x", innerapp)
620
654
 
621
- innerapp.process_request(partial(params.__setitem__, 'inner_request'))
622
- outerapp.process_request(partial(params.__setitem__, 'outer_request'))
655
+ innerapp.process_request(partial(params.__setitem__, "inner_request"))
656
+ outerapp.process_request(partial(params.__setitem__, "outer_request"))
623
657
 
624
658
  with outerapp.requestcontext("/x/y"):
625
659
  outerapp.view()
@@ -894,25 +928,25 @@ class TestRRoute:
894
928
 
895
929
  class TestViewArgs(object):
896
930
  def test_it_uses_args(self):
897
- routes = RouteCollection([Route("/", GET, None, args=(1, 2))])
931
+ routes = RouteCollection([Route("/", GET, dummyview, args=(1, 2))])
898
932
  assert list(routes.get_route_traversals("/", GET)) == [
899
933
  tms.InstanceOf(RouteTraversal, args=(1, 2))
900
934
  ]
901
935
 
902
936
  def test_it_uses_view_args(self):
903
- routes = RouteCollection([Route("/", GET, None, view_args=(1, 2))])
937
+ routes = RouteCollection([Route("/", GET, dummyview, view_args=(1, 2))])
904
938
  assert list(routes.get_route_traversals("/", GET)) == [
905
939
  tms.InstanceOf(RouteTraversal, args=(1, 2))
906
940
  ]
907
941
 
908
942
  def test_it_appends_args_extracted_from_path(self):
909
- routes = RouteCollection([Route("/<:int>", GET, None, view_args=(1, 2))])
943
+ routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1, 2))])
910
944
  assert list(routes.get_route_traversals("/3", GET)) == [
911
945
  tms.InstanceOf(RouteTraversal, args=(1, 2, 3))
912
946
  ]
913
947
 
914
948
  def test_it_keeps_traversal_args_separate(self):
915
- routes = RouteCollection([Route("/<:int>", GET, None, view_args=(1,))])
949
+ routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1,))])
916
950
  assert list(routes.get_route_traversals("/2", GET)) == [
917
951
  tms.InstanceOf(
918
952
  RouteTraversal,
@@ -928,25 +962,27 @@ class TestViewArgs(object):
928
962
 
929
963
  class TestViewKwargs(object):
930
964
  def test_it_reads_from_route_kwargs(self):
931
- routes = RouteCollection([Route("/", GET, None, x=1)])
965
+ routes = RouteCollection([Route("/", GET, dummyview, x=1)])
932
966
  assert list(routes.get_route_traversals("/", GET)) == [
933
967
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
934
968
  ]
935
969
 
936
970
  def test_it_reads_from_kwargs(self):
937
- routes = RouteCollection([Route("/", GET, None, kwargs={"x": 1})])
971
+ routes = RouteCollection([Route("/", GET, dummyview, kwargs={"x": 1})])
938
972
  assert list(routes.get_route_traversals("/", GET)) == [
939
973
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
940
974
  ]
941
975
 
942
976
  def test_it_reads_from_view_kwargs(self):
943
- routes = RouteCollection([Route("/", GET, None, view_kwargs={"x": 1})])
977
+ routes = RouteCollection([Route("/", GET, dummyview, view_kwargs={"x": 1})])
944
978
  assert list(routes.get_route_traversals("/", GET)) == [
945
979
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
946
980
  ]
947
981
 
948
982
  def test_it_keeps_traversal_kwargs_separate(self):
949
- routes = RouteCollection([Route("/<x:int>", GET, None, view_kwargs={"y": 1})])
983
+ routes = RouteCollection(
984
+ [Route("/<x:int>", GET, dummyview, view_kwargs={"y": 1})]
985
+ )
950
986
  assert list(routes.get_route_traversals("/2", GET)) == [
951
987
  tms.InstanceOf(
952
988
  RouteTraversal,
@@ -1113,7 +1149,6 @@ class TestRouteCache(object):
1113
1149
 
1114
1150
 
1115
1151
  class TestRouteAll:
1116
-
1117
1152
  def test_route_all_matches_on_separator(self):
1118
1153
  def view():
1119
1154
  return Response()
@@ -1124,3 +1159,48 @@ class TestRouteAll:
1124
1159
  assert len(list(app.get_route_traversals("/x", GET))) == 1
1125
1160
  assert len(list(app.get_route_traversals("/x/y", GET))) == 1
1126
1161
  assert len(list(app.get_route_traversals("/xy", GET))) == 0
1162
+
1163
+
1164
+ class TestRequestParameter:
1165
+ def test_has_request_parameter(self):
1166
+ def a(request: Request):
1167
+ pass
1168
+
1169
+ def b(request: t.Optional[Request]):
1170
+ pass
1171
+
1172
+ def c(request: t.Union[Request, None]):
1173
+ pass
1174
+
1175
+ def d(request: t.Union[None, Request]):
1176
+ pass
1177
+
1178
+ def e(request: int):
1179
+ pass
1180
+
1181
+ def f(request):
1182
+ pass
1183
+
1184
+ assert _has_request_parameter(a) is True
1185
+ assert _has_request_parameter(b) is True
1186
+ assert _has_request_parameter(c) is True
1187
+ assert _has_request_parameter(d) is True
1188
+ assert _has_request_parameter(e) is False
1189
+ assert _has_request_parameter(f) is False
1190
+
1191
+ def test_request_is_provided_automatically(self):
1192
+ def a(request: Request):
1193
+ return Response("a")
1194
+
1195
+ def b():
1196
+ return Response("b")
1197
+
1198
+ app = FrescoApp()
1199
+ app.route("/a", GET=a)
1200
+ app.route("/b", GET=b)
1201
+
1202
+ with app.requestcontext("/a"):
1203
+ assert app.view().content == "a"
1204
+
1205
+ with app.requestcontext("/b"):
1206
+ assert app.view().content == "b"
@@ -42,7 +42,7 @@ class TestParseQueryString(object):
42
42
  return list(parse_querystring(value))
43
43
 
44
44
  def test_empty(self):
45
- self.p("") == []
45
+ assert self.p("") == []
46
46
 
47
47
  def test_simple_key_value(self):
48
48
  assert self.p("a=b") == [("a", "b")]
@@ -276,12 +276,10 @@ class TestParseFormEncodedData(object):
276
276
  class TestEncodeMultipart(object):
277
277
  def test_it_encodes_a_data_dict(self):
278
278
  data, headers = encode_multipart([("foo", "bar baf")])
279
- data = data.getvalue()
280
279
  assert b'Content-Disposition: form-data; name="foo"\r\n\r\nbar baf' in data
281
280
 
282
281
  def test_it_encodes_a_file_tuple(self):
283
282
  data, headers = encode_multipart(files=[("foo", "foo.txt", "ascii", "bar")])
284
- data = data.getvalue()
285
283
  expected = (
286
284
  b"Content-Disposition: form-data; "
287
285
  b'name="foo"; filename="foo.txt"\r\n'
@@ -15,6 +15,7 @@
15
15
  from fresco.core import FrescoApp
16
16
  from fresco.util.urls import normpath, make_query
17
17
  from fresco.util.urls import is_safe_url
18
+ from fresco.util.urls import add_query
18
19
 
19
20
  # Greek letters as unicode strings (require multi-byte representation in UTF-8)
20
21
  alpha = b"\xce\xb1".decode("utf8")
@@ -174,3 +175,22 @@ class TestSafeURL(object):
174
175
  ]
175
176
  for u in unsafe:
176
177
  assert is_safe_url(u, allowed_hosts={"good.example.org"}) is False
178
+
179
+
180
+ class TestAddQuery:
181
+
182
+ def test_it_adds_query(self):
183
+ url = add_query("http://localhost", {"foo": "bar"})
184
+ assert url == "http://localhost?foo=bar"
185
+
186
+ def test_it_adds_query_from_list(self):
187
+ url = add_query("http://localhost", [("foo", "bar")])
188
+ assert url == "http://localhost?foo=bar"
189
+
190
+ def test_it_adds_query_from_keywords(self):
191
+ url = add_query("http://localhost", foo="bar")
192
+ assert url == "http://localhost?foo=bar"
193
+
194
+ def test_it_appends_query(self):
195
+ url = add_query("http://localhost?foo=bar", {"foo": "baz"})
196
+ assert url == "http://localhost?foo=bar&foo=baz"
fresco/types.py CHANGED
@@ -1,6 +1,32 @@
1
+ from collections.abc import Mapping
2
+ from collections.abc import Iterable
3
+ from collections.abc import Callable
4
+ from types import TracebackType
5
+ from typing import Any
6
+ from typing import Optional
1
7
  import typing as t
2
8
 
9
+ import fresco.response # noqa
10
+
11
+
3
12
  QuerySpec = t.Union[
4
- t.Mapping[str, t.Any],
5
- t.Iterable[t.Tuple[str, t.Any]]
13
+ Mapping[str, Any],
14
+ Iterable[tuple[str, Any]]
15
+ ]
16
+ ViewCallable = Callable[..., "fresco.response.Response"]
17
+ ExcInfo = tuple[type[BaseException], BaseException, TracebackType]
18
+ OptionalExcInfo = Optional[ExcInfo]
19
+
20
+
21
+ HeaderList = list[tuple[str, str]]
22
+ HeadersList = HeaderList
23
+ WSGIEnviron = dict[str, Any]
24
+ WriteCallable = Callable[[bytes], object]
25
+ StartResponse = Callable[[str, HeaderList, OptionalExcInfo], WriteCallable]
26
+ WSGIApplication = Callable[
27
+ [
28
+ WSGIEnviron,
29
+ StartResponse,
30
+ ],
31
+ Iterable[bytes]
6
32
  ]
fresco/util/cache.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from typing import Any
2
+ from typing import Callable
2
3
  from typing import Dict
3
4
  from typing import List
4
5
  import sys
@@ -46,7 +47,7 @@ def cache_generator(original_function, maxsize):
46
47
  link[NEXT] = root
47
48
 
48
49
 
49
- def make_cache(original_function, maxsize=100):
50
+ def make_cache(original_function, maxsize=100) -> Callable[[Any], Any]:
50
51
  "Create a cache around a function that takes a single argument"
51
52
  c = cache_generator(original_function, maxsize)
52
53
  next(c)