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/YAML.py ADDED
@@ -0,0 +1,1167 @@
1
+ """
2
+ Pyjo.YAML - Minimal YAML loader and dumper
3
+ ==========================================
4
+ ::
5
+
6
+ from Pyjo.YAML import dump, dump_all, load, load_all
7
+
8
+ config = load(\"\"\"\\
9
+ foo: bar
10
+ list:
11
+ - one
12
+ - two
13
+ \"\"\")
14
+
15
+ documents = load_all('--- first\\n--- second\\n')
16
+
17
+ string = dump({'foo': 'bar', 'list': ['one', 'two']})
18
+
19
+ :mod:`Pyjo.YAML` loads and dumps the YAML that configuration files and
20
+ workflows use, a subset of `YAML 1.2 <https://yaml.org>`_, which
21
+ :mod:`Pyjoyment.Plugin.YAMLConfig` reads, as Python has no YAML in its
22
+ standard library:
23
+
24
+ * mappings and sequences in the block style, nested by their indentation, and
25
+ a mapping or a sequence that starts in an item of a sequence
26
+ (``- name: value``, ``- - item``);
27
+ * flow sequences and mappings, ``[a, b]`` and ``{a: 1}``, nested, and over
28
+ several lines;
29
+ * scalars, plain, in single quotes (with ``''`` for a quote) and in double
30
+ quotes (with the escapes ``\\n``, ``\\t``, ``\\xHH``, ``\\uHHHH`` and the
31
+ others of YAML),
32
+ over several lines too, and the literal ``|`` and folded ``>`` block
33
+ scalars, with their empty lines, and the indicators ``-`` and ``+`` that
34
+ strip and keep the final line breaks;
35
+ * comments, documents that ``---`` starts and ``...`` ends, a scalar after
36
+ ``---`` and the directive ``%YAML``.
37
+
38
+ A plain scalar has the type of the core schema of YAML 1.2: ``null`` and ``~``
39
+ are ``None``, ``true`` and ``false`` are booleans, and numbers, with ``0o`` and
40
+ ``0x`` too, ``.inf`` and ``.nan``, are :class:`int` and :class:`float`, as
41
+ ``404:`` is a key of :class:`int`. The words and the forms of YAML 1.1, as
42
+ ``yes``, ``off``, ``0755`` for an octal number and an implicit date, are not of
43
+ the core schema, and stay strings or decimal numbers. A quoted scalar and a
44
+ block scalar are strings. ::
45
+
46
+ # {'foo': [1, True, 'yes', '2']}
47
+ load("foo:\\n - 1\\n - true\\n - yes\\n - '2'\\n")
48
+
49
+ A standard tag gives a scalar its type: ``!!str``, ``!!int``, ``!!float``,
50
+ ``!!bool`` and ``!!null`` of YAML 1.2, and ``!!binary``, Base64 for
51
+ :class:`bytes`, and ``!!timestamp``, ISO 8601 for :class:`datetime.date` and
52
+ :class:`datetime.datetime`, of YAML 1.1. ::
53
+
54
+ # {'port': '8080', 'key': b'hello', 'day': datetime.date(2001, 12, 14)}
55
+ load('port: !!str 8080\\nkey: !!binary aGVsbG8=\\nday: !!timestamp 2001-12-14\\n')
56
+
57
+ An anchor ``&name`` names a node, a value or a key, in a block or a flow
58
+ collection, and an alias ``*name`` after it in the same document is the same
59
+ object; an anchor defined again names the node of the aliases after it. The
60
+ merge key ``<<`` of YAML 1.1 puts the keys of the mapping that it names, or of
61
+ a sequence of mappings, the earlier ones first, into the mapping, under the
62
+ keys of the mapping itself. ::
63
+
64
+ # {'base': {'a': 1, 'b': 1}, 'copy': {'a': 1, 'b': 2}}
65
+ load('base: &base\\n a: 1\\n b: 1\\ncopy:\\n <<: *base\\n b: 2\\n')
66
+
67
+ Other tags, complex keys and the indicator of the indentation of a block
68
+ scalar are not supported, and raise an error, as do lines that the subset
69
+ does not know.
70
+
71
+ :func:`dump` writes what the loader reads, in the block style of
72
+ configuration files: a :class:`dict` is a mapping, a :class:`list` and a
73
+ :class:`tuple` are a sequence, ``{}`` and ``[]`` when they are empty, and a
74
+ string is plain where it reads back as the same string, quoted otherwise, and
75
+ a literal block scalar when it has several lines. :class:`bytes` and dates
76
+ have the tags ``!!binary`` and ``!!timestamp``, and another object is what
77
+ :mod:`Pyjo.JSON` encodes, the value of its method ``__json__`` or its string.
78
+ A :class:`dict` or a :class:`list` that is there more than once has an anchor
79
+ where it is first and an alias after it, which the loader reads back as the
80
+ same object, and one inside itself raises an error, as the loader cannot read
81
+ it back; a :class:`tuple` has no identity of its own, as the compiler can
82
+ share one that is a constant, and it is written again. ::
83
+
84
+ # 'name: Pyjoyment\\nlist:\\n - one\\n - 2\\nrun: |\\n make\\n'
85
+ dump({'name': 'Pyjoyment', 'list': ['one', 2], 'run': 'make\\n'})
86
+
87
+ See also
88
+ --------
89
+
90
+ :mod:`Pyjoyment.Plugin.YAMLConfig`, https://mojolicious.org.
91
+
92
+ Inspired by `CPAN::Meta::YAML <https://metacpan.org/pod/CPAN::Meta::YAML>`_
93
+ of Adam Kennedy and David Golden, which Mojolicious uses, whose regular
94
+ expressions of scalars and keys it borrows, with thanks.
95
+ """
96
+
97
+ import Pyjo.Exception
98
+
99
+ import base64
100
+ import binascii
101
+ import builtins
102
+ import collections
103
+ import datetime
104
+ import math
105
+ import re
106
+
107
+ from collections.abc import Callable, Iterable
108
+ from typing import Any, Literal, cast
109
+
110
+
111
+ # The regular expressions of CPAN::Meta::YAML
112
+ _DOUBLE_QUOTED = r'"([^\\"]*(?:\\.[^\\"]*)*)"'
113
+ _SINGLE_QUOTED = r"'([^']*(?:''[^']*)*)'"
114
+ _UNQUOTED_KEY = r'([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*:(?:\s+|$))'
115
+ _TRAILING_COMMENT = r'(?:\s+#.*)?'
116
+ _KEY_VALUE_SEPARATOR = r'\s*:(?:\s+(?:#.*)?|$)'
117
+
118
+ _RE_DOUBLE = re.compile(rf'^{_DOUBLE_QUOTED}{_TRAILING_COMMENT}\Z')
119
+ _RE_SINGLE = re.compile(rf'^{_SINGLE_QUOTED}{_TRAILING_COMMENT}\Z')
120
+ _RE_KEY_DOUBLE = re.compile(rf'^\s*{_DOUBLE_QUOTED}{_KEY_VALUE_SEPARATOR}')
121
+ _RE_KEY_SINGLE = re.compile(rf'^\s*{_SINGLE_QUOTED}{_KEY_VALUE_SEPARATOR}')
122
+ _RE_KEY_UNQUOTED = re.compile(rf'^\s*{_UNQUOTED_KEY}{_KEY_VALUE_SEPARATOR}')
123
+ _RE_BLANK = re.compile(r'^\s*(?:#.*)?\Z')
124
+
125
+ # Not in CPAN::Meta::YAML: the name of an anchor or an alias, an anchor of a key and an alias as a key
126
+ _ANCHOR = r'[^\s,\[\]{}]+'
127
+ _RE_KEY_ALIAS = re.compile(rf'^\s*\*({_ANCHOR}){_KEY_VALUE_SEPARATOR}')
128
+ _RE_KEY_ANCHOR = re.compile(rf'^(\s*)&({_ANCHOR})\s+')
129
+
130
+ _ESCAPES = {
131
+ '0': '\x00',
132
+ 'L': '\u2028',
133
+ 'N': '\x85',
134
+ 'P': '\u2029',
135
+ 'a': '\x07',
136
+ 'b': '\x08',
137
+ 'e': '\x1b',
138
+ 'f': '\x0c',
139
+ 'n': '\x0a',
140
+ 'r': '\x0d',
141
+ 't': '\x09',
142
+ 'v': '\x0b',
143
+ 'z': '\x00',
144
+ '\\': '\\',
145
+ '_': '\xa0',
146
+ }
147
+
148
+
149
+ def dump(
150
+ value: Any,
151
+ *,
152
+ indent: int = 2,
153
+ indent_sequences: bool = True,
154
+ quote: Literal["'", '"'] | None = None,
155
+ quote_keys: Literal["'", '"'] | None = None,
156
+ multiline: bool = True,
157
+ explicit_start: bool = False,
158
+ version: Literal['1.1', '1.2'] = '1.1',
159
+ anchors: bool = True,
160
+ sort_keys: bool = False,
161
+ ) -> str:
162
+ """::
163
+
164
+ string = dump(value)
165
+ string = dump(value, sort_keys=True)
166
+ string = dump(value, indent=4, indent_sequences=False, quote='"')
167
+
168
+ Dump a value to a YAML string of one document, with the options:
169
+
170
+ * ``indent``, the number of spaces of a level of the indentation, ``2``
171
+ by default;
172
+ * ``indent_sequences``, whether a sequence below a key is indented,
173
+ ``True`` by default, or has its items at the column of the key;
174
+ * ``quote``, the quotes of every string value, ``"'"`` or ``'"'``, or
175
+ ``None`` by default, for quotes only where a string needs them, single
176
+ ones unless it needs the escapes of the double ones, which a string
177
+ gets with any of them;
178
+ * ``quote_keys``, the same for the keys, which are quoted only where they
179
+ need it by default;
180
+ * ``multiline``, whether a string of several lines is a literal block
181
+ scalar, ``True`` by default, or a quoted scalar; a string whose lines a
182
+ block scalar cannot keep, as one with a first line that starts with a
183
+ space, is quoted anyway;
184
+ * ``explicit_start``, whether the document starts with ``---``,
185
+ ``False`` by default; a scalar document always does;
186
+ * ``version``, the YAML whose reader gets the strings back: ``'1.1'`` by
187
+ default quotes also the strings that YAML 1.1 reads as other types,
188
+ as ``yes``, ``off``, ``1_000``, ``12:30`` and ``2001-12-14``, which
189
+ PyYAML reads so, and ``'1.2'`` only those of the core schema of YAML
190
+ 1.2, as ``true``, ``null`` and ``1.5``;
191
+ * ``anchors``, whether a :class:`dict` or a :class:`list` that is there
192
+ more than once has an anchor where it is first and an alias after it,
193
+ ``True`` by default, or is written again; an anchor has the name of the
194
+ key of the collection, or of the sequence of the item, with the
195
+ characters that are not ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` as
196
+ ``_``, ``id`` without a key, and a number from ``2`` for another
197
+ anchor of the same name.
198
+ * ``sort_keys``, whether the keys of a mapping are sorted, ``False`` by
199
+ default, for the order of the :class:`dict`. ::
200
+
201
+ # 'foo:\\n- "bar"\\n'
202
+ dump({'foo': ['bar']}, indent_sequences=False, quote='"')
203
+ """
204
+ return dump_all(
205
+ [value],
206
+ indent=indent,
207
+ indent_sequences=indent_sequences,
208
+ quote=quote,
209
+ quote_keys=quote_keys,
210
+ multiline=multiline,
211
+ explicit_start=explicit_start,
212
+ version=version,
213
+ anchors=anchors,
214
+ sort_keys=sort_keys,
215
+ )
216
+
217
+
218
+ def dump_all(
219
+ values: Iterable[Any],
220
+ *,
221
+ indent: int = 2,
222
+ indent_sequences: bool = True,
223
+ quote: Literal["'", '"'] | None = None,
224
+ quote_keys: Literal["'", '"'] | None = None,
225
+ multiline: bool = True,
226
+ explicit_start: bool = False,
227
+ version: Literal['1.1', '1.2'] = '1.1',
228
+ anchors: bool = True,
229
+ sort_keys: bool = False,
230
+ ) -> str:
231
+ """::
232
+
233
+ string = dump_all(values)
234
+
235
+ Dump values to a YAML string of documents, one for each value, with the
236
+ options of :func:`dump`, where the documents after the first one start
237
+ with ``---``. ::
238
+
239
+ # '--- first\\n--- second\\n'
240
+ dump_all(['first', 'second'])
241
+ """
242
+ dumper = _Dumper(indent, indent_sequences, quote, quote_keys, multiline, version, anchors, sort_keys)
243
+ lines: list[str] = []
244
+ for i, value in enumerate(values):
245
+ lines += dumper.document(value, explicit_start or i > 0)
246
+ return ''.join(f'{line}\n' for line in lines)
247
+
248
+
249
+ def load(string: str) -> Any:
250
+ """::
251
+
252
+ value = load(string)
253
+
254
+ Load a YAML string and return its document, or its last one when there
255
+ are several, as the ``Load`` of ``YAML`` of Perl does, or ``None`` for an
256
+ empty string. ::
257
+
258
+ # {'foo': 'bar'}
259
+ load('foo: bar')
260
+ """
261
+ documents = load_all(string)
262
+ return documents[-1] if documents else None
263
+
264
+
265
+ def load_all(string: str) -> list[Any]:
266
+ """::
267
+
268
+ values = load_all(string)
269
+
270
+ Load a YAML string and return a list of all its documents. ::
271
+
272
+ # ['first', 'second']
273
+ load_all('--- first\\n--- second\\n')
274
+ """
275
+ return _Loader(string).documents()
276
+
277
+
278
+ def _error(message: str) -> Pyjo.Exception.object:
279
+ return Pyjo.Exception.new(f'YAML {message}')
280
+
281
+
282
+ def _indent(line: str) -> int:
283
+ return len(line) - len(line.lstrip())
284
+
285
+
286
+ def _unquote_double(string: str) -> str:
287
+ def unescape(m: re.Match[str]) -> str:
288
+ if (code := m[1] or m[2] or m[3]) is not None:
289
+ return chr(int(code, 16))
290
+ return _ESCAPES.get(m[4], m[4])
291
+
292
+ return re.sub(
293
+ r'\\(?:x([0-9a-fA-F]{2})|u([0-9a-fA-F]{4})|U(0010[0-9a-fA-F]{4}|000[0-9a-fA-F]{5})|(["\\/ \t0LNP_abefnrtvz]))',
294
+ unescape,
295
+ string,
296
+ )
297
+
298
+
299
+ # Not in CPAN::Meta::YAML: the type of a plain scalar of the core schema of YAML 1.2, where CPAN::Meta::YAML keeps a
300
+ # string, as Perl takes "8080" for 8080, and Python does not
301
+ def _resolve(string: str) -> Any:
302
+ if re.fullmatch(r'~|null|Null|NULL', string):
303
+ return None
304
+ if re.fullmatch(r'true|True|TRUE', string):
305
+ return True
306
+ if re.fullmatch(r'false|False|FALSE', string):
307
+ return False
308
+ if re.fullmatch(r'[-+]?[0-9]+', string):
309
+ return int(string)
310
+ if re.fullmatch(r'0o[0-7]+', string):
311
+ return int(string[2:], 8)
312
+ if re.fullmatch(r'0x[0-9a-fA-F]+', string):
313
+ return int(string[2:], 16)
314
+ if re.fullmatch(r'[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?', string):
315
+ return float(string)
316
+ if m := re.fullmatch(r'([-+]?)\.(?:inf|Inf|INF)', string):
317
+ return float(f'{m[1]}inf')
318
+ if re.fullmatch(r'\.(?:nan|NaN|NAN)', string):
319
+ return float('nan')
320
+ return string
321
+
322
+
323
+ # The standard tags of the scalars, of the core schema of YAML 1.2 and, for the types of Python they have, of YAML 1.1
324
+ def _tag_binary(value: str) -> bytes:
325
+ try:
326
+ return base64.b64decode(re.sub(r'\s+', '', value), validate=True)
327
+ except binascii.Error as e:
328
+ raise ValueError(str(e)) from None
329
+
330
+
331
+ def _tag_bool(value: str) -> bool:
332
+ if (resolved := _resolve(value)) is True or resolved is False:
333
+ return resolved
334
+ raise ValueError(value)
335
+
336
+
337
+ def _tag_float(value: str) -> float:
338
+ if isinstance(resolved := _resolve(value), (int, float)) and not isinstance(resolved, bool):
339
+ return float(resolved)
340
+ raise ValueError(value)
341
+
342
+
343
+ def _tag_int(value: str) -> int:
344
+ if isinstance(resolved := _resolve(value), int) and not isinstance(resolved, bool):
345
+ return resolved
346
+ raise ValueError(value)
347
+
348
+
349
+ def _tag_null(value: str) -> None:
350
+ if value not in ('', '~') and _resolve(value) is not None:
351
+ raise ValueError(value)
352
+
353
+
354
+ def _tag_timestamp(value: str) -> datetime.date | datetime.datetime:
355
+ if re.fullmatch(r'\d{4}-\d\d-\d\d', value):
356
+ return datetime.date.fromisoformat(value)
357
+ return datetime.datetime.fromisoformat(re.sub(r'(?<=\d)[tT](?=\d)', 'T', value))
358
+
359
+
360
+ _TAGS: dict[str, Callable[[str], Any]] = {
361
+ 'binary': _tag_binary,
362
+ 'bool': _tag_bool,
363
+ 'float': _tag_float,
364
+ 'int': _tag_int,
365
+ 'null': _tag_null,
366
+ 'str': str,
367
+ 'timestamp': _tag_timestamp,
368
+ }
369
+
370
+
371
+ def _unquote_single(string: str) -> str:
372
+ return string.replace("''", "'")
373
+
374
+
375
+ class _Loader:
376
+ def __init__(self, string: str) -> None:
377
+ self.anchors: dict[str, Any] = {}
378
+ # A deque, whose popleft takes the first line as shift of Perl does, where pop(0) of a list moves all the others
379
+ self.lines = collections.deque(re.split(r'\r\r?\n|\r|\n', string.removeprefix('\ufeff')))
380
+
381
+ # The line break at the end ends the last line, it does not start an empty one
382
+ if self.lines[-1] == '':
383
+ self.lines.pop()
384
+
385
+ def alias(self, name: str) -> Any:
386
+ if name not in self.anchors:
387
+ raise _error(f"found an unknown alias '*{name}'")
388
+ return self.anchors[name]
389
+
390
+ def block(self, string: str, indent: int | None) -> str:
391
+ if not (m := re.search(r'^([|>])([+-]?)\s*(?:#.*)?\Z', string)):
392
+ raise _error(f"does not support a feature in line '{string}'")
393
+
394
+ # The lines more indented than the key, with the empty ones among them
395
+ lines = self.lines
396
+ block: list[str] = []
397
+ depth: int | None = None
398
+ while lines:
399
+ if not lines[0].strip():
400
+ block.append('')
401
+ lines.popleft()
402
+ continue
403
+ if depth is None:
404
+ if indent is not None and _indent(lines[0]) <= indent:
405
+ break
406
+ depth = _indent(lines[0])
407
+ if _indent(lines[0]) < depth:
408
+ break
409
+ block.append(lines.popleft()[depth:])
410
+
411
+ # The empty lines at the end, which the chomping indicator keeps, strips or clips to one line break
412
+ trailing = len(block) - len(content := _rstrip_empty(block))
413
+ text = '\n'.join(content) if m[1] == '|' else _fold(content)
414
+ if m[2] == '-' or not content:
415
+ return text
416
+ return text + '\n' * (1 + trailing if m[2] == '+' else 1)
417
+
418
+ def documents(self) -> list[Any]:
419
+ documents: list[Any] = []
420
+ explicit = False
421
+ lines = self.lines
422
+
423
+ # The directive of the version
424
+ if self.skip() and re.search(r'^%YAML[: ][\d.]+', lines[0]):
425
+ lines.popleft()
426
+
427
+ while self.skip():
428
+ # The anchors of a document are its own
429
+ self.anchors = {}
430
+
431
+ # The start of a document, with a scalar after it
432
+ if m := re.search(r'^---\s*(?:(.+?)\s*)?\Z', lines[0]):
433
+ lines.popleft()
434
+ if m[1] is not None and not re.search(r'^(?:#.+|%YAML[: ][\d.]+)\Z', m[1]):
435
+ documents.append(self.scalar(m[1], None))
436
+ continue
437
+ explicit = True
438
+
439
+ # An empty document
440
+ if not self.skip() or re.search(r'^(?:---|\.\.\.)', lines[0]):
441
+ documents.append(None)
442
+ while lines and not lines[0].startswith('---'):
443
+ lines.popleft()
444
+ explicit = False
445
+
446
+ # Only the first document can do without its start
447
+ elif not explicit and documents:
448
+ raise _error(f"failed to classify the line '{lines[0]}'")
449
+
450
+ # A sequence
451
+ elif re.search(r'^\s*-(?:\s|$|-+$)', lines[0]):
452
+ documents.append(self.sequence(0))
453
+
454
+ # A flow collection
455
+ elif re.search(r'^\s*[\[{]', lines[0]):
456
+ documents.append(self.scalar(lines.popleft().strip(), None))
457
+
458
+ # A mapping
459
+ else:
460
+ documents.append(self.mapping(_indent(lines[0])))
461
+
462
+ return documents
463
+
464
+ def end_of_document(self) -> bool:
465
+ lines = self.lines
466
+ if not self.skip():
467
+ return True
468
+ if not re.search(r'^(?:---|\.\.\.)', lines[0]):
469
+ return False
470
+ while lines and not lines[0].startswith('---'):
471
+ lines.popleft()
472
+ return True
473
+
474
+ def flow(self, string: str) -> Any:
475
+ # The lines of the collection, until its brackets are closed
476
+ text = string
477
+ while (end := _flow_end(text)) is None:
478
+ if not self.lines:
479
+ raise _error(f"failed to find the end of the flow collection '{string}'")
480
+ if not _RE_BLANK.search(line := self.lines.popleft()):
481
+ text += ' ' + line.strip()
482
+ if (rest := text[end:].strip()) and not rest.startswith('#'):
483
+ raise _error(f"found illegal characters after the flow collection '{text}'")
484
+ return _Flow(text[:end], self).value()
485
+
486
+ def mapping(self, indent: int, outer: int | None = None) -> dict[Any, Any]:
487
+ mapping: dict[Any, Any] = {}
488
+ merges: list[Any] = []
489
+ lines = self.lines
490
+ while not self.end_of_document():
491
+ if (depth := _indent(lines[0])) < indent:
492
+ break
493
+ if depth > indent:
494
+ raise _error(f"found bad indenting in line '{lines[0]}'")
495
+
496
+ # Not in CPAN::Meta::YAML: the anchor of the key
497
+ line = lines[0]
498
+ anchor = None
499
+ if m := _RE_KEY_ANCHOR.search(line):
500
+ anchor = m[2]
501
+ line = m[1] + line[m.end() :]
502
+
503
+ # The key, quoted or not, or an alias, and the rest of the line
504
+ if m := _RE_KEY_ALIAS.search(line):
505
+ quote = '*'
506
+ key = self.alias(m[1])
507
+ elif re.search(r'^\s*[!&*]', line):
508
+ raise _error(f"does not support a feature in line '{line}'")
509
+ elif m := _key(line):
510
+ quote = line.lstrip()[:1]
511
+ if quote == "'":
512
+ key = _unquote_single(m[1])
513
+ elif quote == '"':
514
+ key = _unquote_double(m[1])
515
+ else:
516
+ key = _resolve(m[1].rstrip())
517
+ elif re.search(r'^\s*\?', line):
518
+ raise _error(f"does not support a feature in line '{line}'")
519
+ else:
520
+ raise _error(f"failed to classify line '{line}'")
521
+ rest = line[m.end() :]
522
+ lines.popleft()
523
+ try:
524
+ hash(key)
525
+ except TypeError:
526
+ raise _error(f"found a key that is not hashable in line '{line}'") from None
527
+ if anchor is not None:
528
+ self.anchors[anchor] = key
529
+
530
+ # A value on the line, or below it, or none
531
+ value = self.scalar(rest, indent) if rest else self.value(indent)
532
+ if key == '<<' and quote not in ("'", '"', '*'):
533
+ merges.append(value)
534
+ else:
535
+ mapping[key] = value
536
+
537
+ return _merge(mapping, merges)
538
+
539
+ def plain(self, string: str, indent: int | None) -> str:
540
+ if re.search(r'^(?:-(?:\s|$)|[@%`])', string) or re.search(r':(?:\s|$)', string):
541
+ raise _error(f"found illegal characters in plain scalar: '{string}'")
542
+ plain = re.sub(r'\s+#.*\Z', '', string)
543
+ if plain != string or indent is None:
544
+ return plain
545
+
546
+ # The lines more indented than the key, folded, which a comment ends
547
+ lines = self.lines
548
+ empty = 0
549
+ while lines and (indent < _indent(lines[0]) or not lines[0].strip()):
550
+ line = lines[0].strip()
551
+ if line.startswith('#'):
552
+ break
553
+ lines.popleft()
554
+ if not line:
555
+ empty += 1
556
+ continue
557
+ if re.search(r':(?:\s|$)', line):
558
+ raise _error(f"found illegal characters in plain scalar: '{line}'")
559
+ continued = re.sub(r'\s+#.*\Z', '', line)
560
+ plain += ('\n' * empty if empty else ' ') + continued
561
+ empty = 0
562
+ if continued != line:
563
+ break
564
+ return plain
565
+
566
+ def quoted(self, string: str) -> str:
567
+ double = string.startswith('"')
568
+ pattern = _RE_DOUBLE if double else _RE_SINGLE
569
+
570
+ # The lines of the scalar, until its quote is closed, folded
571
+ text = string
572
+ empty = 0
573
+ while not (m := pattern.search(text)):
574
+ if not self.lines:
575
+ raise _error(f"failed to find the end of the quoted scalar '{string}'")
576
+ line = self.lines.popleft().strip()
577
+ if not line:
578
+ empty += 1
579
+ continue
580
+ text = text.rstrip(' \t')
581
+ if double and re.search(r'(?<!\\)(?:\\\\)*\\\Z', text) and not empty:
582
+ text = text[:-1] + line
583
+ else:
584
+ text += ('\n' * empty if empty else ' ') + line
585
+ empty = 0
586
+ return _unquote_double(m[1]) if double else _unquote_single(m[1])
587
+
588
+ def scalar(self, string: str, indent: int | None, item: bool = False) -> Any:
589
+ string = string.rstrip()
590
+
591
+ # Not in CPAN::Meta::YAML: an alias, the node of its anchor
592
+ if string.startswith('*'):
593
+ if not (m := re.search(rf'^\*({_ANCHOR})(?:\s+#.*)?\Z', string)):
594
+ raise _error(f"found illegal characters after the alias '{string}'")
595
+ return self.alias(m[1])
596
+
597
+ # Not in CPAN::Meta::YAML: the properties of a node, an anchor and a standard tag, in either order
598
+ anchor = tag = None
599
+ while m := re.search(rf'^(?:&({_ANCHOR})|!!(\w+))(?:\s+|\Z)', string):
600
+ if m[1] is not None and anchor is None:
601
+ anchor = m[1]
602
+ elif m[2] is not None and tag is None:
603
+ tag = m[2]
604
+ else:
605
+ raise _error(f"found a property twice in line '{string}'")
606
+ string = string[m.end() :]
607
+
608
+ # A node below the anchor
609
+ if anchor is not None and tag is None and (not string or string.startswith('#')):
610
+ value = self.value(-1 if indent is None else indent, item)
611
+
612
+ # The tag, which gives a scalar its type
613
+ elif tag is not None:
614
+ if (convert := _TAGS.get(tag)) is None or string.startswith(('[', '{')):
615
+ raise _error(f"does not support a feature in line '!!{tag} {string}'")
616
+ text = self.text(string, indent, False)
617
+ try:
618
+ value = convert(text)
619
+ except ValueError:
620
+ raise _error(f"found an invalid value of the tag !!{tag}: '{text}'") from None
621
+
622
+ else:
623
+ value = self.text(string, indent, True)
624
+
625
+ # Unlike PyYAML: an anchor can be defined again, as in YAML 1.2, and an alias is the node of the last one
626
+ if anchor is not None:
627
+ self.anchors[anchor] = value
628
+ return value
629
+
630
+ def sequence(self, indent: int, outer: int | None = None) -> list[Any]:
631
+ sequence: list[Any] = []
632
+ lines = self.lines
633
+ while not self.end_of_document():
634
+ if (depth := _indent(lines[0])) < indent:
635
+ break
636
+ if depth > indent:
637
+ raise _error(f"found bad indenting in line '{lines[0]}'")
638
+
639
+ # Not in CPAN::Meta::YAML: a sequence that starts on the line of the item
640
+ if m := re.search(r'^(\s*-\s+)-(?:\s|\Z)', lines[0]):
641
+ lines[0] = lines[0].replace('-', ' ', 1)
642
+ sequence.append(self.sequence(len(m[1])))
643
+
644
+ # A mapping that starts on the line of the item
645
+ elif (m := re.search(r'^(\s*-\s+)(?![\[{!|>])', lines[0])) and _is_key(lines[0][m.end() :]):
646
+ lines[0] = lines[0].replace('-', ' ', 1)
647
+ sequence.append(self.mapping(len(m[1]), indent))
648
+
649
+ # An item whose value is below it, or none
650
+ elif re.search(r'^\s*-\s*(?:#.*)?\Z', lines[0]):
651
+ lines.popleft()
652
+ sequence.append(self.value(indent, True))
653
+
654
+ # An item with a value
655
+ elif m := re.search(r'^\s*-\s+(.+?)\s*\Z', lines[0]):
656
+ lines.popleft()
657
+ sequence.append(self.scalar(m[1], indent, True))
658
+
659
+ # The key of a mapping after a sequence at its indentation
660
+ elif outer is not None and indent == outer:
661
+ break
662
+
663
+ else:
664
+ raise _error(f"failed to classify line '{lines[0]}'")
665
+
666
+ return sequence
667
+
668
+ def skip(self) -> bool:
669
+ # The empty lines and the lines of comments, before the next line of the structure
670
+ lines = self.lines
671
+ while lines and _RE_BLANK.search(lines[0]):
672
+ lines.popleft()
673
+ return bool(lines)
674
+
675
+ def text(self, string: str, indent: int | None, resolve: bool) -> Any:
676
+ if string.startswith(('[', '{')):
677
+ return self.flow(string)
678
+ if string.startswith(("'", '"')):
679
+ return self.quoted(string)
680
+ if re.search(r'^[!&*]', string):
681
+ raise _error(f"does not support a feature in line '{string}'")
682
+ if string.startswith(('|', '>')):
683
+ return self.block(string, indent)
684
+ plain = self.plain(string, indent)
685
+ return _resolve(plain) if resolve else plain
686
+
687
+ def value(self, indent: int, item: bool = False) -> Any:
688
+ # A sequence, a mapping or a scalar below its key or its item, or nothing
689
+ lines = self.lines
690
+ if not self.skip() or re.search(r'^(?:---|\.\.\.)', lines[0]):
691
+ return None
692
+ depth = _indent(lines[0])
693
+ if m := re.search(r'^(\s*)-(?:\s|\Z)', lines[0]):
694
+ if item and len(m[1]) == indent:
695
+ return None
696
+ if len(m[1]) < indent or item and len(m[1]) <= indent:
697
+ return None
698
+ return self.sequence(len(m[1]), indent)
699
+ if depth <= indent:
700
+ return None
701
+ if _is_key(lines[0]):
702
+ return self.mapping(depth, indent)
703
+ if re.search(r'^\s*\?', lines[0]):
704
+ raise _error(f"does not support a feature in line '{lines[0]}'")
705
+ return self.scalar(lines.popleft().strip(), indent)
706
+
707
+
708
+ class _Flow:
709
+ # A flow collection, in one string, with the anchors of its document
710
+ def __init__(self, text: str, loader: _Loader) -> None:
711
+ self.loader = loader
712
+ self.text = text
713
+ self.pos = 0
714
+
715
+ def entry(self, end: str) -> tuple[Any, bool]:
716
+ # A node, or a pair of a key and a value, as a mapping is an entry of a sequence
717
+ if (c := self.peek()) in ('"', "'"):
718
+ key: Any = self.quoted()
719
+ elif c in ('[', '{', '*', '&'):
720
+ return self.node(), False
721
+ else:
722
+ key = self.plain(end)
723
+ if self.peek() == ':':
724
+ self.pos += 1
725
+ value = None if self.peek() in (',', end) else self.node()
726
+ return {key if c in ('"', "'") else _resolve(key): value}, True
727
+ return (key if c in ('"', "'") else _resolve(key)), False
728
+
729
+ def error(self) -> Pyjo.Exception.object:
730
+ return _error(f"failed to parse the flow collection '{self.text}' at offset {self.pos}")
731
+
732
+ def mapping(self) -> dict[Any, Any]:
733
+ mapping: dict[Any, Any] = {}
734
+ merges: list[Any] = []
735
+ self.pos += 1
736
+ while (c := self.peek()) != '}':
737
+ key = self.quoted() if c in ('"', "'") else _resolve(self.plain('}'))
738
+ value = None
739
+ if self.peek() == ':':
740
+ self.pos += 1
741
+ value = None if self.peek() in (',', '}') else self.node()
742
+ if key == '<<' and c not in ('"', "'"):
743
+ merges.append(value)
744
+ else:
745
+ mapping[key] = value
746
+ if self.peek() == ',':
747
+ self.pos += 1
748
+ elif self.peek() != '}':
749
+ raise self.error()
750
+ self.pos += 1
751
+ return _merge(mapping, merges)
752
+
753
+ def name(self) -> str:
754
+ # The name of an anchor or an alias, after its indicator
755
+ if not (m := re.compile(_ANCHOR).match(self.text, self.pos + 1)):
756
+ raise self.error()
757
+ self.pos = m.end()
758
+ return m[0]
759
+
760
+ def node(self) -> Any:
761
+ c = self.peek()
762
+ if c == '*':
763
+ return self.loader.alias(self.name())
764
+ if c == '&':
765
+ anchor = self.name()
766
+ self.loader.anchors[anchor] = value = self.node()
767
+ return value
768
+ if c == '[':
769
+ return self.sequence()
770
+ if c == '{':
771
+ return self.mapping()
772
+ if c in ('"', "'"):
773
+ return self.quoted()
774
+ return _resolve(self.plain(None))
775
+
776
+ def peek(self) -> str:
777
+ text = self.text
778
+ while self.pos < len(text) and text[self.pos] in ' \t':
779
+ self.pos += 1
780
+ if self.pos >= len(text):
781
+ raise self.error()
782
+ return text[self.pos]
783
+
784
+ def plain(self, end: str | None) -> str:
785
+ text = self.text
786
+ start = self.pos
787
+ while self.pos < len(text):
788
+ c = text[self.pos]
789
+ if c in ',[]{}' or c == ':' and (self.pos + 1 == len(text) or text[self.pos + 1] in ' ,[]{}'):
790
+ break
791
+ self.pos += 1
792
+ plain = text[start : self.pos].strip()
793
+ if not plain or re.search(r'^[!&*@`|>%#]|^-(?:\s|$)', plain):
794
+ raise self.error()
795
+ return plain
796
+
797
+ def quoted(self) -> str:
798
+ text = self.text
799
+ quote = text[self.pos]
800
+ pattern = _DOUBLE_QUOTED if quote == '"' else _SINGLE_QUOTED
801
+ if not (m := re.compile(pattern).match(text, self.pos)):
802
+ raise self.error()
803
+ self.pos = m.end()
804
+ return _unquote_double(m[1]) if quote == '"' else _unquote_single(m[1])
805
+
806
+ def sequence(self) -> list[Any]:
807
+ sequence: list[Any] = []
808
+ self.pos += 1
809
+ while self.peek() != ']':
810
+ sequence.append(self.entry(']')[0])
811
+ if self.peek() == ',':
812
+ self.pos += 1
813
+ elif self.peek() != ']':
814
+ raise self.error()
815
+ self.pos += 1
816
+ return sequence
817
+
818
+ def value(self) -> Any:
819
+ value = self.node()
820
+ if self.text[self.pos :].strip():
821
+ raise self.error()
822
+ return value
823
+
824
+
825
+ # Not in CPAN::Meta::YAML: the merge keys of a mapping, << of YAML 1.1, which PyYAML reads too: the keys of the
826
+ # mappings that they name, of the earlier ones first, with the keys of the mapping itself over them
827
+ def _merge(mapping: dict[Any, Any], merges: list[Any]) -> dict[Any, Any]:
828
+ if not merges:
829
+ return mapping
830
+ merged: dict[Any, Any] = {}
831
+ for merge in merges:
832
+ sources: list[Any] = cast(list[Any], merge) if isinstance(merge, list) else [merge]
833
+ for source in reversed(sources):
834
+ if not isinstance(source, dict):
835
+ raise _error('found a merge key with a value that is not a mapping or a sequence of mappings')
836
+ merged.update(cast(dict[Any, Any], source))
837
+ merged.update(mapping)
838
+ return merged
839
+
840
+
841
+ # The key at the start of a line, quoted, which only the pattern of its quotes reads, or not
842
+ def _key(line: str) -> re.Match[str] | None:
843
+ if (stripped := line.lstrip()).startswith("'"):
844
+ return _RE_KEY_SINGLE.search(line)
845
+ if stripped.startswith('"'):
846
+ return _RE_KEY_DOUBLE.search(line)
847
+ return _RE_KEY_UNQUOTED.search(line)
848
+
849
+
850
+ def _is_key(line: str) -> bool:
851
+ line = _RE_KEY_ANCHOR.sub(r'\1', line, count=1)
852
+ return _key(line) is not None or _RE_KEY_ALIAS.search(line) is not None
853
+
854
+
855
+ # The end of the flow collection at the start of a string, after its closing bracket, or None while it is open
856
+ def _flow_end(text: str) -> int | None:
857
+ depth = 0
858
+ quote = ''
859
+ i = 0
860
+ while i < len(text):
861
+ c = text[i]
862
+ if quote == '"':
863
+ if c == '\\':
864
+ i += 1
865
+ elif c == '"':
866
+ quote = ''
867
+ elif quote == "'":
868
+ if c == "'" and text[i + 1 : i + 2] == "'":
869
+ i += 1
870
+ elif c == "'":
871
+ quote = ''
872
+ elif c in ('"', "'"):
873
+ quote = c
874
+ elif c in '[{':
875
+ depth += 1
876
+ elif c in ']}':
877
+ depth -= 1
878
+ if depth == 0:
879
+ return i + 1
880
+ i += 1
881
+ return None
882
+
883
+
884
+ # The lines of a folded block scalar: a line break between two lines is a space, an empty line a line break, and the
885
+ # lines more indented keep their line breaks
886
+ def _fold(lines: list[str]) -> str:
887
+ folded = ''
888
+ previous: str | None = None
889
+ empty = 0
890
+ for line in lines:
891
+ if not line:
892
+ empty += 1
893
+ continue
894
+ if previous is None:
895
+ folded = '\n' * empty + line
896
+ elif not empty and not previous[:1].isspace() and not line[:1].isspace():
897
+ folded += ' ' + line
898
+ elif previous[:1].isspace() or line[:1].isspace():
899
+ folded += '\n' * (1 + empty) + line
900
+ else:
901
+ folded += '\n' * empty + line
902
+ previous = line
903
+ empty = 0
904
+ return folded
905
+
906
+
907
+ def _rstrip_empty(lines: list[str]) -> list[str]:
908
+ end = len(lines)
909
+ while end and not lines[end - 1]:
910
+ end -= 1
911
+ return lines[:end]
912
+
913
+
914
+ # The characters that a double quoted scalar escapes, which a single quoted and a plain one cannot have, and the ones
915
+ # that a literal block scalar cannot have either, which are the same without the line break and the tab
916
+ _RE_ESCAPE = re.compile(r'["\\\x00-\x1f\x7f-\x9f\u2028\u2029\ufeff\ud800-\udfff\ufffe\uffff]')
917
+ _RE_UNQUOTABLE = re.compile(r'[\x00-\x1f\x7f-\x9f\u2028\u2029\ufeff\ud800-\udfff\ufffe\uffff]')
918
+ _RE_UNLITERAL = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\x9f\u2028\u2029\ufeff\ud800-\udfff\ufffe\uffff]')
919
+
920
+ _ESCAPED = {
921
+ '\x00': '\\0',
922
+ '\x07': '\\a',
923
+ '\x08': '\\b',
924
+ '\x09': '\\t',
925
+ '\x0a': '\\n',
926
+ '\x0b': '\\v',
927
+ '\x0c': '\\f',
928
+ '\x0d': '\\r',
929
+ '\x1b': '\\e',
930
+ '"': '\\"',
931
+ '\\': '\\\\',
932
+ '\x85': '\\N',
933
+ '\u2028': '\\L',
934
+ '\u2029': '\\P',
935
+ }
936
+
937
+ # The plain scalars that YAML 1.1 reads as other types than a string, as PyYAML does: booleans, numbers with
938
+ # underscores, of base 2 and 60, and timestamps
939
+ _RE_YAML11 = re.compile(
940
+ r'[yYnN]|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF|='
941
+ r'|[-+]?(?:0b[01_]+|0x[0-9a-fA-F_]+|[0-9][0-9_]*(?::[0-5]?[0-9])*(?:\.[0-9_]*)?|\.[0-9_]+)(?:[eE][-+]?[0-9]+)?'
942
+ r'|[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:(?:[Tt]|[ \t]+)[0-9]{1,2}:[0-9]{2}:[0-9]{2}.*)?'
943
+ )
944
+
945
+
946
+ class _Dumper:
947
+ def __init__(
948
+ self,
949
+ indent: int,
950
+ indent_sequences: bool,
951
+ quote: str | None,
952
+ quote_keys: str | None,
953
+ multiline: bool,
954
+ version: str,
955
+ anchors: bool,
956
+ sort_keys: bool = False,
957
+ ) -> None:
958
+ if indent < 1:
959
+ raise ValueError(f'indent must be 1 or more, not {indent}')
960
+ for name, value in (('quote', quote), ('quote_keys', quote_keys)):
961
+ if value not in (None, "'", '"'):
962
+ raise ValueError(f"""{name} must be "'", '"' or None, not {value!r}""")
963
+ if version not in ('1.1', '1.2'):
964
+ raise ValueError(f"version must be '1.1' or '1.2', not {version!r}")
965
+ self.anchors = anchors
966
+ self.sort_keys = sort_keys
967
+ self.counts: dict[int, int] = {}
968
+ self.indent = indent
969
+ self.indent_sequences = indent_sequences
970
+ self.multiline = multiline
971
+ self.names: dict[int, str] = {}
972
+ self.parents: set[int] = set()
973
+ self.quote = quote
974
+ self.quote_keys = quote_keys
975
+ self.yaml11 = version == '1.1'
976
+
977
+ def collection(self, value: Any, column: int, name: str) -> list[str] | None:
978
+ # The lines of a collection that is not empty, which is not a scalar, or None
979
+ if not value or not isinstance(value, (dict, list, tuple)):
980
+ return None
981
+ collection = cast(dict[Any, Any] | list[Any] | tuple[Any, ...], value)
982
+ if id(collection) in self.parents:
983
+ raise _error('found a collection inside itself')
984
+ self.parents.add(id(collection))
985
+ if isinstance(collection, dict):
986
+ lines = self.mapping(collection, column)
987
+ else:
988
+ lines = self.sequence(collection, column, name)
989
+ self.parents.remove(id(collection))
990
+ return lines
991
+
992
+ def count(self, value: Any) -> None:
993
+ # The times that each dict and list is there, and the ones inside it, the first time
994
+ if isinstance(value, tuple):
995
+ for item in cast(tuple[Any, ...], value):
996
+ self.count(item)
997
+ elif isinstance(value, (dict, list)):
998
+ collection = cast(dict[Any, Any] | list[Any], value)
999
+ self.counts[id(collection)] = count = self.counts.get(id(collection), 0) + 1
1000
+ if count == 1:
1001
+ for item in collection.values() if isinstance(collection, dict) else collection:
1002
+ self.count(item)
1003
+
1004
+ def document(self, value: Any, start: bool) -> list[str]:
1005
+ # The anchors of a document are its own
1006
+ self.counts = {}
1007
+ self.names = {}
1008
+ if self.anchors:
1009
+ self.count(value)
1010
+
1011
+ if (lines := self.collection(value := _data(value), 0, 'id')) is not None:
1012
+ return ['---', *lines] if start else lines
1013
+
1014
+ # A scalar after the start of its document, where the loader reads it
1015
+ text, block = self.scalar(value, 0)
1016
+ return [f'--- {text}', *block]
1017
+
1018
+ def key(self, key: Any) -> str:
1019
+ if isinstance(key, str):
1020
+ return self.string(str.__str__(key), 0, self.quote_keys, False, True)[0]
1021
+ if (text := _plain_value(key)) is None:
1022
+ raise _error(f"does not support a key of the type '{type(key).__name__}'")
1023
+ return text
1024
+
1025
+ def literal(self, string: str, column: int) -> tuple[str, list[str]] | None:
1026
+ # A literal block scalar, unless the loader would not read its lines back: its first line that is not empty
1027
+ # starts with a space, which needs the indicator of the indentation, or a line has only spaces
1028
+ body = string.rstrip('\n')
1029
+ lines = body.split('\n')
1030
+ first = next((line for line in lines if line), '')
1031
+ if not first or first[:1].isspace() or _RE_UNLITERAL.search(body):
1032
+ return None
1033
+ if any(line and not line.strip() for line in lines):
1034
+ return None
1035
+
1036
+ # The final line breaks, which the chomping indicator strips, clips to one or keeps
1037
+ breaks = len(string) - len(body)
1038
+ chomping = '-' if breaks == 0 else '' if breaks == 1 else '+'
1039
+ pad = ' ' * (column + self.indent)
1040
+ return f'|{chomping}', [pad + line if line else '' for line in lines + [''] * (breaks - 1)]
1041
+
1042
+ def mapping(self, mapping: dict[Any, Any], column: int) -> list[str]:
1043
+ lines: list[str] = []
1044
+ items = list(mapping.items())
1045
+ if self.sort_keys:
1046
+ items.sort(key=lambda item: str(item[0]))
1047
+ for key, value in items:
1048
+ prefix = ' ' * column + (text := self.key(key)) + ':'
1049
+ name = key if isinstance(key, str) else text
1050
+ value = _data(value)
1051
+ nested = column + self.indent if isinstance(value, dict) or self.indent_sequences else column
1052
+ if (reference := self.reference(value, name)).startswith('*'):
1053
+ lines.append(f'{prefix} {reference}')
1054
+ elif (below := self.collection(value, nested, name)) is not None:
1055
+ lines += [f'{prefix} {reference}' if reference else prefix, *below]
1056
+ else:
1057
+ text, block = self.scalar(value, column)
1058
+ lines += [f'{prefix} {reference} {text}' if reference else f'{prefix} {text}', *block]
1059
+ return lines
1060
+
1061
+ def plain(self, string: str, key: bool) -> bool:
1062
+ # A string that the loader reads back as the same string without quotes
1063
+ if not string or string != string.strip() or _RE_UNQUOTABLE.search(string):
1064
+ return False
1065
+ if string[0] in '?:,[]{}#&*!|>\'"%@`' or re.search(r'^(?:-(?:\s|\Z|--)|\.\.\.)|\s#|:(?:\s|\Z)', string):
1066
+ return False
1067
+ if key and (string[0] == '-' or string == '<<'):
1068
+ return False
1069
+ if not isinstance(_resolve(string), str):
1070
+ return False
1071
+ return not (self.yaml11 and _RE_YAML11.fullmatch(string))
1072
+
1073
+ def reference(self, value: Any, name: str) -> str:
1074
+ # The anchor of a dict or a list that is there more than once, where it is first, with the name of its key and
1075
+ # a number for another one of the name, and its alias after it
1076
+ if not isinstance(value, (dict, list)) or self.counts.get(key := id(cast(Any, value)), 0) < 2:
1077
+ return ''
1078
+ if (anchor := self.names.get(key)) is None:
1079
+ anchor = name = re.sub(r'[^0-9A-Za-z_-]+', '_', name).strip('_') or 'id'
1080
+ number = 1
1081
+ while anchor in self.names.values():
1082
+ number += 1
1083
+ anchor = f'{name}{number}'
1084
+ self.names[key] = anchor
1085
+ return f'&{anchor}'
1086
+ if key in self.parents:
1087
+ raise _error('found a collection inside itself')
1088
+ return f'*{anchor}'
1089
+
1090
+ def scalar(self, value: Any, column: int) -> tuple[str, list[str]]:
1091
+ if isinstance(value, str):
1092
+ return self.string(str.__str__(value), column, self.quote, self.multiline)
1093
+ if (text := _plain_value(value)) is not None:
1094
+ return text, []
1095
+ if isinstance(value, dict):
1096
+ return '{}', []
1097
+ if isinstance(value, (list, tuple)):
1098
+ return '[]', []
1099
+ if isinstance(value, (bytes, bytearray)):
1100
+ return '!!binary ' + base64.b64encode(value).decode(), []
1101
+ if isinstance(value, datetime.date):
1102
+ return '!!timestamp ' + value.isoformat(), []
1103
+ raise _error(f"does not support a value of the type '{type(value).__name__}'")
1104
+
1105
+ def sequence(self, sequence: list[Any] | tuple[Any, ...], column: int, name: str) -> list[str]:
1106
+ lines: list[str] = []
1107
+ pad = ' ' * column
1108
+ for item in sequence:
1109
+ # A collection that starts on the line of the item, or below its anchor
1110
+ item = _data(item)
1111
+ if (reference := self.reference(item, name)).startswith('*'):
1112
+ lines.append(f'{pad}- {reference}')
1113
+ elif (nested := self.collection(item, column + 2, name)) is not None:
1114
+ if reference:
1115
+ lines += [f'{pad}- {reference}', *nested]
1116
+ else:
1117
+ lines += [f'{pad}- {nested[0][column + 2 :]}', *nested[1:]]
1118
+ else:
1119
+ text, block = self.scalar(item, column)
1120
+ lines += [f'{pad}- {reference} {text}' if reference else f'{pad}- {text}', *block]
1121
+ return lines
1122
+
1123
+ def string(
1124
+ self, string: str, column: int, quote: str | None, multiline: bool, key: bool = False
1125
+ ) -> tuple[str, list[str]]:
1126
+ if multiline and '\n' in string and (block := self.literal(string, column)) is not None:
1127
+ return block
1128
+ if quote is None and self.plain(string, key):
1129
+ return string, []
1130
+ if quote != '"' and not _RE_UNQUOTABLE.search(string):
1131
+ return "'" + string.replace("'", "''") + "'", []
1132
+ return '"' + _RE_ESCAPE.sub(_escape, string) + '"', []
1133
+
1134
+
1135
+ # An object that Pyjo.JSON encodes, as the value of its __json__ method, or as a string where its class has its own
1136
+ # __str__
1137
+ def _data(value: Any) -> Any:
1138
+ if value is None or isinstance(value, (bool, int, float, str, bytes, bytearray, dict, list, tuple, datetime.date)):
1139
+ return cast(Any, value)
1140
+ if (method := getattr(value, '__json__', None)) is not None:
1141
+ return method()
1142
+ if type(value).__str__ is not builtins.object.__str__:
1143
+ return str(value)
1144
+ return value
1145
+
1146
+
1147
+ def _escape(m: re.Match[str]) -> str:
1148
+ if (escaped := _ESCAPED.get(c := m[0])) is not None:
1149
+ return escaped
1150
+ return f'\\x{ord(c):02x}' if ord(c) < 0x100 else f'\\u{ord(c):04x}'
1151
+
1152
+
1153
+ # The text of a null, a boolean or a number, which the loader reads back as the same value
1154
+ def _plain_value(value: Any) -> str | None:
1155
+ if value is None:
1156
+ return 'null'
1157
+ if isinstance(value, bool):
1158
+ return 'true' if value else 'false'
1159
+ if isinstance(value, int):
1160
+ return int.__repr__(value)
1161
+ if isinstance(value, float):
1162
+ if math.isnan(value):
1163
+ return '.nan'
1164
+ if math.isinf(value):
1165
+ return '.inf' if value > 0 else '-.inf'
1166
+ return float.__repr__(value)
1167
+ return None