omlish 0.0.0.dev4__py3-none-any.whl → 0.0.0.dev6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (143) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +1 -1
  3. omlish/asyncs/__init__.py +10 -4
  4. omlish/asyncs/anyio.py +142 -12
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/flavors.py +27 -1
  9. omlish/asyncs/trio_asyncio.py +28 -18
  10. omlish/c3.py +1 -1
  11. omlish/cached.py +1 -2
  12. omlish/collections/__init__.py +5 -1
  13. omlish/collections/cache/impl.py +1 -1
  14. omlish/collections/identity.py +7 -0
  15. omlish/collections/indexed.py +1 -1
  16. omlish/collections/utils.py +38 -6
  17. omlish/configs/__init__.py +5 -0
  18. omlish/configs/classes.py +53 -0
  19. omlish/configs/strings.py +94 -0
  20. omlish/dataclasses/__init__.py +9 -0
  21. omlish/dataclasses/impl/api.py +1 -1
  22. omlish/dataclasses/impl/as_.py +1 -1
  23. omlish/dataclasses/impl/copy.py +30 -0
  24. omlish/dataclasses/impl/exceptions.py +6 -0
  25. omlish/dataclasses/impl/fields.py +25 -25
  26. omlish/dataclasses/impl/init.py +5 -3
  27. omlish/dataclasses/impl/main.py +3 -0
  28. omlish/dataclasses/impl/metaclass.py +6 -1
  29. omlish/dataclasses/impl/order.py +1 -1
  30. omlish/dataclasses/impl/reflect.py +15 -2
  31. omlish/dataclasses/utils.py +44 -0
  32. omlish/defs.py +1 -1
  33. omlish/diag/__init__.py +4 -0
  34. omlish/diag/procfs.py +31 -3
  35. omlish/diag/procstats.py +32 -0
  36. omlish/{testing → diag}/pydevd.py +35 -0
  37. omlish/diag/replserver/console.py +3 -3
  38. omlish/diag/replserver/server.py +6 -5
  39. omlish/diag/threads.py +86 -0
  40. omlish/dispatch/_dispatch2.py +65 -0
  41. omlish/dispatch/_dispatch3.py +104 -0
  42. omlish/docker.py +20 -1
  43. omlish/fnpairs.py +37 -18
  44. omlish/graphs/dags.py +113 -0
  45. omlish/graphs/domination.py +268 -0
  46. omlish/graphs/trees.py +2 -2
  47. omlish/http/__init__.py +25 -0
  48. omlish/http/asgi.py +132 -0
  49. omlish/http/collections.py +15 -0
  50. omlish/http/consts.py +47 -5
  51. omlish/http/cookies.py +194 -0
  52. omlish/http/dates.py +70 -0
  53. omlish/http/encodings.py +6 -0
  54. omlish/http/json.py +273 -0
  55. omlish/http/sessions.py +204 -0
  56. omlish/inject/__init__.py +51 -17
  57. omlish/inject/binder.py +185 -5
  58. omlish/inject/bindings.py +3 -36
  59. omlish/inject/eagers.py +2 -8
  60. omlish/inject/elements.py +30 -9
  61. omlish/inject/exceptions.py +3 -3
  62. omlish/inject/impl/elements.py +65 -31
  63. omlish/inject/impl/injector.py +20 -2
  64. omlish/inject/impl/inspect.py +33 -5
  65. omlish/inject/impl/multis.py +74 -0
  66. omlish/inject/impl/origins.py +75 -0
  67. omlish/inject/impl/{private.py → privates.py} +2 -2
  68. omlish/inject/impl/providers.py +19 -39
  69. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  70. omlish/inject/impl/scopes.py +7 -2
  71. omlish/inject/injector.py +9 -4
  72. omlish/inject/inspect.py +18 -0
  73. omlish/inject/keys.py +11 -23
  74. omlish/inject/listeners.py +26 -0
  75. omlish/inject/managed.py +76 -10
  76. omlish/inject/multis.py +120 -0
  77. omlish/inject/origins.py +27 -0
  78. omlish/inject/overrides.py +5 -4
  79. omlish/inject/{private.py → privates.py} +6 -10
  80. omlish/inject/providers.py +12 -85
  81. omlish/inject/scopes.py +20 -9
  82. omlish/inject/types.py +2 -8
  83. omlish/iterators.py +13 -0
  84. omlish/lang/__init__.py +12 -2
  85. omlish/lang/cached.py +2 -2
  86. omlish/lang/classes/restrict.py +3 -2
  87. omlish/lang/classes/simple.py +18 -8
  88. omlish/lang/classes/virtual.py +2 -2
  89. omlish/lang/contextmanagers.py +75 -2
  90. omlish/lang/datetimes.py +6 -5
  91. omlish/lang/descriptors.py +131 -0
  92. omlish/lang/functions.py +18 -28
  93. omlish/lang/imports.py +11 -2
  94. omlish/lang/iterables.py +20 -1
  95. omlish/lang/typing.py +6 -0
  96. omlish/lifecycles/__init__.py +34 -0
  97. omlish/lifecycles/abstract.py +43 -0
  98. omlish/lifecycles/base.py +51 -0
  99. omlish/lifecycles/contextmanagers.py +74 -0
  100. omlish/lifecycles/controller.py +116 -0
  101. omlish/lifecycles/manager.py +161 -0
  102. omlish/lifecycles/states.py +43 -0
  103. omlish/lifecycles/transitions.py +64 -0
  104. omlish/logs/formatters.py +1 -1
  105. omlish/logs/utils.py +1 -1
  106. omlish/marshal/__init__.py +4 -0
  107. omlish/marshal/datetimes.py +1 -1
  108. omlish/marshal/naming.py +4 -0
  109. omlish/marshal/objects.py +1 -0
  110. omlish/marshal/polymorphism.py +4 -4
  111. omlish/reflect.py +139 -18
  112. omlish/secrets/__init__.py +7 -0
  113. omlish/secrets/marshal.py +41 -0
  114. omlish/secrets/passwords.py +120 -0
  115. omlish/secrets/secrets.py +47 -0
  116. omlish/serde/__init__.py +0 -0
  117. omlish/serde/dotenv.py +574 -0
  118. omlish/{json.py → serde/json.py} +4 -2
  119. omlish/serde/props.py +604 -0
  120. omlish/serde/yaml.py +223 -0
  121. omlish/sql/dbs.py +1 -1
  122. omlish/sql/duckdb.py +136 -0
  123. omlish/sql/sqlean.py +17 -0
  124. omlish/sync.py +70 -0
  125. omlish/term.py +7 -2
  126. omlish/testing/pytest/__init__.py +8 -2
  127. omlish/testing/pytest/helpers.py +0 -24
  128. omlish/testing/pytest/inject/harness.py +4 -4
  129. omlish/testing/pytest/marks.py +45 -0
  130. omlish/testing/pytest/plugins/__init__.py +3 -0
  131. omlish/testing/pytest/plugins/asyncs.py +136 -0
  132. omlish/testing/pytest/plugins/managermarks.py +60 -0
  133. omlish/testing/pytest/plugins/pydevd.py +1 -1
  134. omlish/testing/testing.py +10 -0
  135. omlish/text/delimit.py +4 -0
  136. omlish/text/glyphsplit.py +92 -0
  137. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  138. omlish-0.0.0.dev6.dist-info/RECORD +240 -0
  139. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +1 -1
  140. omlish/configs/props.py +0 -64
  141. omlish-0.0.0.dev4.dist-info/RECORD +0 -195
  142. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  143. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/serde/props.py ADDED
