Pyjoyment 0.1.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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Path.py ADDED
@@ -0,0 +1,419 @@
1
+ """
2
+ Pyjo.Path - Path
3
+ ================
4
+ ::
5
+
6
+ from Pyjo.Path import Path
7
+
8
+ # Parse
9
+ path = Path('/foo%2Fbar%3B/baz.html')
10
+ print(path[0])
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjo.Path
15
+
16
+ # Parse
17
+ path = Pyjo.Path.new('/foo%2Fbar%3B/baz.html')
18
+ print(path[0])
19
+
20
+ # Build
21
+ path = Pyjo.Path.new('/i/♥')
22
+ path.parts.append('pyjoyment')
23
+ print(path)
24
+
25
+ :mod:`Pyjo.Path` is a container for paths used by :mod:`Pyjo.URL`, based on
26
+ :rfc:`3986`.
27
+
28
+ See also
29
+ --------
30
+
31
+ :mod:`Pyjoyment`, https://mojolicious.org.
32
+
33
+ Ported from `Mojo::Path <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Path.pm>`_
34
+ of Mojolicious 9.49.
35
+
36
+ Classes
37
+ -------
38
+ """
39
+
40
+ import Pyjo.Base
41
+ import Pyjo.String.Mixin
42
+
43
+ from Pyjo.Util import decode, url_escape, url_unescape
44
+
45
+ import re
46
+
47
+ from collections.abc import Iterator
48
+ from typing import Self
49
+
50
+
51
+ class Pyjo_Path(Pyjo.Base.object, Pyjo.String.Mixin.object):
52
+ """
53
+ :mod:`Pyjo.Path` inherits all methods from :mod:`Pyjo.Base` and
54
+ :mod:`Pyjo.String.Mixin` and implements the following new ones.
55
+ """
56
+
57
+ def __init__(self, path: 'str | bytes | Pyjo_Path | None' = None) -> None:
58
+ """::
59
+
60
+ path = Pyjo.Path.new()
61
+ path = Pyjo.Path.new('/foo%2Fbar%3B/baz.html')
62
+
63
+ Construct a new :mod:`Pyjo.Path` object and :meth:`parse` path if necessary.
64
+ """
65
+ self.charset: str | None = 'UTF-8'
66
+ """::
67
+
68
+ charset = path.charset
69
+ path.charset = 'UTF-8'
70
+
71
+ Charset used for encoding and decoding, defaults to ``UTF-8``. ::
72
+
73
+ # Disable encoding and decoding
74
+ path.charset = None
75
+ """
76
+
77
+ self._path: str | bytes | None = None
78
+ self._parts: list[str] | None = None
79
+ self._leading_slash = False
80
+ self._trailing_slash = False
81
+
82
+ if path is not None:
83
+ self.parse(path)
84
+
85
+ def __bool__(self) -> bool:
86
+ """::
87
+
88
+ boolean = bool(path)
89
+
90
+ Always true.
91
+ """
92
+ return True
93
+
94
+ def __getitem__(self, index: int) -> str:
95
+ """::
96
+
97
+ part = path[0]
98
+
99
+ Alias for :attr:`parts`. Note that this will normalize the path and that
100
+ ``%2F`` will be treated as ``/`` for security reasons. ::
101
+
102
+ print(path[0])
103
+ """
104
+ return self.parts[index]
105
+
106
+ def __iter__(self) -> Iterator[str]:
107
+ """::
108
+
109
+ parts = list(path)
110
+
111
+ Alias for :attr:`parts`. Note that this will normalize the path and that
112
+ ``%2F`` will be treated as ``/`` for security reasons. ::
113
+
114
+ for part in path:
115
+ print(part)
116
+ """
117
+ return iter(self.parts)
118
+
119
+ def __len__(self) -> int:
120
+ """::
121
+
122
+ size = len(path)
123
+
124
+ Alias for :attr:`parts`. Note that this will normalize the path and that
125
+ ``%2F`` will be treated as ``/`` for security reasons.
126
+ """
127
+ return len(self.parts)
128
+
129
+ def __str__(self) -> str:
130
+ """::
131
+
132
+ string = str(path)
133
+
134
+ Alias for :meth:`to_str`.
135
+ """
136
+ return self.to_str()
137
+
138
+ def canonicalize(self) -> Self:
139
+ """::
140
+
141
+ path = path.canonicalize()
142
+
143
+ Canonicalize path by resolving ``.`` and ``..``, in addition ``...`` will be
144
+ treated as ``.`` to protect from path traversal attacks. ::
145
+
146
+ # "/foo/baz"
147
+ Pyjo.Path.new('/foo/./bar/../baz').canonicalize()
148
+
149
+ # "/../baz"
150
+ Pyjo.Path.new('/foo/../bar/../../baz').canonicalize()
151
+
152
+ # "/foo/bar"
153
+ Pyjo.Path.new('/foo/.../bar').canonicalize()
154
+ """
155
+ parts = self.parts
156
+ i = 0
157
+ while i < len(parts):
158
+ if parts[i] in ('', '.', '...'):
159
+ del parts[i]
160
+ elif i < 1 or parts[i] != '..' or parts[i - 1] == '..':
161
+ i += 1
162
+ else:
163
+ i -= 1
164
+ del parts[i : i + 2]
165
+
166
+ if not parts:
167
+ self.trailing_slash = False
168
+ return self
169
+
170
+ def clone(self) -> Self:
171
+ """::
172
+
173
+ clone = path.clone()
174
+
175
+ Return a new :mod:`Pyjo.Path` object cloned from this path.
176
+ """
177
+ clone = self.new()
178
+ clone.charset = self.charset
179
+ if self._parts is not None:
180
+ clone._leading_slash = self._leading_slash
181
+ clone._trailing_slash = self._trailing_slash
182
+ clone._parts = list(self._parts)
183
+ else:
184
+ clone._path = self._path
185
+ return clone
186
+
187
+ def contains(self, prefix: str) -> bool:
188
+ """::
189
+
190
+ boolean = path.contains('/i/♥/pyjoyment')
191
+
192
+ Check if path contains given prefix. ::
193
+
194
+ # True
195
+ Pyjo.Path.new('/foo/bar').contains('/')
196
+ Pyjo.Path.new('/foo/bar').contains('/foo')
197
+ Pyjo.Path.new('/foo/bar').contains('/foo/bar')
198
+
199
+ # False
200
+ Pyjo.Path.new('/foo/bar').contains('/f')
201
+ Pyjo.Path.new('/foo/bar').contains('/bar')
202
+ Pyjo.Path.new('/foo/bar').contains('/whatever')
203
+ """
204
+ return prefix == '/' or re.match(re.escape(prefix) + r'(?:/|$)', self.to_route()) is not None
205
+
206
+ @property
207
+ def leading_slash(self) -> bool:
208
+ """::
209
+
210
+ boolean = path.leading_slash
211
+ path.leading_slash = boolean
212
+
213
+ Path has a leading slash. Note that this method will normalize the path and
214
+ that ``%2F`` will be treated as ``/`` for security reasons. ::
215
+
216
+ # "/foo/bar"
217
+ Pyjo.Path.new('foo/bar').set(leading_slash=True)
218
+
219
+ # "foo/bar"
220
+ Pyjo.Path.new('/foo/bar').set(leading_slash=False)
221
+ """
222
+ self._parse()
223
+ return self._leading_slash
224
+
225
+ @leading_slash.setter
226
+ def leading_slash(self, value: bool) -> None:
227
+ self._parse()
228
+ self._leading_slash = value
229
+
230
+ def merge(self, path: 'str | bytes | Pyjo_Path') -> Self:
231
+ """::
232
+
233
+ path = path.merge('/foo/bar')
234
+ path = path.merge('foo/bar')
235
+ path = path.merge(Pyjo.Path.new())
236
+
237
+ Merge paths. Note that this method will normalize both paths if necessary and
238
+ that ``%2F`` will be treated as ``/`` for security reasons. ::
239
+
240
+ # "/baz/yada"
241
+ Pyjo.Path.new('/foo/bar').merge('/baz/yada')
242
+
243
+ # "/foo/baz/yada"
244
+ Pyjo.Path.new('/foo/bar').merge('baz/yada')
245
+
246
+ # "/foo/bar/baz/yada"
247
+ Pyjo.Path.new('/foo/bar/').merge('baz/yada')
248
+ """
249
+ # Replace
250
+ # Not in Mojolicious: bytes, encoded with the charset
251
+ if path.startswith(b'/') if isinstance(path, bytes) else str(path).startswith('/'):
252
+ return self.parse(path)
253
+
254
+ # Merge
255
+ if not self.trailing_slash and self.parts:
256
+ self.parts.pop()
257
+ other = self.new(path)
258
+ self.parts.extend(other.parts)
259
+ self.trailing_slash = other.trailing_slash
260
+ return self
261
+
262
+ def parse(self, path: 'str | bytes | Pyjo_Path') -> Self:
263
+ """::
264
+
265
+ path = path.parse('/foo%2Fbar%3B/baz.html')
266
+
267
+ Parse path, given as characters or as bytes encoded with :attr:`charset`.
268
+ """
269
+ # Not in Mojolicious: bytes, encoded with the charset
270
+ self._path = path if isinstance(path, bytes) else str(path)
271
+ self._parts = None
272
+ self._leading_slash = self._trailing_slash = False
273
+ return self
274
+
275
+ @property
276
+ def parts(self) -> list[str]:
277
+ """::
278
+
279
+ parts = path.parts
280
+ path.parts = ['foo', 'bar', 'baz']
281
+
282
+ The path parts. Note that this method will normalize the path and that
283
+ ``%2F`` will be treated as ``/`` for security reasons. ::
284
+
285
+ # Part with slash
286
+ path.parts.append('foo/bar')
287
+ """
288
+ return self._parse()
289
+
290
+ @parts.setter
291
+ def parts(self, value: list[str]) -> None:
292
+ self._parse()
293
+ self._parts = value
294
+
295
+ def to_abs_str(self) -> str:
296
+ """::
297
+
298
+ string = path.to_abs_str()
299
+
300
+ Turn path into an absolute string. ::
301
+
302
+ # "/i/%E2%99%A5/pyjoyment"
303
+ Pyjo.Path.new('/i/%E2%99%A5/pyjoyment').to_abs_str()
304
+ Pyjo.Path.new('i/%E2%99%A5/pyjoyment').to_abs_str()
305
+ """
306
+ path = self.to_str()
307
+ return path if path.startswith('/') else f'/{path}'
308
+
309
+ def to_dir(self) -> Self:
310
+ """::
311
+
312
+ dir = route.to_dir()
313
+
314
+ Clone path and remove everything after the right-most slash. ::
315
+
316
+ # "/i/%E2%99%A5/"
317
+ Pyjo.Path.new('/i/%E2%99%A5/pyjoyment').to_dir().to_abs_str()
318
+
319
+ # "i/%E2%99%A5/"
320
+ Pyjo.Path.new('i/%E2%99%A5/pyjoyment').to_dir().to_abs_str()
321
+ """
322
+ clone = self.clone()
323
+ if not clone.trailing_slash and clone.parts:
324
+ clone.parts.pop()
325
+ clone.trailing_slash = bool(clone.parts)
326
+ return clone
327
+
328
+ def to_route(self) -> str:
329
+ """::
330
+
331
+ route = path.to_route()
332
+
333
+ Turn path into a route. ::
334
+
335
+ # "/i/♥/pyjoyment"
336
+ Pyjo.Path.new('/i/%E2%99%A5/pyjoyment').to_route()
337
+ Pyjo.Path.new('i/%E2%99%A5/pyjoyment').to_route()
338
+ """
339
+ clone = self.clone()
340
+ return '/' + '/'.join([*clone.parts, ''] if clone.trailing_slash else clone.parts)
341
+
342
+ def to_str(self) -> str:
343
+ """::
344
+
345
+ string = path.to_str()
346
+
347
+ Turn path into a string. ::
348
+
349
+ # "/i/%E2%99%A5/pyjoyment"
350
+ Pyjo.Path.new('/i/%E2%99%A5/pyjoyment').to_str()
351
+
352
+ # "i/%E2%99%A5/pyjoyment"
353
+ Pyjo.Path.new('i/%E2%99%A5/pyjoyment').to_str()
354
+ """
355
+ # Path
356
+ if self._path is not None:
357
+ return url_escape(self._encode(self._path), rb"^A-Za-z0-9\-._~!$&'()*+,;=%:@/").decode('ascii')
358
+
359
+ # Build path
360
+ path = '/'.join(
361
+ url_escape(self._encode(p), rb"^A-Za-z0-9\-._~!$&'()*+,;=:@").decode('ascii') for p in self.parts
362
+ )
363
+ if self.leading_slash:
364
+ path = f'/{path}'
365
+ if self.trailing_slash:
366
+ path = f'{path}/'
367
+ return path
368
+
369
+ @property
370
+ def trailing_slash(self) -> bool:
371
+ """::
372
+
373
+ boolean = path.trailing_slash
374
+ path.trailing_slash = boolean
375
+
376
+ Path has a trailing slash. Note that this method will normalize the path and
377
+ that ``%2F`` will be treated as ``/`` for security reasons. ::
378
+
379
+ # "/foo/bar/"
380
+ Pyjo.Path.new('/foo/bar').set(trailing_slash=True)
381
+
382
+ # "/foo/bar"
383
+ Pyjo.Path.new('/foo/bar/').set(trailing_slash=False)
384
+ """
385
+ self._parse()
386
+ return self._trailing_slash
387
+
388
+ @trailing_slash.setter
389
+ def trailing_slash(self, value: bool) -> None:
390
+ self._parse()
391
+ self._trailing_slash = value
392
+
393
+ def _encode(self, string: str | bytes) -> bytes:
394
+ # Without a charset, a character is a byte, as in a Perl byte string
395
+ return string if isinstance(string, bytes) else string.encode(self.charset or 'latin-1')
396
+
397
+ def _decode(self, path: str | bytes) -> str:
398
+ # What does not decode stays characters 0-255, as a Perl byte string
399
+ if (decoded := decode(self.charset or 'latin-1', path)) is not None:
400
+ return decoded
401
+ return path if isinstance(path, str) else path.decode('latin-1')
402
+
403
+ def _parse(self) -> list[str]:
404
+ if self._parts is None:
405
+ path = self._decode(url_unescape(self._path or ''))
406
+ self._path = None
407
+
408
+ self._leading_slash = path.startswith('/')
409
+ path = path.removeprefix('/')
410
+ self._trailing_slash = path.endswith('/')
411
+ path = path.removesuffix('/')
412
+ self._parts = path.split('/') if path else []
413
+
414
+ return self._parts
415
+
416
+
417
+ new = Pyjo_Path.new
418
+ object = Pyjo_Path
419
+ Path = Pyjo_Path