1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
|
"""When it comes to combining multiple controller or view functions
(however you want to call them) you need a dispatcher. A simple way
would be applying regular expression tests on the ``PATH_INFO`` and
calling registered callback functions that return the value then.
This module implements a much more powerful system than simple regular
expression matching because it can also convert values in the URLs and
build URLs.
Here a simple example that creates a URL map for an application with
two subdomains (www and kb) and some URL rules:
.. code-block:: python
m = Map([
# Static URLs
Rule('/', endpoint='static/index'),
Rule('/about', endpoint='static/about'),
Rule('/help', endpoint='static/help'),
# Knowledge Base
Subdomain('kb', [
Rule('/', endpoint='kb/index'),
Rule('/browse/', endpoint='kb/browse'),
Rule('/browse/<int:id>/', endpoint='kb/browse'),
Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse')
])
], default_subdomain='www')
If the application doesn't use subdomains it's perfectly fine to not set
the default subdomain and not use the `Subdomain` rule factory. The
endpoint in the rules can be anything, for example import paths or
unique identifiers. The WSGI application can use those endpoints to get the
handler for that URL. It doesn't have to be a string at all but it's
recommended.
Now it's possible to create a URL adapter for one of the subdomains and
build URLs:
.. code-block:: python
c = m.bind('example.com')
c.build("kb/browse", dict(id=42))
'http://kb.example.com/browse/42/'
c.build("kb/browse", dict())
'http://kb.example.com/browse/'
c.build("kb/browse", dict(id=42, page=3))
'http://kb.example.com/browse/42/3'
c.build("static/about")
'/about'
c.build("static/index", force_external=True)
'http://www.example.com/'
c = m.bind('example.com', subdomain='kb')
c.build("static/about")
'http://www.example.com/about'
The first argument to bind is the server name *without* the subdomain.
Per default it will assume that the script is mounted on the root, but
often that's not the case so you can provide the real mount point as
second argument:
.. code-block:: python
c = m.bind('example.com', '/applications/example')
The third argument can be the subdomain, if not given the default
subdomain is used. For more details about binding have a look at the
documentation of the `MapAdapter`.
And here is how you can match URLs:
.. code-block:: python
c = m.bind('example.com')
c.match("/")
('static/index', {})
c.match("/about")
('static/about', {})
c = m.bind('example.com', '/', 'kb')
c.match("/")
('kb/index', {})
c.match("/browse/42/23")
('kb/browse', {'id': 42, 'page': 23})
If matching fails you get a ``NotFound`` exception, if the rule thinks
it's a good idea to redirect (for example because the URL was defined
to have a slash at the end but the request was missing that slash) it
will raise a ``RequestRedirect`` exception. Both are subclasses of
``HTTPException`` so you can use those errors as responses in the
application.
If matching succeeded but the URL rule was incompatible to the given
method (for example there were only rules for ``GET`` and ``HEAD`` but
routing tried to match a ``POST`` request) a ``MethodNotAllowed``
exception is raised.
"""
import ast
import difflib
import posixpath
import re
import typing
import typing as t
import uuid
import warnings
from pprint import pformat
from string import Template
from threading import Lock
from types import CodeType
from ._internal import _encode_idna
from ._internal import _get_environ
from ._internal import _to_bytes
from ._internal import _to_str
from ._internal import _wsgi_decoding_dance
from .datastructures import ImmutableDict
from .datastructures import MultiDict
from .exceptions import BadHost
from .exceptions import BadRequest
from .exceptions import HTTPException
from .exceptions import MethodNotAllowed
from .exceptions import NotFound
from .urls import _fast_url_quote
from .urls import url_encode
from .urls import url_join
from .urls import url_quote
from .urls import url_unquote
from .utils import cached_property
from .utils import redirect
from .wsgi import get_host
if t.TYPE_CHECKING:
import typing_extensions as te
from _typeshed.wsgi import WSGIApplication
from _typeshed.wsgi import WSGIEnvironment
from .wrappers.request import Request
from .wrappers.response import Response
_rule_re = re.compile(
r"""
(?P<static>[^<]*) # static rule data
<
(?:
(?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
(?:\((?P<args>.*?)\))? # converter arguments
\: # variable delimiter
)?
(?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
>
""",
re.VERBOSE,
)
_simple_rule_re = re.compile(r"<([^>]+)>")
_converter_args_re = re.compile(
r"""
((?P<name>\w+)\s*=\s*)?
(?P<value>
True|False|
\d+.\d+|
\d+.|
\d+|
[\w\d_.]+|
[urUR]?(?P<stringval>"[^"]*?"|'[^']*')
)\s*,
""",
re.VERBOSE,
)
_PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
def _pythonize(value: str) -> t.Union[None, bool, int, float, str]:
if value in _PYTHON_CONSTANTS:
return _PYTHON_CONSTANTS[value]
for convert in int, float:
try:
return convert(value) # type: ignore
except ValueError:
pass
if value[:1] == value[-1:] and value[0] in "\"'":
value = value[1:-1]
return str(value)
def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]:
argstr += ","
args = []
kwargs = {}
for item in _converter_args_re.finditer(argstr):
value = item.group("stringval")
if value is None:
value = item.group("value")
value = _pythonize(value)
if not item.group("name"):
args.append(value)
else:
name = item.group("name")
kwargs[name] = value
return tuple(args), kwargs
def parse_rule(rule: str) -> t.Iterator[t.Tuple[t.Optional[str], t.Optional[str], str]]:
"""Parse a rule and return it as generator. Each iteration yields tuples
in the form ``(converter, arguments, variable)``. If the converter is
`None` it's a static url part, otherwise it's a dynamic one.
:internal:
"""
pos = 0
end = len(rule)
do_match = _rule_re.match
used_names = set()
while pos < end:
m = do_match(rule, pos)
if m is None:
break
data = m.groupdict()
if data["static"]:
yield None, None, data["static"]
variable = data["variable"]
converter = data["converter"] or "default"
if variable in used_names:
raise ValueError(f"variable name {variable!r} used twice.")
used_names.add(variable)
yield converter, data["args"] or None, variable
pos = m.end()
if pos < end:
remaining = rule[pos:]
if ">" in remaining or "<" in remaining:
raise ValueError(f"malformed url rule: {rule!r}")
yield None, None, remaining
class RoutingException(Exception):
"""Special exceptions that require the application to redirect, notifying
about missing urls, etc.
:internal:
"""
class RequestRedirect(HTTPException, RoutingException):
"""Raise if the map requests a redirect. This is for example the case if
`strict_slashes` are activated and an url that requires a trailing slash.
The attribute `new_url` contains the absolute destination url.
"""
code = 308
def __init__(self, new_url: str) -> None:
super().__init__(new_url)
self.new_url = new_url
def get_response(
self,
environ: t.Optional[t.Union["WSGIEnvironment", "Request"]] = None,
scope: t.Optional[dict] = None,
) -> "Response":
return redirect(self.new_url, self.code)
class RequestPath(RoutingException):
"""Internal exception."""
__slots__ = ("path_info",)
def __init__(self, path_info: str) -> None:
super().__init__()
self.path_info = path_info
class RequestAliasRedirect(RoutingException): # noqa: B903
"""This rule is an alias and wants to redirect to the canonical URL."""
def __init__(self, matched_values: t.Mapping[str, t.Any]) -> None:
super().__init__()
self.matched_values = matched_values
class BuildError(RoutingException, LookupError):
"""Raised if the build system cannot find a URL for an endpoint with the
values provided.
"""
def __init__(
self,
endpoint: str,
values: t.Mapping[str, t.Any],
method: t.Optional[str],
adapter: t.Optional["MapAdapter"] = None,
) -> None:
super().__init__(endpoint, values, method)
self.endpoint = endpoint
self.values = values
self.method = method
self.adapter = adapter
@cached_property
def suggested(self) -> t.Optional["Rule"]:
return self.closest_rule(self.adapter)
def closest_rule(self, adapter: t.Optional["MapAdapter"]) -> t.Optional["Rule"]:
def _score_rule(rule: "Rule") -> float:
return sum(
[
0.98
* difflib.SequenceMatcher(
None, rule.endpoint, self.endpoint
).ratio(),
0.01 * bool(set(self.values or ()).issubset(rule.arguments)),
0.01 * bool(rule.methods and self.method in rule.methods),
]
)
if adapter and adapter.map._rules:
return max(adapter.map._rules, key=_score_rule)
return None
def __str__(self) -> str:
message = [f"Could not build url for endpoint {self.endpoint!r}"]
if self.method:
message.append(f" ({self.method!r})")
if self.values:
message.append(f" with values {sorted(self.values)!r}")
message.append(".")
if self.suggested:
if self.endpoint == self.suggested.endpoint:
if (
self.method
and self.suggested.methods is not None
and self.method not in self.suggested.methods
):
message.append(
" Did you mean to use methods"
f" {sorted(self.suggested.methods)!r}?"
)
missing_values = self.suggested.arguments.union(
set(self.suggested.defaults or ())
) - set(self.values.keys())
if missing_values:
message.append(
f" Did you forget to specify values {sorted(missing_values)!r}?"
)
else:
message.append(f" Did you mean {self.suggested.endpoint!r} instead?")
return "".join(message)
class WebsocketMismatch(BadRequest):
"""The only matched rule is either a WebSocket and the request is
HTTP, or the rule is HTTP and the request is a WebSocket.
"""
class ValidationError(ValueError):
"""Validation error. If a rule converter raises this exception the rule
does not match the current URL and the next URL is tried.
"""
class RuleFactory:
"""As soon as you have more complex URL setups it's a good idea to use rule
factories to avoid repetitive tasks. Some of them are builtin, others can
be added by subclassing `RuleFactory` and overriding `get_rules`.
"""
def get_rules(self, map: "Map") -> t.Iterable["Rule"]:
"""Subclasses of `RuleFactory` have to override this method and return
an iterable of rules."""
raise NotImplementedError()
class Subdomain(RuleFactory):
"""All URLs provided by this factory have the subdomain set to a
specific domain. For example if you want to use the subdomain for
the current language this can be a good setup::
url_map = Map([
Rule('/', endpoint='#select_language'),
Subdomain('<string(length=2):lang_code>', [
Rule('/', endpoint='index'),
Rule('/about', endpoint='about'),
Rule('/help', endpoint='help')
])
])
All the rules except for the ``'#select_language'`` endpoint will now
listen on a two letter long subdomain that holds the language code
for the current request.
"""
def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
self.subdomain = subdomain
self.rules = rules
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
for rulefactory in self.rules:
for rule in rulefactory.get_rules(map):
rule = rule.empty()
rule.subdomain = self.subdomain
yield rule
class Submount(RuleFactory):
"""Like `Subdomain` but prefixes the URL rule with a given string::
url_map = Map([
Rule('/', endpoint='index'),
Submount('/blog', [
Rule('/', endpoint='blog/index'),
Rule('/entry/<entry_slug>', endpoint='blog/show')
])
])
Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
"""
def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
self.path = path.rstrip("/")
self.rules = rules
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
for rulefactory in self.rules:
for rule in rulefactory.get_rules(map):
rule = rule.empty()
rule.rule = self.path + rule.rule
yield rule
class EndpointPrefix(RuleFactory):
"""Prefixes all endpoints (which must be strings for this factory) with
another string. This can be useful for sub applications::
url_map = Map([
Rule('/', endpoint='index'),
EndpointPrefix('blog/', [Submount('/blog', [
Rule('/', endpoint='index'),
Rule('/entry/<entry_slug>', endpoint='show')
])])
])
"""
def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
self.prefix = prefix
self.rules = rules
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
for rulefactory in self.rules:
for rule in rulefactory.get_rules(map):
rule = rule.empty()
rule.endpoint = self.prefix + rule.endpoint
yield rule
class RuleTemplate:
"""Returns copies of the rules wrapped and expands string templates in
the endpoint, rule, defaults or subdomain sections.
Here a small example for such a rule template::
from werkzeug.routing import Map, Rule, RuleTemplate
resource = RuleTemplate([
Rule('/$name/', endpoint='$name.list'),
Rule('/$name/<int:id>', endpoint='$name.show')
])
url_map = Map([resource(name='user'), resource(name='page')])
When a rule template is called the keyword arguments are used to
replace the placeholders in all the string parameters.
"""
def __init__(self, rules: t.Iterable["Rule"]) -> None:
self.rules = list(rules)
def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory":
return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
class RuleTemplateFactory(RuleFactory):
"""A factory that fills in template variables into rules. Used by
`RuleTemplate` internally.
:internal:
"""
def __init__(
self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any]
) -> None:
self.rules = rules
self.context = context
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
for rulefactory in self.rules:
for rule in rulefactory.get_rules(map):
new_defaults = subdomain = None
if rule.defaults:
new_defaults = {}
for key, value in rule.defaults.items():
if isinstance(value, str):
value = Template(value).substitute(self.context)
new_defaults[key] = value
if rule.subdomain is not None:
subdomain = Template(rule.subdomain).substitute(self.context)
new_endpoint = rule.endpoint
if isinstance(new_endpoint, str):
new_endpoint = Template(new_endpoint).substitute(self.context)
yield Rule(
Template(rule.rule).substitute(self.context),
new_defaults,
subdomain,
rule.methods,
rule.build_only,
new_endpoint,
rule.strict_slashes,
)
def _prefix_names(src: str) -> ast.stmt:
"""ast parse and prefix names with `.` to avoid collision with user vars"""
tree = ast.parse(src).body[0]
if isinstance(tree, ast.Expr):
tree = tree.value # type: ignore
for node in ast.walk(tree):
if isinstance(node, ast.Name):
node.id = f".{node.id}"
return tree
_CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
_IF_KWARGS_URL_ENCODE_CODE = """\
if kwargs:
q = '?'
params = self._encode_query_vars(kwargs)
else:
q = params = ''
"""
_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE)
_URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params"))
class Rule(RuleFactory):
"""A Rule represents one URL pattern. There are some options for `Rule`
that change the way it behaves and are passed to the `Rule` constructor.
Note that besides the rule-string all arguments *must* be keyword arguments
in order to not break the application on Werkzeug upgrades.
`string`
Rule strings basically are just normal URL paths with placeholders in
the format ``<converter(arguments):name>`` where the converter and the
arguments are optional. If no converter is defined the `default`
converter is used which means `string` in the normal configuration.
URL rules that end with a slash are branch URLs, others are leaves.
If you have `strict_slashes` enabled (which is the default), all
branch URLs that are matched without a trailing slash will trigger a
redirect to the same URL with the missing slash appended.
The converters are defined on the `Map`.
`endpoint`
The endpoint for this rule. This can be anything. A reference to a
function, a string, a number etc. The preferred way is using a string
because the endpoint is used for URL generation.
`defaults`
An optional dict with defaults for other rules with the same endpoint.
This is a bit tricky but useful if you want to have unique URLs::
url_map = Map([
Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
Rule('/all/page/<int:page>', endpoint='all_entries')
])
If a user now visits ``http://example.com/all/page/1`` he will be
redirected to ``http://example.com/all/``. If `redirect_defaults` is
disabled on the `Map` instance this will only affect the URL
generation.
`subdomain`
The subdomain rule string for this rule. If not specified the rule
only matches for the `default_subdomain` of the map. If the map is
not bound to a subdomain this feature is disabled.
Can be useful if you want to have user profiles on different subdomains
and all subdomains are forwarded to your application::
url_map = Map([
Rule('/', subdomain='<username>', endpoint='user/homepage'),
Rule('/stats', subdomain='<username>', endpoint='user/stats')
])
`methods`
A sequence of http methods this rule applies to. If not specified, all
methods are allowed. For example this can be useful if you want different
endpoints for `POST` and `GET`. If methods are defined and the path
matches but the method matched against is not in this list or in the
list of another rule for that path the error raised is of the type
`MethodNotAllowed` rather than `NotFound`. If `GET` is present in the
list of methods and `HEAD` is not, `HEAD` is added automatically.
`strict_slashes`
Override the `Map` setting for `strict_slashes` only for this rule. If
not specified the `Map` setting is used.
`merge_slashes`
Override :attr:`Map.merge_slashes` for this rule.
`build_only`
Set this to True and the rule will never match but will create a URL
that can be build. This is useful if you have resources on a subdomain
or folder that are not handled by the WSGI application (like static data)
`redirect_to`
If given this must be either a string or callable. In case of a
callable it's called with the url adapter that triggered the match and
the values of the URL as keyword arguments and has to return the target
for the redirect, otherwise it has to be a string with placeholders in
rule syntax::
def foo_with_slug(adapter, id):
# ask the database for the slug for the old id. this of
# course has nothing to do with werkzeug.
return f'foo/{Foo.get_slug_for_id(id)}'
url_map = Map([
Rule('/foo/<slug>', endpoint='foo'),
Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug)
])
When the rule is matched the routing system will raise a
`RequestRedirect` exception with the target for the redirect.
Keep in mind that the URL will be joined against the URL root of the
script so don't use a leading slash on the target URL unless you
really mean root of that domain.
`alias`
If enabled this rule serves as an alias for another rule with the same
endpoint and arguments.
`host`
If provided and the URL map has host matching enabled this can be
used to provide a match rule for the whole host. This also means
that the subdomain feature is disabled.
`websocket`
If ``True``, this rule is only matches for WebSocket (``ws://``,
``wss://``) requests. By default, rules will only match for HTTP
requests.
.. versionadded:: 1.0
Added ``websocket``.
.. versionadded:: 1.0
Added ``merge_slashes``.
.. versionadded:: 0.7
Added ``alias`` and ``host``.
.. versionchanged:: 0.6.1
``HEAD`` is added to ``methods`` if ``GET`` is present.
"""
def __init__(
self,
string: str,
defaults: t.Optional[t.Mapping[str, t.Any]] = None,
subdomain: t.Optional[str] = None,
methods: t.Optional[t.Iterable[str]] = None,
build_only: bool = False,
endpoint: t.Optional[str] = None,
strict_slashes: t.Optional[bool] = None,
merge_slashes: t.Optional[bool] = None,
redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None,
alias: bool = False,
host: t.Optional[str] = None,
websocket: bool = False,
) -> None:
if not string.startswith("/"):
raise ValueError("urls must start with a leading slash")
self.rule = string
self.is_leaf = not string.endswith("/")
self.map: "Map" = None # type: ignore
self.strict_slashes = strict_slashes
self.merge_slashes = merge_slashes
self.subdomain = subdomain
self.host = host
self.defaults = defaults
self.build_only = build_only
self.alias = alias
self.websocket = websocket
if methods is not None:
if isinstance(methods, str):
raise TypeError("'methods' should be a list of strings.")
methods = {x.upper() for x in methods}
if "HEAD" not in methods and "GET" in methods:
methods.add("HEAD")
if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
raise ValueError(
"WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
)
self.methods = methods
self.endpoint: str = endpoint # type: ignore
self.redirect_to = redirect_to
if defaults:
self.arguments = set(map(str, defaults))
else:
self.arguments = set()
self._trace: t.List[t.Tuple[bool, str]] = []
def empty(self) -> "Rule":
"""
Return an unbound copy of this rule.
This can be useful if want to reuse an already bound URL for another
map. See ``get_empty_kwargs`` to override what keyword arguments are
provided to the new copy.
"""
return type(self)(self.rule, **self.get_empty_kwargs())
def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
"""
Provides kwargs for instantiating empty copy with empty()
Use this method to provide custom keyword arguments to the subclass of
``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
has custom keyword arguments that are needed at instantiation.
Must return a ``dict`` that will be provided as kwargs to the new
instance of ``Rule``, following the initial ``self.rule`` value which
is always provided as the first, required positional argument.
"""
defaults = None
if self.defaults:
defaults = dict(self.defaults)
return dict(
defaults=defaults,
subdomain=self.subdomain,
methods=self.methods,
build_only=self.build_only,
endpoint=self.endpoint,
strict_slashes=self.strict_slashes,
redirect_to=self.redirect_to,
alias=self.alias,
host=self.host,
)
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
yield self
def refresh(self) -> None:
"""Rebinds and refreshes the URL. Call this if you modified the
rule in place.
:internal:
"""
self.bind(self.map, rebind=True)
def bind(self, map: "Map", rebind: bool = False) -> None:
"""Bind the url to a map and create a regular expression based on
the information from the rule itself and the defaults from the map.
:internal:
"""
if self.map is not None and not rebind:
raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
self.map = map
if self.strict_slashes is None:
self.strict_slashes = map.strict_slashes
if self.merge_slashes is None:
self.merge_slashes = map.merge_slashes
if self.subdomain is None:
self.subdomain = map.default_subdomain
self.compile()
def get_converter(
self,
variable_name: str,
converter_name: str,
args: t.Tuple,
kwargs: t.Mapping[str, t.Any],
) -> "BaseConverter":
"""Looks up the converter for the given parameter.
.. versionadded:: 0.9
"""
if converter_name not in self.map.converters:
raise LookupError(f"the converter {converter_name!r} does not exist")
return self.map.converters[converter_name](self.map, *args, **kwargs)
def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
return url_encode(
query_vars,
charset=self.map.charset,
sort=self.map.sort_parameters,
key=self.map.sort_key,
)
def compile(self) -> None:
"""Compiles the regular expression and stores it."""
assert self.map is not None, "rule not bound"
if self.map.host_matching:
domain_rule = self.host or ""
else:
domain_rule = self.subdomain or ""
self._trace = []
self._converters: t.Dict[str, "BaseConverter"] = {}
self._static_weights: t.List[t.Tuple[int, int]] = []
self._argument_weights: t.List[int] = []
regex_parts = []
def _build_regex(rule: str) -> None:
index = 0
for converter, arguments, variable in parse_rule(rule):
if converter is None:
for match in re.finditer(r"/+|[^/]+", variable):
part = match.group(0)
if part.startswith("/"):
if self.merge_slashes:
regex_parts.append(r"/+?")
self._trace.append((False, "/"))
else:
regex_parts.append(part)
self._trace.append((False, part))
continue
self._trace.append((False, part))
regex_parts.append(re.escape(part))
if part:
self._static_weights.append((index, -len(part)))
else:
if arguments:
c_args, c_kwargs = parse_converter_args(arguments)
else:
c_args = ()
c_kwargs = {}
convobj = self.get_converter(variable, converter, c_args, c_kwargs)
regex_parts.append(f"(?P<{variable}>{convobj.regex})")
self._converters[variable] = convobj
self._trace.append((True, variable))
self._argument_weights.append(convobj.weight)
self.arguments.add(str(variable))
index = index + 1
_build_regex(domain_rule)
regex_parts.append("\\|")
self._trace.append((False, "|"))
_build_regex(self.rule if self.is_leaf else self.rule.rstrip("/"))
if not self.is_leaf:
self._trace.append((False, "/"))
self._build: t.Callable[..., t.Tuple[str, str]]
self._build = self._compile_builder(False).__get__(self, None) # type: ignore
self._build_unknown: t.Callable[..., t.Tuple[str, str]]
self._build_unknown = self._compile_builder(True).__get__( # type: ignore
self, None
)
if self.build_only:
return
if not (self.is_leaf and self.strict_slashes):
reps = "*" if self.merge_slashes else "?"
tail = f"(?<!/)(?P<__suffix__>/{reps})"
else:
tail = ""
regex = f"^{''.join(regex_parts)}{tail}$"
self._regex = re.compile(regex)
def match(
self, path: str, method: t.Optional[str] = None
) -> t.Optional[t.MutableMapping[str, t.Any]]:
"""Check if the rule matches a given path. Path is a string in the
form ``"subdomain|/path"`` and is assembled by the map. If
the map is doing host matching the subdomain part will be the host
instead.
If the rule matches a dict with the converted values is returned,
otherwise the return value is `None`.
:internal:
"""
if not self.build_only:
require_redirect = False
m = self._regex.search(path)
if m is not None:
groups = m.groupdict()
# we have a folder like part of the url without a trailing
# slash and strict slashes enabled. raise an exception that
# tells the map to redirect to the same url but with a
# trailing slash
if (
self.strict_slashes
and not self.is_leaf
and not groups.pop("__suffix__")
and (
method is None or self.methods is None or method in self.methods
)
):
path += "/"
require_redirect = True
# if we are not in strict slashes mode we have to remove
# a __suffix__
elif not self.strict_slashes:
del groups["__suffix__"]
result = {}
for name, value in groups.items():
try:
value = self._converters[name].to_python(value)
except ValidationError:
return None
result[str(name)] = value
if self.defaults:
result.update(self.defaults)
if self.merge_slashes:
new_path = "|".join(self.build(result, False)) # type: ignore
if path.endswith("/") and not new_path.endswith("/"):
new_path += "/"
if new_path.count("/") < path.count("/"):
# The URL will be encoded when MapAdapter.match
# handles the RequestPath raised below. Decode
# the URL here to avoid a double encoding.
path = url_unquote(new_path)
require_redirect = True
if require_redirect:
path = path.split("|", 1)[1]
raise RequestPath(path)
if self.alias and self.map.redirect_defaults:
raise RequestAliasRedirect(result)
return result
return None
@staticmethod
def _get_func_code(code: CodeType, name: str) -> t.Callable[..., t.Tuple[str, str]]:
globs: t.Dict[str, t.Any] = {}
locs: t.Dict[str, t.Any] = {}
exec(code, globs, locs)
return locs[name] # type: ignore
def _compile_builder(
self, append_unknown: bool = True
) -> t.Callable[..., t.Tuple[str, str]]:
defaults = self.defaults or {}
dom_ops: t.List[t.Tuple[bool, str]] = []
url_ops: t.List[t.Tuple[bool, str]] = []
opl = dom_ops
for is_dynamic, data in self._trace:
if data == "|" and opl is dom_ops:
opl = url_ops
continue
# this seems like a silly case to ever come up but:
# if a default is given for a value that appears in the rule,
# resolve it to a constant ahead of time
if is_dynamic and data in defaults:
data = self._converters[data].to_url(defaults[data])
opl.append((False, data))
elif not is_dynamic:
opl.append(
(False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+"))
)
else:
opl.append((True, data))
def _convert(elem: str) -> ast.stmt:
ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2
return ret
def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]:
parts = [
_convert(elem) if is_dynamic else ast.Str(s=elem)
for is_dynamic, elem in ops
]
parts = parts or [ast.Str("")]
# constant fold
ret = [parts[0]]
for p in parts[1:]:
if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
ret[-1] = ast.Str(ret[-1].s + p.s)
else:
ret.append(p)
return ret
dom_parts = _parts(dom_ops)
url_parts = _parts(url_ops)
if not append_unknown:
body = []
else:
body = [_IF_KWARGS_URL_ENCODE_AST]
url_parts.extend(_URL_ENCODE_AST_NAMES)
def _join(parts: t.List[ast.AST]) -> ast.AST:
if len(parts) == 1: # shortcut
return parts[0]
return ast.JoinedStr(parts)
body.append(
ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
)
pargs = [
elem
for is_dynamic, elem in dom_ops + url_ops
if is_dynamic and elem not in defaults
]
kargs = [str(k) for k in defaults]
func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore
func_ast.name = f"<builder:{self.rule!r}>"
func_ast.args.args.append(ast.arg(".self", None))
for arg in pargs + kargs:
func_ast.args.args.append(ast.arg(arg, None))
func_ast.args.kwarg = ast.arg(".kwargs", None)
for _ in kargs:
func_ast.args.defaults.append(ast.Str(""))
func_ast.body = body
# use `ast.parse` instead of `ast.Module` for better portability
# Python 3.8 changes the signature of `ast.Module`
module = ast.parse("")
module.body = [func_ast]
# mark everything as on line 1, offset 0
# less error-prone than `ast.fix_missing_locations`
# bad line numbers cause an assert to fail in debug builds
for node in ast.walk(module):
if "lineno" in node._attributes:
node.lineno = 1
if "col_offset" in node._attributes:
node.col_offset = 0
code = compile(module, "<werkzeug routing>", "exec")
return self._get_func_code(code, func_ast.name)
def build(
self, values: t.Mapping[str, t.Any], append_unknown: bool = True
) -> t.Optional[t.Tuple[str, str]]:
"""Assembles the relative url for that rule and the subdomain.
If building doesn't work for some reasons `None` is returned.
:internal:
"""
try:
if append_unknown:
return self._build_unknown(**values)
else:
return self._build(**values)
except ValidationError:
return None
def provides_defaults_for(self, rule: "Rule") -> bool:
"""Check if this rule has defaults for a given rule.
:internal:
"""
return bool(
not self.build_only
and self.defaults
and self.endpoint == rule.endpoint
and self != rule
and self.arguments == rule.arguments
)
def suitable_for(
self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None
) -> bool:
"""Check if the dict of values has enough data for url generation.
:internal:
"""
# if a method was given explicitly and that method is not supported
# by this rule, this rule is not suitable.
if (
method is not None
and self.methods is not None
and method not in self.methods
):
return False
defaults = self.defaults or ()
# all arguments required must be either in the defaults dict or
# the value dictionary otherwise it's not suitable
for key in self.arguments:
if key not in defaults and key not in values:
return False
# in case defaults are given we ensure that either the value was
# skipped or the value is the same as the default value.
if defaults:
for key, value in defaults.items():
if key in values and value != values[key]:
return False
return True
def match_compare_key(
self,
) -> t.Tuple[bool, int, t.Iterable[t.Tuple[int, int]], int, t.Iterable[int]]:
"""The match compare key for sorting.
Current implementation:
1. rules without any arguments come first for performance
reasons only as we expect them to match faster and some
common ones usually don't have any arguments (index pages etc.)
2. rules with more static parts come first so the second argument
is the negative length of the number of the static weights.
3. we order by static weights, which is a combination of index
and length
4. The more complex rules come first so the next argument is the
negative length of the number of argument weights.
5. lastly we order by the actual argument weights.
:internal:
"""
return (
bool(self.arguments),
-len(self._static_weights),
self._static_weights,
-len(self._argument_weights),
self._argument_weights,
)
def build_compare_key(self) -> t.Tuple[int, int, int]:
"""The build compare key for sorting.
:internal:
"""
return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
def __eq__(self, other: object) -> bool:
return isinstance(other, type(self)) and self._trace == other._trace
__hash__ = None # type: ignore
def __str__(self) -> str:
return self.rule
def __repr__(self) -> str:
if self.map is None:
return f"<{type(self).__name__} (unbound)>"
parts = []
for is_dynamic, data in self._trace:
if is_dynamic:
parts.append(f"<{data}>")
else:
parts.append(data)
parts = "".join(parts).lstrip("|")
methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>"
class BaseConverter:
"""Base class for all converters."""
regex = "[^/]+"
weight = 100
def __init__(self, map: "Map", *args: t.Any, **kwargs: t.Any) -> None:
self.map = map
def to_python(self, value: str) -> t.Any:
return value
def to_url(self, value: t.Any) -> str:
if isinstance(value, (bytes, bytearray)):
return _fast_url_quote(value)
return _fast_url_quote(str(value).encode(self.map.charset))
class UnicodeConverter(BaseConverter):
"""This converter is the default converter and accepts any string but
only one path segment. Thus the string can not include a slash.
This is the default validator.
Example::
Rule('/pages/<page>'),
Rule('/<string(length=2):lang_code>')
:param map: the :class:`Map`.
:param minlength: the minimum length of the string. Must be greater
or equal 1.
:param maxlength: the maximum length of the string.
:param length: the exact length of the string.
"""
def __init__(
self,
map: "Map",
minlength: int = 1,
maxlength: t.Optional[int] = None,
length: t.Optional[int] = None,
) -> None:
super().__init__(map)
if length is not None:
length_regex = f"{{{int(length)}}}"
else:
if maxlength is None:
maxlength_value = ""
else:
maxlength_value = str(int(maxlength))
length_regex = f"{{{int(minlength)},{maxlength_value}}}"
self.regex = f"[^/]{length_regex}"
class AnyConverter(BaseConverter):
"""Matches one of the items provided. Items can either be Python
identifiers or strings::
Rule('/<any(about, help, imprint, class, "foo,bar"):page_name>')
:param map: the :class:`Map`.
:param items: this function accepts the possible items as positional
arguments.
"""
def __init__(self, map: "Map", *items: str) -> None:
super().__init__(map)
self.regex = f"(?:{'|'.join([re.escape(x) for x in items])})"
class PathConverter(BaseConverter):
"""Like the default :class:`UnicodeConverter`, but it also matches
slashes. This is useful for wikis and similar applications::
Rule('/<path:wikipage>')
Rule('/<path:wikipage>/edit')
:param map: the :class:`Map`.
"""
regex = "[^/].*?"
weight = 200
class NumberConverter(BaseConverter):
"""Baseclass for `IntegerConverter` and `FloatConverter`.
:internal:
"""
weight = 50
num_convert: t.Callable = int
def __init__(
self,
map: "Map",
fixed_digits: int = 0,
min: t.Optional[int] = None,
max: t.Optional[int] = None,
signed: bool = False,
) -> None:
if signed:
self.regex = self.signed_regex
super().__init__(map)
self.fixed_digits = fixed_digits
self.min = min
self.max = max
self.signed = signed
def to_python(self, value: str) -> t.Any:
if self.fixed_digits and len(value) != self.fixed_digits:
raise ValidationError()
value = self.num_convert(value)
if (self.min is not None and value < self.min) or (
self.max is not None and value > self.max
):
raise ValidationError()
return value
def to_url(self, value: t.Any) -> str:
value = str(self.num_convert(value))
if self.fixed_digits:
value = value.zfill(self.fixed_digits)
return value
@property
def signed_regex(self) -> str:
return f"-?{self.regex}"
class IntegerConverter(NumberConverter):
"""This converter only accepts integer values::
Rule("/page/<int:page>")
By default it only accepts unsigned, positive values. The ``signed``
parameter will enable signed, negative values. ::
Rule("/page/<int(signed=True):page>")
:param map: The :class:`Map`.
:param fixed_digits: The number of fixed digits in the URL. If you
set this to ``4`` for example, the rule will only match if the
URL looks like ``/0001/``. The default is variable length.
:param min: The minimal value.
:param max: The maximal value.
:param signed: Allow signed (negative) values.
.. versionadded:: 0.15
The ``signed`` parameter.
"""
regex = r"\d+"
class FloatConverter(NumberConverter):
"""This converter only accepts floating point values::
Rule("/probability/<float:probability>")
By default it only accepts unsigned, positive values. The ``signed``
parameter will enable signed, negative values. ::
Rule("/offset/<float(signed=True):offset>")
:param map: The :class:`Map`.
:param min: The minimal value.
:param max: The maximal value.
:param signed: Allow signed (negative) values.
.. versionadded:: 0.15
The ``signed`` parameter.
"""
regex = r"\d+\.\d+"
num_convert = float
def __init__(
self,
map: "Map",
min: t.Optional[float] = None,
max: t.Optional[float] = None,
signed: bool = False,
) -> None:
super().__init__(map, min=min, max=max, signed=signed) # type: ignore
class UUIDConverter(BaseConverter):
"""This converter only accepts UUID strings::
Rule('/object/<uuid:identifier>')
.. versionadded:: 0.10
:param map: the :class:`Map`.
"""
regex = (
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}"
)
def to_python(self, value: str) -> uuid.UUID:
return uuid.UUID(value)
def to_url(self, value: uuid.UUID) -> str:
return str(value)
#: the default converter mapping for the map.
DEFAULT_CONVERTERS: t.Mapping[str, t.Type[BaseConverter]] = {
"default": UnicodeConverter,
"string": UnicodeConverter,
"any": AnyConverter,
"path": PathConverter,
"int": IntegerConverter,
"float": FloatConverter,
"uuid": UUIDConverter,
}
class Map:
"""The map class stores all the URL rules and some configuration
parameters. Some of the configuration values are only stored on the
`Map` instance since those affect all rules, others are just defaults
and can be overridden for each rule. Note that you have to specify all
arguments besides the `rules` as keyword arguments!
:param rules: sequence of url rules for this map.
:param default_subdomain: The default subdomain for rules without a
subdomain defined.
:param charset: charset of the url. defaults to ``"utf-8"``
:param strict_slashes: If a rule ends with a slash but the matched
URL does not, redirect to the URL with a trailing slash.
:param merge_slashes: Merge consecutive slashes when matching or
building URLs. Matches will redirect to the normalized URL.
Slashes in variable parts are not merged.
:param redirect_defaults: This will redirect to the default rule if it
wasn't visited that way. This helps creating
unique URLs.
:param converters: A dict of converters that adds additional converters
to the list of converters. If you redefine one
converter this will override the original one.
:param sort_parameters: If set to `True` the url parameters are sorted.
See `url_encode` for more details.
:param sort_key: The sort key function for `url_encode`.
:param encoding_errors: the error method to use for decoding
:param host_matching: if set to `True` it enables the host matching
feature and disables the subdomain one. If
enabled the `host` parameter to rules is used
instead of the `subdomain` one.
.. versionchanged:: 1.0
If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules
will match.
.. versionchanged:: 1.0
Added ``merge_slashes``.
.. versionchanged:: 0.7
Added ``encoding_errors`` and ``host_matching``.
.. versionchanged:: 0.5
Added ``sort_parameters`` and ``sort_key``.
"""
#: A dict of default converters to be used.
default_converters = ImmutableDict(DEFAULT_CONVERTERS)
#: The type of lock to use when updating.
#:
#: .. versionadded:: 1.0
lock_class = Lock
def __init__(
self,
rules: t.Optional[t.Iterable[RuleFactory]] = None,
default_subdomain: str = "",
charset: str = "utf-8",
strict_slashes: bool = True,
merge_slashes: bool = True,
redirect_defaults: bool = True,
converters: t.Optional[t.Mapping[str, t.Type[BaseConverter]]] = None,
sort_parameters: bool = False,
sort_key: t.Optional[t.Callable[[t.Any], t.Any]] = None,
encoding_errors: str = "replace",
host_matching: bool = False,
) -> None:
self._rules: t.List[Rule] = []
self._rules_by_endpoint: t.Dict[str, t.List[Rule]] = {}
self._remap = True
self._remap_lock = self.lock_class()
self.default_subdomain = default_subdomain
self.charset = charset
self.encoding_errors = encoding_errors
self.strict_slashes = strict_slashes
self.merge_slashes = merge_slashes
self.redirect_defaults = redirect_defaults
self.host_matching = host_matching
self.converters = self.default_converters.copy()
if converters:
self.converters.update(converters)
self.sort_parameters = sort_parameters
self.sort_key = sort_key
for rulefactory in rules or ():
self.add(rulefactory)
def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool:
"""Iterate over all rules and check if the endpoint expects
the arguments provided. This is for example useful if you have
some URLs that expect a language code and others that do not and
you want to wrap the builder a bit so that the current language
code is automatically added if not provided but endpoints expect
it.
:param endpoint: the endpoint to check.
:param arguments: this function accepts one or more arguments
as positional arguments. Each one of them is
checked.
"""
self.update()
arguments = set(arguments)
for rule in self._rules_by_endpoint[endpoint]:
if arguments.issubset(rule.arguments):
return True
return False
def iter_rules(self, endpoint: t.Optional[str] = None) -> t.Iterator[Rule]:
"""Iterate over all rules or the rules of an endpoint.
:param endpoint: if provided only the rules for that endpoint
are returned.
:return: an iterator
"""
self.update()
if endpoint is not None:
return iter(self._rules_by_endpoint[endpoint])
return iter(self._rules)
def add(self, rulefactory: RuleFactory) -> None:
"""Add a new rule or factory to the map and bind it. Requires that the
rule is not bound to another map.
:param rulefactory: a :class:`Rule` or :class:`RuleFactory`
"""
for rule in rulefactory.get_rules(self):
rule.bind(self)
self._rules.append(rule)
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
def bind(
self,
server_name: str,
script_name: t.Optional[str] = None,
subdomain: t.Optional[str] = None,
url_scheme: str = "http",
default_method: str = "GET",
path_info: t.Optional[str] = None,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
) -> "MapAdapter":
"""Return a new :class:`MapAdapter` with the details specified to the
call. Note that `script_name` will default to ``'/'`` if not further
specified or `None`. The `server_name` at least is a requirement
because the HTTP RFC requires absolute URLs for redirects and so all
redirect exceptions raised by Werkzeug will contain the full canonical
URL.
If no path_info is passed to :meth:`match` it will use the default path
info passed to bind. While this doesn't really make sense for
manual bind calls, it's useful if you bind a map to a WSGI
environment which already contains the path info.
`subdomain` will default to the `default_subdomain` for this map if
no defined. If there is no `default_subdomain` you cannot use the
subdomain feature.
.. versionchanged:: 1.0
If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules
will match.
.. versionchanged:: 0.15
``path_info`` defaults to ``'/'`` if ``None``.
.. versionchanged:: 0.8
``query_args`` can be a string.
.. versionchanged:: 0.7
Added ``query_args``.
"""
server_name = server_name.lower()
if self.host_matching:
if subdomain is not None:
raise RuntimeError("host matching enabled and a subdomain was provided")
elif subdomain is None:
subdomain = self.default_subdomain
if script_name is None:
script_name = "/"
if path_info is None:
path_info = "/"
try:
server_name = _encode_idna(server_name) # type: ignore
except UnicodeError as e:
raise BadHost() from e
return MapAdapter(
self,
server_name,
script_name,
subdomain,
url_scheme,
path_info,
default_method,
query_args,
)
def bind_to_environ(
self,
environ: t.Union["WSGIEnvironment", "Request"],
server_name: t.Optional[str] = None,
subdomain: t.Optional[str] = None,
) -> "MapAdapter":
"""Like :meth:`bind` but you can pass it an WSGI environment and it
will fetch the information from that dictionary. Note that because of
limitations in the protocol there is no way to get the current
subdomain and real `server_name` from the environment. If you don't
provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or
`HTTP_HOST` if provided) as used `server_name` with disabled subdomain
feature.
If `subdomain` is `None` but an environment and a server name is
provided it will calculate the current subdomain automatically.
Example: `server_name` is ``'example.com'`` and the `SERVER_NAME`
in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated
subdomain will be ``'staging.dev'``.
If the object passed as environ has an environ attribute, the value of
this attribute is used instead. This allows you to pass request
objects. Additionally `PATH_INFO` added as a default of the
:class:`MapAdapter` so that you don't have to pass the path info to
the match method.
.. versionchanged:: 1.0.0
If the passed server name specifies port 443, it will match
if the incoming scheme is ``https`` without a port.
.. versionchanged:: 1.0.0
A warning is shown when the passed server name does not
match the incoming WSGI server name.
.. versionchanged:: 0.8
This will no longer raise a ValueError when an unexpected server
name was passed.
.. versionchanged:: 0.5
previously this method accepted a bogus `calculate_subdomain`
parameter that did not have any effect. It was removed because
of that.
:param environ: a WSGI environment.
:param server_name: an optional server name hint (see above).
:param subdomain: optionally the current subdomain (see above).
"""
env = _get_environ(environ)
wsgi_server_name = get_host(env).lower()
scheme = env["wsgi.url_scheme"]
upgrade = any(
v.strip() == "upgrade"
for v in env.get("HTTP_CONNECTION", "").lower().split(",")
)
if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket":
scheme = "wss" if scheme == "https" else "ws"
if server_name is None:
server_name = wsgi_server_name
else:
server_name = server_name.lower()
# strip standard port to match get_host()
if scheme in {"http", "ws"} and server_name.endswith(":80"):
server_name = server_name[:-3]
elif scheme in {"https", "wss"} and server_name.endswith(":443"):
server_name = server_name[:-4]
if subdomain is None and not self.host_matching:
cur_server_name = wsgi_server_name.split(".")
real_server_name = server_name.split(".")
offset = -len(real_server_name)
if cur_server_name[offset:] != real_server_name:
# This can happen even with valid configs if the server was
# accessed directly by IP address under some situations.
# Instead of raising an exception like in Werkzeug 0.7 or
# earlier we go by an invalid subdomain which will result
# in a 404 error on matching.
warnings.warn(
f"Current server name {wsgi_server_name!r} doesn't match configured"
f" server name {server_name!r}",
stacklevel=2,
)
subdomain = "<invalid>"
else:
subdomain = ".".join(filter(None, cur_server_name[:offset]))
def _get_wsgi_string(name: str) -> t.Optional[str]:
val = env.get(name)
if val is not None:
return _wsgi_decoding_dance(val, self.charset)
return None
script_name = _get_wsgi_string("SCRIPT_NAME")
path_info = _get_wsgi_string("PATH_INFO")
query_args = _get_wsgi_string("QUERY_STRING")
return Map.bind(
self,
server_name,
script_name,
subdomain,
scheme,
env["REQUEST_METHOD"],
path_info,
query_args=query_args,
)
def update(self) -> None:
"""Called before matching and building to keep the compiled rules
in the correct order after things changed.
"""
if not self._remap:
return
with self._remap_lock:
if not self._remap:
return
self._rules.sort(key=lambda x: x.match_compare_key())
for rules in self._rules_by_endpoint.values():
rules.sort(key=lambda x: x.build_compare_key())
self._remap = False
def __repr__(self) -> str:
rules = self.iter_rules()
return f"{type(self).__name__}({pformat(list(rules))})"
class MapAdapter:
"""Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
the URL matching and building based on runtime information.
"""
def __init__(
self,
map: Map,
server_name: str,
script_name: str,
subdomain: t.Optional[str],
url_scheme: str,
path_info: str,
default_method: str,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
):
self.map = map
self.server_name = _to_str(server_name)
script_name = _to_str(script_name)
if not script_name.endswith("/"):
script_name += "/"
self.script_name = script_name
self.subdomain = _to_str(subdomain)
self.url_scheme = _to_str(url_scheme)
self.path_info = _to_str(path_info)
self.default_method = _to_str(default_method)
self.query_args = query_args
self.websocket = self.url_scheme in {"ws", "wss"}
def dispatch(
self,
view_func: t.Callable[[str, t.Mapping[str, t.Any]], "WSGIApplication"],
path_info: t.Optional[str] = None,
method: t.Optional[str] = None,
catch_http_exceptions: bool = False,
) -> "WSGIApplication":
"""Does the complete dispatching process. `view_func` is called with
the endpoint and a dict with the values for the view. It should
look up the view function, call it, and return a response object
or WSGI application. http exceptions are not caught by default
so that applications can display nicer error messages by just
catching them by hand. If you want to stick with the default
error messages you can pass it ``catch_http_exceptions=True`` and
it will catch the http exceptions.
Here a small example for the dispatch usage::
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import responder
from werkzeug.routing import Map, Rule
def on_index(request):
return Response('Hello from the index')
url_map = Map([Rule('/', endpoint='index')])
views = {'index': on_index}
@responder
def application(environ, start_response):
request = Request(environ)
urls = url_map.bind_to_environ(environ)
return urls.dispatch(lambda e, v: views[e](request, **v),
catch_http_exceptions=True)
Keep in mind that this method might return exception objects, too, so
use :class:`Response.force_type` to get a response object.
:param view_func: a function that is called with the endpoint as
first argument and the value dict as second. Has
to dispatch to the actual view function with this
information. (see above)
:param path_info: the path info to use for matching. Overrides the
path info specified on binding.
:param method: the HTTP method used for matching. Overrides the
method specified on binding.
:param catch_http_exceptions: set to `True` to catch any of the
werkzeug :class:`HTTPException`\\s.
"""
try:
try:
endpoint, args = self.match(path_info, method)
except RequestRedirect as e:
return e
return view_func(endpoint, args)
except HTTPException as e:
if catch_http_exceptions:
return e
raise
@typing.overload
def match( # type: ignore
self,
path_info: t.Optional[str] = None,
method: t.Optional[str] = None,
return_rule: "te.Literal[False]" = False,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
websocket: t.Optional[bool] = None,
) -> t.Tuple[str, t.Mapping[str, t.Any]]:
...
@typing.overload
def match(
self,
path_info: t.Optional[str] = None,
method: t.Optional[str] = None,
return_rule: "te.Literal[True]" = True,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
websocket: t.Optional[bool] = None,
) -> t.Tuple[Rule, t.Mapping[str, t.Any]]:
...
def match(
self,
path_info: t.Optional[str] = None,
method: t.Optional[str] = None,
return_rule: bool = False,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
websocket: t.Optional[bool] = None,
) -> t.Tuple[t.Union[str, Rule], t.Mapping[str, t.Any]]:
"""The usage is simple: you just pass the match method the current
path info as well as the method (which defaults to `GET`). The
following things can then happen:
- you receive a `NotFound` exception that indicates that no URL is
matching. A `NotFound` exception is also a WSGI application you
can call to get a default page not found page (happens to be the
same object as `werkzeug.exceptions.NotFound`)
- you receive a `MethodNotAllowed` exception that indicates that there
is a match for this URL but not for the current request method.
This is useful for RESTful applications.
- you receive a `RequestRedirect` exception with a `new_url`
attribute. This exception is used to notify you about a request
Werkzeug requests from your WSGI application. This is for example the
case if you request ``/foo`` although the correct URL is ``/foo/``
You can use the `RequestRedirect` instance as response-like object
similar to all other subclasses of `HTTPException`.
- you receive a ``WebsocketMismatch`` exception if the only
match is a WebSocket rule but the bind is an HTTP request, or
if the match is an HTTP rule but the bind is a WebSocket
request.
- you get a tuple in the form ``(endpoint, arguments)`` if there is
a match (unless `return_rule` is True, in which case you get a tuple
in the form ``(rule, arguments)``)
If the path info is not passed to the match method the default path
info of the map is used (defaults to the root URL if not defined
explicitly).
All of the exceptions raised are subclasses of `HTTPException` so they
can be used as WSGI responses. They will all render generic error or
redirect pages.
Here is a small example for matching:
>>> m = Map([
... Rule('/', endpoint='index'),
... Rule('/downloads/', endpoint='downloads/index'),
... Rule('/downloads/<int:id>', endpoint='downloads/show')
... ])
>>> urls = m.bind("example.com", "/")
>>> urls.match("/", "GET")
('index', {})
>>> urls.match("/downloads/42")
('downloads/show', {'id': 42})
And here is what happens on redirect and missing URLs:
>>> urls.match("/downloads")
Traceback (most recent call last):
...
RequestRedirect: http://example.com/downloads/
>>> urls.match("/missing")
Traceback (most recent call last):
...
NotFound: 404 Not Found
:param path_info: the path info to use for matching. Overrides the
path info specified on binding.
:param method: the HTTP method used for matching. Overrides the
method specified on binding.
:param return_rule: return the rule that matched instead of just the
endpoint (defaults to `False`).
:param query_args: optional query arguments that are used for
automatic redirects as string or dictionary. It's
currently not possible to use the query arguments
for URL matching.
:param websocket: Match WebSocket instead of HTTP requests. A
websocket request has a ``ws`` or ``wss``
:attr:`url_scheme`. This overrides that detection.
.. versionadded:: 1.0
Added ``websocket``.
.. versionchanged:: 0.8
``query_args`` can be a string.
.. versionadded:: 0.7
Added ``query_args``.
.. versionadded:: 0.6
Added ``return_rule``.
"""
self.map.update()
if path_info is None:
path_info = self.path_info
else:
path_info = _to_str(path_info, self.map.charset)
if query_args is None:
query_args = self.query_args or {}
method = (method or self.default_method).upper()
if websocket is None:
websocket = self.websocket
require_redirect = False
domain_part = self.server_name if self.map.host_matching else self.subdomain
path_part = f"/{path_info.lstrip('/')}" if path_info else ""
path = f"{domain_part}|{path_part}"
have_match_for = set()
websocket_mismatch = False
for rule in self.map._rules:
try:
rv = rule.match(path, method)
except RequestPath as e:
raise RequestRedirect(
self.make_redirect_url(
url_quote(e.path_info, self.map.charset, safe="/:|+"),
query_args,
)
) from None
except RequestAliasRedirect as e:
raise RequestRedirect(
self.make_alias_redirect_url(
path, rule.endpoint, e.matched_values, method, query_args
)
) from None
if rv is None:
continue
if rule.methods is not None and method not in rule.methods:
have_match_for.update(rule.methods)
continue
if rule.websocket != websocket:
websocket_mismatch = True
continue
if self.map.redirect_defaults:
redirect_url = self.get_default_redirect(rule, method, rv, query_args)
if redirect_url is not None:
raise RequestRedirect(redirect_url)
if rule.redirect_to is not None:
if isinstance(rule.redirect_to, str):
def _handle_match(match: t.Match[str]) -> str:
value = rv[match.group(1)] # type: ignore
return rule._converters[match.group(1)].to_url(value)
redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to)
else:
redirect_url = rule.redirect_to(self, **rv)
if self.subdomain:
netloc = f"{self.subdomain}.{self.server_name}"
else:
netloc = self.server_name
raise RequestRedirect(
url_join(
f"{self.url_scheme or 'http'}://{netloc}{self.script_name}",
redirect_url,
)
)
if require_redirect:
raise RequestRedirect(
self.make_redirect_url(
url_quote(path_info, self.map.charset, safe="/:|+"), query_args
)
)
if return_rule:
return rule, rv
else:
return rule.endpoint, rv
if have_match_for:
raise MethodNotAllowed(valid_methods=list(have_match_for))
if websocket_mismatch:
raise WebsocketMismatch()
raise NotFound()
def test(
self, path_info: t.Optional[str] = None, method: t.Optional[str] = None
) -> bool:
"""Test if a rule would match. Works like `match` but returns `True`
if the URL matches, or `False` if it does not exist.
:param path_info: the path info to use for matching. Overrides the
path info specified on binding.
:param method: the HTTP method used for matching. Overrides the
method specified on binding.
"""
try:
self.match(path_info, method)
except RequestRedirect:
pass
except HTTPException:
return False
return True
def allowed_methods(self, path_info: t.Optional[str] = None) -> t.Iterable[str]:
"""Returns the valid methods that match for a given path.
.. versionadded:: 0.7
"""
try:
self.match(path_info, method="--")
except MethodNotAllowed as e:
return e.valid_methods # type: ignore
except HTTPException:
pass
return []
def get_host(self, domain_part: t.Optional[str]) -> str:
"""Figures out the full host name for the given domain part. The
domain part is a subdomain in case host matching is disabled or
a full host name.
"""
if self.map.host_matching:
if domain_part is None:
return self.server_name
return _to_str(domain_part, "ascii")
subdomain = domain_part
if subdomain is None:
subdomain = self.subdomain
else:
subdomain = _to_str(subdomain, "ascii")
if subdomain:
return f"{subdomain}.{self.server_name}"
else:
return self.server_name
def get_default_redirect(
self,
rule: Rule,
method: str,
values: t.MutableMapping[str, t.Any],
query_args: t.Union[t.Mapping[str, t.Any], str],
) -> t.Optional[str]:
"""A helper that returns the URL to redirect to if it finds one.
This is used for default redirecting only.
:internal:
"""
assert self.map.redirect_defaults
for r in self.map._rules_by_endpoint[rule.endpoint]:
# every rule that comes after this one, including ourself
# has a lower priority for the defaults. We order the ones
# with the highest priority up for building.
if r is rule:
break
if r.provides_defaults_for(rule) and r.suitable_for(values, method):
values.update(r.defaults) # type: ignore
domain_part, path = r.build(values) # type: ignore
return self.make_redirect_url(path, query_args, domain_part=domain_part)
return None
def encode_query_args(self, query_args: t.Union[t.Mapping[str, t.Any], str]) -> str:
if not isinstance(query_args, str):
return url_encode(query_args, self.map.charset)
return query_args
def make_redirect_url(
self,
path_info: str,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
domain_part: t.Optional[str] = None,
) -> str:
"""Creates a redirect URL.
:internal:
"""
if query_args:
suffix = f"?{self.encode_query_args(query_args)}"
else:
suffix = ""
scheme = self.url_scheme or "http"
host = self.get_host(domain_part)
path = posixpath.join(self.script_name.strip("/"), path_info.lstrip("/"))
return f"{scheme}://{host}/{path}{suffix}"
def make_alias_redirect_url(
self,
path: str,
endpoint: str,
values: t.Mapping[str, t.Any],
method: str,
query_args: t.Union[t.Mapping[str, t.Any], str],
) -> str:
"""Internally called to make an alias redirect URL."""
url = self.build(
endpoint, values, method, append_unknown=False, force_external=True
)
if query_args:
url += f"?{self.encode_query_args(query_args)}"
assert url != path, "detected invalid alias setting. No canonical URL found"
return url
def _partial_build(
self,
endpoint: str,
values: t.Mapping[str, t.Any],
method: t.Optional[str],
append_unknown: bool,
) -> t.Optional[t.Tuple[str, str, bool]]:
"""Helper for :meth:`build`. Returns subdomain and path for the
rule that accepts this endpoint, values and method.
:internal:
"""
# in case the method is none, try with the default method first
if method is None:
rv = self._partial_build(
endpoint, values, self.default_method, append_unknown
)
if rv is not None:
return rv
# Default method did not match or a specific method is passed.
# Check all for first match with matching host. If no matching
# host is found, go with first result.
first_match = None
for rule in self.map._rules_by_endpoint.get(endpoint, ()):
if rule.suitable_for(values, method):
build_rv = rule.build(values, append_unknown)
if build_rv is not None:
rv = (build_rv[0], build_rv[1], rule.websocket)
if self.map.host_matching:
if rv[0] == self.server_name:
return rv
elif first_match is None:
first_match = rv
else:
return rv
return first_match
def build(
self,
endpoint: str,
values: t.Optional[t.Mapping[str, t.Any]] = None,
method: t.Optional[str] = None,
force_external: bool = False,
append_unknown: bool = True,
url_scheme: t.Optional[str] = None,
) -> str:
"""Building URLs works pretty much the other way round. Instead of
`match` you call `build` and pass it the endpoint and a dict of
arguments for the placeholders.
The `build` function also accepts an argument called `force_external`
which, if you set it to `True` will force external URLs. Per default
external URLs (include the server name) will only be used if the
target URL is on a different subdomain.
>>> m = Map([
... Rule('/', endpoint='index'),
... Rule('/downloads/', endpoint='downloads/index'),
... Rule('/downloads/<int:id>', endpoint='downloads/show')
... ])
>>> urls = m.bind("example.com", "/")
>>> urls.build("index", {})
'/'
>>> urls.build("downloads/show", {'id': 42})
'/downloads/42'
>>> urls.build("downloads/show", {'id': 42}, force_external=True)
'http://example.com/downloads/42'
Because URLs cannot contain non ASCII data you will always get
bytes back. Non ASCII characters are urlencoded with the
charset defined on the map instance.
Additional values are converted to strings and appended to the URL as
URL querystring parameters:
>>> urls.build("index", {'q': 'My Searchstring'})
'/?q=My+Searchstring'
When processing those additional values, lists are furthermore
interpreted as multiple values (as per
:py:class:`werkzeug.datastructures.MultiDict`):
>>> urls.build("index", {'q': ['a', 'b', 'c']})
'/?q=a&q=b&q=c'
Passing a ``MultiDict`` will also add multiple values:
>>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b'))))
'/?p=z&q=a&q=b'
If a rule does not exist when building a `BuildError` exception is
raised.
The build method accepts an argument called `method` which allows you
to specify the method you want to have an URL built for if you have
different methods for the same endpoint specified.
:param endpoint: the endpoint of the URL to build.
:param values: the values for the URL to build. Unhandled values are
appended to the URL as query parameters.
:param method: the HTTP method for the rule if there are different
URLs for different methods on the same endpoint.
:param force_external: enforce full canonical external URLs. If the URL
scheme is not provided, this will generate
a protocol-relative URL.
:param append_unknown: unknown parameters are appended to the generated
URL as query string argument. Disable this
if you want the builder to ignore those.
:param url_scheme: Scheme to use in place of the bound
:attr:`url_scheme`.
.. versionchanged:: 2.0
Added the ``url_scheme`` parameter.
.. versionadded:: 0.6
Added the ``append_unknown`` parameter.
"""
self.map.update()
if values:
temp_values: t.Dict[str, t.Union[t.List[t.Any], t.Any]] = {}
always_list = isinstance(values, MultiDict)
key: str
value: t.Optional[t.Union[t.List[t.Any], t.Any]]
# For MultiDict, dict.items(values) is like values.lists()
# without the call or list coercion overhead.
for key, value in dict.items(values): # type: ignore
if value is None:
continue
if always_list or isinstance(value, (list, tuple)):
value = [v for v in value if v is not None]
if not value:
continue
if len(value) == 1:
value = value[0]
temp_values[key] = value
values = temp_values
else:
values = {}
rv = self._partial_build(endpoint, values, method, append_unknown)
if rv is None:
raise BuildError(endpoint, values, method, self)
domain_part, path, websocket = rv
host = self.get_host(domain_part)
if url_scheme is None:
url_scheme = self.url_scheme
# Always build WebSocket routes with the scheme (browsers
# require full URLs). If bound to a WebSocket, ensure that HTTP
# routes are built with an HTTP scheme.
secure = url_scheme in {"https", "wss"}
if websocket:
force_external = True
url_scheme = "wss" if secure else "ws"
elif url_scheme:
url_scheme = "https" if secure else "http"
# shortcut this.
if not force_external and (
(self.map.host_matching and host == self.server_name)
or (not self.map.host_matching and domain_part == self.subdomain)
):
return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}"
scheme = f"{url_scheme}:" if url_scheme else ""
return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}"
|