@@ -0,0 +1,604 @@
1
+ # jProperties - Java Property file parser and writer for Python
2
+ #
3
+ # Copyright (c) 2015, Tilman Blumenbach
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
7
+ # following conditions are met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
10
+ # disclaimer.
11
+ #
12
+ # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
13
+ # disclaimer in the documentation and/or other materials provided with the distribution.
14
+ #
15
+ # * Neither the name of jProperties nor the names of its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
19
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
+ import codecs
26
+ import collections.abc
27
+ import contextlib
28
+ import functools
29
+ import io
30
+ import itertools
31
+ import re
32
+ import struct
33
+ import sys
34
+ import time
35
+ import typing as ta
36
+
37
+
38
+ class PropertyTuple(ta.NamedTuple):
39
+ data: ta.Any
40
+ meta: ta.Any
41
+
42
+
43
+ def _is_runtime_meta(key: str | bytes) -> bool:
44
+ return (
45
+ (isinstance(key, str) and key.startswith('__')) or
46
+ (isinstance(key, bytes) and key.startswith(b'__'))
47
+ )
48
+
49
+
50
+ def _escape_non_ascii(unicode_obj: str | bytes) -> str:
51
+ def replace(match):
52
+ s = match.group(0)
53
+ n = ord(s)
54
+ if n < 0x10000:
55
+ return f'\\u{n:04x}'
56
+ else:
57
+ n -= 0x10000
58
+ s1 = 0xd800 | ((n >> 10) & 0x3ff)
59
+ s2 = 0xdc00 | (n & 0x3ff)
60
+ return f'\\u{s1:04x}\\u{s2:04x}'
61
+
62
+ if isinstance(unicode_obj, bytes):
63
+ unicode_obj = unicode_obj.decode('utf-8')
64
+
65
+ return re.sub(r'[^ -~]', replace, unicode_obj)
66
+
67
+
68
+ _jbackslash_replace_codec_name = __name__ + '.jbackslashreplace'
69
+
70
+
71
+ @functools.partial(codecs.register_error, _jbackslash_replace_codec_name)
72
+ def _jbackslashreplace_error_handler(err):
73
+ if not isinstance(err, UnicodeEncodeError):
74
+ raise err
75
+
76
+ return _escape_non_ascii(err.object[err.start:err.end]), err.end
77
+
78
+
79
+ def _escape_str(
80
+ raw_str: ta.Any,
81
+ *,
82
+ only_leading_spaces: bool = False,
83
+ escape_non_printing: bool = False,
84
+ line_breaks_only: bool = False,
85
+ ) -> str:
86
+ if isinstance(raw_str, bytes):
87
+ raw_str = raw_str.decode('utf-8')
88
+ elif not isinstance(raw_str, str):
89
+ raw_str = str(raw_str)
90
+
91
+ trans_dict = {
92
+ ord('\r'): '\\r',
93
+ ord('\n'): '\\n',
94
+ ord('\f'): '\\f',
95
+ }
96
+
97
+ if not line_breaks_only:
98
+ trans_dict.update(
99
+ {
100
+ ord('#'): '\\#',
101
+ ord('!'): '\\!',
102
+ ord('='): '\\=',
103
+ ord(':'): '\\:',
104
+ ord('\\'): '\\\\',
105
+ ord('\t'): '\\t',
106
+ },
107
+ )
108
+
109
+ escaped_str = raw_str.translate(trans_dict)
110
+
111
+ if not only_leading_spaces:
112
+ escaped_str = escaped_str.replace(' ', '\\ ')
113
+ else:
114
+ escaped_str = re.sub('^ ', '\\\\ ', escaped_str)
115
+
116
+ if escape_non_printing:
117
+ escaped_str = _escape_non_ascii(escaped_str)
118
+
119
+ return escaped_str
120
+
121
+
122
+ class PropertyError(Exception):
123
+ """Base exception class for all exceptions raised by this module."""
124
+
125
+
126
+ class ParseError(PropertyError):
127
+ def __init__(self, message: str, line_number: int, file_obj: ta.Any = None) -> None:
128
+ super().__init__()
129
+ self.message = message
130
+ self.line_number = line_number
131
+ self.file_obj = file_obj
132
+
133
+ def __str__(self) -> str:
134
+ filename = '<unknown>' if not hasattr(self.file_obj, 'filename') else self.file_obj.filename
135
+ return f'Parse error in {filename}:{self.line_number}: {self.message}'
136
+
137
+
138
+ class Properties(collections.abc.MutableMapping):
139
+ """
140
+ A parser for Java property files.
141
+
142
+ This class implements parsing Java property files as defined here:
143
+ http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#load(java.io.Reader)
144
+ """
145
+
146
+ _EOL = '\r\n'
147
+ _WHITESPACE = ' \t\f'
148
+ _ALLWHITESPACE = _EOL + _WHITESPACE
149
+
150
+ def __init__(
151
+ self,
152
+ *,
153
+ process_escapes_in_values: bool = True,
154
+ ) -> None:
155
+ super().__init__()
156
+
157
+ self._process_escapes_in_values = process_escapes_in_values
158
+
159
+ self.reset()
160
+ self.clear()
161
+
162
+ _source_file: ta.IO[str] | None
163
+ _next_metadata: dict[str, str]
164
+ _lookahead: str | None = None
165
+ _prev_key: str | None
166
+ _metadata: dict[str, ta.Any]
167
+ _key_order: list[str]
168
+ _properties: dict[str, ta.Any]
169
+ _line_number: int
170
+ _metadoc: bool
171
+
172
+ def __len__(self) -> int:
173
+ return len(self._properties)
174
+
175
+ def __getitem__(self, item: str) -> PropertyTuple:
176
+ if not isinstance(item, str):
177
+ raise TypeError('Property keys must be of type str')
178
+
179
+ if item not in self._properties:
180
+ raise KeyError('Key not found')
181
+
182
+ return PropertyTuple(
183
+ self._properties[item],
184
+ self._metadata.get(item, {}),
185
+ )
186
+
187
+ def __setitem__(self, key: str, value) -> None:
188
+ if not isinstance(key, str):
189
+ raise TypeError('Property keys must be of type str')
190
+
191
+ metadata = None
192
+ if isinstance(value, tuple):
193
+ value, metadata = value
194
+
195
+ if not isinstance(value, str):
196
+ raise TypeError('Property values must be of type str')
197
+
198
+ if metadata is not None and not isinstance(metadata, dict):
199
+ raise TypeError('Metadata needs to be a dictionary')
200
+
201
+ self._properties[key] = value
202
+ if metadata is not None:
203
+ self._metadata[key] = metadata
204
+
205
+ def __delitem__(self, key: str) -> None:
206
+ if not isinstance(key, str):
207
+ raise TypeError('Property keys must be of type str')
208
+
209
+ if key not in self._properties:
210
+ raise KeyError('Key not found')
211
+
212
+ # Remove the property itself.
213
+ del self._properties[key]
214
+
215
+ # Remove its metadata as well.
216
+ if key in self._metadata:
217
+ del self._metadata[key]
218
+
219
+ # We also no longer need to remember its key order since the property does not exist anymore.
220
+ with contextlib.suppress(ValueError):
221
+ self._key_order.remove(key)
222
+
223
+ def __iter__(self) -> ta.Iterator:
224
+ return self._properties.__iter__()
225
+
226
+ @property
227
+ def properties(self):
228
+ return self._properties
229
+
230
+ @properties.setter
231
+ def properties(self, value):
232
+ self._properties = value
233
+
234
+ @properties.deleter
235
+ def properties(self):
236
+ self._properties = {}
237
+
238
+ def getmeta(self, key: str) -> dict:
239
+ return self._metadata.get(key, {})
240
+
241
+ def setmeta(self, key: str, metadata: dict):
242
+ if not isinstance(metadata, dict):
243
+ raise TypeError('Metadata needs to be a dictionary')
244
+
245
+ self._metadata[key] = metadata
246
+
247
+ def _peek(self) -> str:
248
+ if self._lookahead is None:
249
+ c = self._source_file.read(1) # type: ignore
250
+ if c == '':
251
+ raise EOFError
252
+ self._lookahead = c
253
+ return self._lookahead
254
+
255
+ def _getc(self) -> str:
256
+ c = self._peek()
257
+ self._lookahead = None
258
+ return c
259
+
260
+ def _handle_eol(self) -> None:
261
+ c = self._peek()
262
+
263
+ if c == '\r':
264
+ self._line_number += 1
265
+ self._getc()
266
+ try:
267
+ if self._peek() == '\n':
268
+ # DOS line ending. Skip it.
269
+ self._getc()
270
+ except EOFError:
271
+ pass
272
+
273
+ elif c == '\n':
274
+ self._line_number += 1
275
+ self._getc()
276
+
277
+ def _skip_whitespace(self, stop_at_eol: bool = False) -> None:
278
+ while True:
279
+ c = self._peek()
280
+ if c not in self._ALLWHITESPACE:
281
+ return
282
+
283
+ if c in self._EOL:
284
+ if stop_at_eol:
285
+ return
286
+ self._handle_eol()
287
+
288
+ else:
289
+ self._getc()
290
+
291
+ def _skip_natural_line(self) -> str:
292
+ line = ''
293
+ try:
294
+ while self._peek() not in self._EOL:
295
+ line += self._getc()
296
+ self._handle_eol()
297
+ except EOFError:
298
+ pass
299
+ return line
300
+
301
+ def _parse_comment(self) -> None:
302
+ self._getc()
303
+
304
+ if self._peek() != ':':
305
+ docstr = self._skip_natural_line()
306
+ if self._metadoc and self._prev_key:
307
+ prev_metadata = self._metadata.setdefault(self._prev_key, {})
308
+ prev_metadata.setdefault('_doc', '')
309
+ if docstr.startswith(' '):
310
+ docstr = docstr[1:]
311
+ prev_metadata['_doc'] += docstr + '\n'
312
+ return
313
+
314
+ self._getc()
315
+
316
+ key = self._parse_key(True)
317
+ value = self._parse_value(True)
318
+
319
+ if not key:
320
+ raise ParseError('Empty key in metadata key-value pair', self._line_number, self._source_file)
321
+
322
+ self._next_metadata[key] = value
323
+
324
+ # \r, \n, \t or \f.
325
+ _ESCAPED_CHARS: ta.ClassVar[ta.Mapping[str, str]] = {
326
+ ec: eval(r"u'\%s'" % (ec,)) # noqa
327
+ for ec in 'rntf'
328
+ }
329
+
330
+ def _handle_escape(self, allow_line_continuation: bool = True) -> str:
331
+ if self._peek() == '\\':
332
+ self._getc()
333
+
334
+ try:
335
+ escaped_char = self._peek()
336
+ except EOFError:
337
+ return ''
338
+
339
+ if escaped_char in self._EOL:
340
+ if allow_line_continuation:
341
+ try:
342
+ self._handle_eol()
343
+ self._skip_whitespace(True)
344
+ except EOFError:
345
+ pass
346
+
347
+ return ''
348
+
349
+ self._getc()
350
+
351
+ try:
352
+ return self._ESCAPED_CHARS[escaped_char]
353
+ except KeyError:
354
+ pass
355
+
356
+ if escaped_char == 'u':
357
+ start_linenumber = self._line_number
358
+
359
+ try:
360
+ codepoint_hex = ''
361
+ for _ in range(4):
362
+ codepoint_hex += self._getc()
363
+
364
+ codepoint = int(codepoint_hex, base=16)
365
+
366
+ # See: http://unicodebook.readthedocs.io/unicode_encodings.html#utf-16-surrogate-pairs
367
+ if 0xD800 <= codepoint <= 0xDBFF:
368
+ codepoint2_hex = ''
369
+ try:
370
+ for _ in range(6):
371
+ codepoint2_hex += self._getc()
372
+ except EOFError:
373
+ pass
374
+
375
+ if codepoint2_hex[:2] != r'\u' or len(codepoint2_hex) != 6:
376
+ raise ParseError(
377
+ 'High surrogate unicode escape sequence not followed by another '
378
+ '(low surrogate) unicode escape sequence.',
379
+ start_linenumber,
380
+ self._source_file,
381
+ )
382
+
383
+ codepoint2 = int(codepoint2_hex[2:], base=16)
384
+ if not (0xDC00 <= codepoint2 <= 0xDFFF):
385
+ raise ParseError(
386
+ 'Low surrogate unicode escape sequence expected after high surrogate '
387
+ 'escape sequence, but got a non-low-surrogate unicode escape sequence.',
388
+ start_linenumber,
389
+ self._source_file,
390
+ )
391
+
392
+ final_codepoint = 0x10000
393
+ final_codepoint += (codepoint & 0x03FF) << 10
394
+ final_codepoint += codepoint2 & 0x03FF
395
+
396
+ codepoint = final_codepoint
397
+
398
+ return struct.pack('=I', codepoint).decode('utf-32')
399
+ except (EOFError, ValueError) as e:
400
+ raise ParseError('Parse error', start_linenumber, self._source_file) from e
401
+
402
+ return escaped_char
403
+
404
+ def _parse_key(self, single_line_only: bool = False) -> str:
405
+ self._skip_whitespace(single_line_only)
406
+
407
+ key = ''
408
+ while True:
409
+ try:
410
+ c = self._peek()
411
+ except EOFError:
412
+ break
413
+
414
+ if c == '\\':
415
+ key += self._handle_escape(not single_line_only)
416
+ continue
417
+
418
+ if c in self._ALLWHITESPACE or c in ':=':
419
+ break
420
+
421
+ key += self._getc()
422
+
423
+ return key
424
+
425
+ def _parse_value(self, single_line_only: bool = False) -> str:
426
+ try:
427
+ self._skip_whitespace(True)
428
+ if self._peek() in ':=':
429
+ self._getc()
430
+ self._skip_whitespace(True)
431
+ except EOFError:
432
+ return ''
433
+
434
+ value = ''
435
+ while True:
436
+ try:
437
+ c = self._peek()
438
+ except EOFError:
439
+ break
440
+
441
+ if c == '\\' and self._process_escapes_in_values:
442
+ value += self._handle_escape(not single_line_only)
443
+ continue
444
+
445
+ if c in self._EOL:
446
+ self._handle_eol()
447
+ break
448
+
449
+ value += self._getc()
450
+
451
+ return value
452
+
453
+ def _parse_logical_line(self) -> bool:
454
+ try:
455
+ self._skip_whitespace()
456
+ c = self._peek()
457
+ except EOFError:
458
+ return False
459
+
460
+ if c in '!#':
461
+ try:
462
+ self._parse_comment()
463
+ except EOFError:
464
+ # Nothing more to parse.
465
+ return False
466
+
467
+ return True
468
+
469
+ try:
470
+ key = self._parse_key()
471
+ value = self._parse_value()
472
+ except EOFError:
473
+ return False
474
+
475
+ if key not in self._properties:
476
+ self._key_order.append(key)
477
+
478
+ self._properties[key] = value
479
+
480
+ if self._next_metadata:
481
+ self._metadata[key] = self._next_metadata
482
+ self._next_metadata = {}
483
+ self._prev_key = key
484
+
485
+ return True
486
+
487
+ def _parse(self) -> None:
488
+ while self._parse_logical_line():
489
+ pass
490
+
491
+ def reset(self, metadoc: bool = False) -> None:
492
+ self._source_file = None
493
+ self._line_number = 1
494
+ self._lookahead = None
495
+
496
+ self._next_metadata = {}
497
+
498
+ self._prev_key = None
499
+ self._metadoc = metadoc
500
+
501
+ def clear(self) -> None:
502
+ self._properties = {}
503
+ self._metadata = {}
504
+ self._key_order = []
505
+
506
+ def load(
507
+ self,
508
+ source_data,
509
+ encoding: str | None = 'iso-8859-1',
510
+ metadoc: bool = False,
511
+ ) -> None:
512
+ self.reset(metadoc)
513
+
514
+ if isinstance(source_data, bytes):
515
+ self._source_file = io.StringIO(source_data.decode(encoding or 'iso-8859-1'))
516
+ elif isinstance(source_data, str):
517
+ self._source_file = io.StringIO(source_data)
518
+ elif encoding is not None:
519
+ self._source_file = codecs.getreader(encoding)(source_data) # type: ignore
520
+ else:
521
+ self._source_file = source_data
522
+
523
+ self._parse()
524
+
525
+ def store(
526
+ self,
527
+ out_stream,
528
+ initial_comments: str | None = None,
529
+ encoding: str = 'iso-8859-1',
530
+ strict: bool = True,
531
+ strip_meta: bool = True,
532
+ timestamp: bool = True,
533
+ ) -> None:
534
+ out_codec_info = codecs.lookup(encoding)
535
+ wrapped_out_stream = out_codec_info.streamwriter(out_stream, _jbackslash_replace_codec_name)
536
+ properties_escape_nonprinting = strict and out_codec_info == codecs.lookup('latin_1')
537
+
538
+ if initial_comments is not None:
539
+ initial_comments = re.sub(r'(\r\n|\r)', '\n', initial_comments)
540
+ initial_comments = re.sub(r'\n(?![#!])', '\n#', initial_comments)
541
+ initial_comments = re.sub(r'(\n[#!]):', r'\g<1>\:', initial_comments)
542
+ print('#' + initial_comments, file=wrapped_out_stream)
543
+
544
+ if timestamp:
545
+ day_of_week = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
546
+ month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
547
+ now = time.gmtime()
548
+ print(
549
+ '#%s %s %02d %02d:%02d:%02d UTC %04d' % (
550
+ day_of_week[now.tm_wday],
551
+ month[now.tm_mon - 1],
552
+ now.tm_mday,
553
+ now.tm_hour,
554
+ now.tm_min,
555
+ now.tm_sec,
556
+ now.tm_year,
557
+ ),
558
+ file=wrapped_out_stream,
559
+ )
560
+
561
+ unordered_keys = set(self._properties)
562
+ ordered_keys = set(self._key_order)
563
+ unordered_keys -= ordered_keys
564
+
565
+ unordered_keys_xs = list(unordered_keys)
566
+ unordered_keys_xs.sort()
567
+
568
+ for key in itertools.chain(self._key_order, unordered_keys_xs):
569
+ if key in self._properties:
570
+ metadata = self.getmeta(key)
571
+ if not strip_meta and len(metadata):
572
+ for mkey in sorted(metadata):
573
+ if _is_runtime_meta(mkey):
574
+ continue
575
+
576
+ print(
577
+ '#: {}={}'.format( # noqa
578
+ _escape_str(mkey),
579
+ _escape_str(metadata[mkey], only_leading_spaces=True),
580
+ ),
581
+ file=wrapped_out_stream,
582
+ )
583
+
584
+ print(
585
+ '='.join([
586
+ _escape_str(
587
+ key,
588
+ escape_non_printing=properties_escape_nonprinting,
589
+ ),
590
+ _escape_str(
591
+ self._properties[key],
592
+ only_leading_spaces=True,
593
+ escape_non_printing=properties_escape_nonprinting,
594
+ line_breaks_only=not self._process_escapes_in_values,
595
+ ),
596
+ ]),
597
+ file=wrapped_out_stream,
598
+ )
599
+
600
+ def list(self, out_stream=sys.stderr) -> None:
601
+ print('-- listing properties --', file=out_stream)
602
+ for key in self._properties:
603
+ msg = f'{key}={self._properties[key]}'
604
+ print(msg, file=out_stream)