omlish 0.0.0.dev304__py3-none-any.whl → 0.0.0.dev306__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 (35) hide show
  1. omlish/.manifests.json +14 -0
  2. omlish/__about__.py +3 -3
  3. omlish/dataclasses/__init__.py +5 -0
  4. omlish/dataclasses/api/__init__.py +1 -0
  5. omlish/dataclasses/api/fields/metadata.py +8 -0
  6. omlish/dataclasses/tools/only_.py +45 -0
  7. omlish/diag/_pycharm/runhack.py +4 -1
  8. omlish/formats/edn/LICENSE +16 -0
  9. omlish/formats/edn/__init__.py +15 -0
  10. omlish/formats/edn/codec.py +26 -0
  11. omlish/formats/edn/parsing.py +359 -0
  12. omlish/formats/edn/values.py +162 -0
  13. omlish/graphs/trees.py +6 -0
  14. omlish/lang/__init__.py +7 -0
  15. omlish/lang/cached/function.py +7 -2
  16. omlish/lang/objects.py +28 -0
  17. omlish/lang/recursion.py +109 -0
  18. omlish/marshal/__init__.py +1 -1
  19. omlish/marshal/objects/helpers.py +1 -1
  20. omlish/sql/api/__init__.py +8 -1
  21. omlish/sql/api/base.py +1 -1
  22. omlish/sql/api/funcs.py +70 -11
  23. omlish/sql/qualifiedname.py +20 -12
  24. omlish/sql/queries/__init__.py +3 -0
  25. omlish/sql/queries/base.py +16 -0
  26. omlish/sql/queries/idents.py +12 -1
  27. omlish/sql/queries/names.py +8 -1
  28. omlish/sql/queries/relations.py +0 -12
  29. omlish/sql/queries/rendering.py +0 -3
  30. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/METADATA +3 -3
  31. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/RECORD +35 -28
  32. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/WHEEL +0 -0
  33. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/entry_points.txt +0 -0
  34. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/licenses/LICENSE +0 -0
  35. {omlish-0.0.0.dev304.dist-info → omlish-0.0.0.dev306.dist-info}/top_level.txt +0 -0
omlish/.manifests.json CHANGED
@@ -51,6 +51,20 @@
51
51
  }
52
52
  }
53
53
  },
54
+ {
55
+ "module": ".formats.edn.codec",
56
+ "attr": "_EDN_LAZY_CODEC",
57
+ "file": "omlish/formats/edn/codec.py",
58
+ "line": 25,
59
+ "value": {
60
+ "$.codecs.base.LazyLoadedCodec": {
61
+ "mod_name": "omlish.formats.edn.codec",
62
+ "attr_name": "EDN_CODEC",
63
+ "name": "edn",
64
+ "aliases": null
65
+ }
66
+ }
67
+ },
54
68
  {
55
69
  "module": ".formats.ini.codec",
56
70
  "attr": "_INI_LAZY_CODEC",
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev304'
2
- __revision__ = '0ecc67df99993947b491b2d6d6163fd00b00202e'
1
+ __version__ = '0.0.0.dev306'
2
+ __revision__ = 'dda70244f84192bfce2428b682f9a5e2349fb05a'
3
3
 
4
4
 
5
5
  #
@@ -101,7 +101,7 @@ class Project(ProjectBase):
101
101
 
102
102
  'apsw ~= 3.49',
103
103
 
104
- 'sqlean.py ~= 3.47',
104
+ 'sqlean.py ~= 3.49',
105
105
 
106
106
  'duckdb ~= 1.2',
107
107
  ],
@@ -60,6 +60,7 @@ from .api import ( # noqa
60
60
  extra_field_params,
61
61
  set_field_metadata,
62
62
  update_extra_field_params,
63
+ with_extra_field_params,
63
64
  )
64
65
 
65
66
  from .errors import ( # noqa
@@ -108,6 +109,10 @@ from .tools.modifiers import ( # noqa
108
109
  update_fields,
109
110
  )
110
111
 
112
+ from .tools.only_ import ( # noqa
113
+ only,
114
+ )
115
+
111
116
  from .tools.replace import ( # noqa
112
117
  deep_replace,
113
118
  )
@@ -18,6 +18,7 @@ from .fields.metadata import ( # noqa
18
18
  extra_field_params,
19
19
  set_field_metadata,
20
20
  update_extra_field_params,
21
+ with_extra_field_params,
21
22
  )
22
23
 
23
24
  from .fields.constructor import ( # noqa
@@ -5,6 +5,7 @@ from .... import check
5
5
  from .... import lang
6
6
  from ...debug import DEBUG
7
7
  from ...specs import FieldSpec
8
+ from ...tools.modifiers import field_modifier
8
9
  from ...utils import chain_mapping_proxy
9
10
 
10
11
 
@@ -92,3 +93,10 @@ def update_extra_field_params(
92
93
  **(fe if unless_non_default else {}),
93
94
  },
94
95
  })
96
+
97
+
98
+ def with_extra_field_params(**kwargs: ta.Any) -> field_modifier:
99
+ @field_modifier
100
+ def inner(f: dc.Field) -> dc.Field:
101
+ return update_extra_field_params(f, **kwargs)
102
+ return inner
@@ -0,0 +1,45 @@
1
+ import collections.abc
2
+ import typing as ta
3
+
4
+ from .iter import fields_dict
5
+
6
+
7
+ ##
8
+
9
+
10
+ def _only_test(v: ta.Any) -> bool:
11
+ if v is None:
12
+ return False
13
+ elif isinstance(v, collections.abc.Iterable):
14
+ return bool(v)
15
+ else:
16
+ return True
17
+
18
+
19
+ def only(
20
+ obj: ta.Any,
21
+ *flds: str,
22
+ all: bool = False, # noqa
23
+ test: ta.Callable[[ta.Any], bool] = _only_test,
24
+ ) -> bool:
25
+ fdct = fields_dict(obj)
26
+ for fn in flds:
27
+ if fn not in fdct:
28
+ raise KeyError(fn)
29
+
30
+ rem = set(flds)
31
+ for fn, f in fdct.items():
32
+ if not f.compare and fn not in rem:
33
+ continue
34
+
35
+ v = getattr(obj, fn)
36
+ if test(v):
37
+ if fn in rem:
38
+ rem.remove(fn)
39
+ else:
40
+ return False
41
+
42
+ if rem and all:
43
+ return False
44
+
45
+ return True
@@ -24,7 +24,10 @@ DEBUG_ENV_VAR = 'OMLISH_PYCHARM_RUNHACK_DEBUG'
24
24
 
25
25
  #
26
26
 
27
- _DEFAULT_PTH_FILE_NAME = 'omlish-pycharm-runhack.pth'
27
+ # This hack must run first, before things like '__editable__.foo.pth' files installed by setuptools. pth file processing
28
+ # is sorted by name, so prepend with underscores:
29
+ # https://github.com/python/cpython/blob/aeb3a6f61af53ed3fbf31f0b3704f49b71ac553c/Lib/site.py#L246
30
+ _DEFAULT_PTH_FILE_NAME = '___omlish-pycharm-runhack.pth'
28
31
 
29
32
  _DEFAULT_DEBUG = False
30
33
  _DEFAULT_ENABLED = True
@@ -0,0 +1,16 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Jorin Vogel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,15 @@
1
+ from .parsing import ( # Noqa
2
+ parse,
3
+ parse_list,
4
+ )
5
+
6
+ from .values import ( # noqa
7
+ Char,
8
+ Keyword,
9
+ List,
10
+ Map,
11
+ Set,
12
+ Symbol,
13
+ TaggedVal,
14
+ Vector,
15
+ )
@@ -0,0 +1,26 @@
1
+ import typing as ta
2
+
3
+ from ..codecs import make_object_lazy_loaded_codec
4
+ from ..codecs import make_str_object_codec
5
+ from .parsing import parse
6
+
7
+
8
+ ##
9
+
10
+
11
+ def dumps(obj: ta.Any) -> str:
12
+ # return json.dumps(obj)
13
+ raise NotImplementedError
14
+
15
+
16
+ def loads(s: str) -> ta.Any:
17
+ return parse(s)
18
+
19
+
20
+ ##
21
+
22
+
23
+ EDN_CODEC = make_str_object_codec('edn', dumps, loads)
24
+
25
+ # @omlish-manifest
26
+ _EDN_LAZY_CODEC = make_object_lazy_loaded_codec(__name__, 'EDN_CODEC', EDN_CODEC)
@@ -0,0 +1,359 @@
1
+ """
2
+ TODO:
3
+ - reader meta - ^:foo
4
+ """
5
+ # https://github.com/jorinvo/edn-data/blob/1e5824f63803eb58f35e98839352000053d47115/test/parse.test.ts
6
+ import datetime
7
+ import enum
8
+ import re
9
+ import typing as ta
10
+
11
+ from ... import check
12
+ from .values import Char
13
+ from .values import Keyword
14
+ from .values import List
15
+ from .values import Map
16
+ from .values import Set
17
+ from .values import Symbol
18
+ from .values import TaggedVal
19
+ from .values import Vector
20
+
21
+
22
+ ##
23
+
24
+
25
+ class ListParser:
26
+ DEFAULT_TAG_HANDLERS: ta.ClassVar[ta.Mapping[str, ta.Callable[..., ta.Any]]] = {
27
+ 'inst': lambda val: datetime.datetime.fromisoformat(val) if isinstance(val, str) else None,
28
+ }
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ keyword_maker: ta.Callable[..., ta.Any] = Keyword,
34
+ char_maker: ta.Callable[..., ta.Any] = Char,
35
+ symbol_maker: ta.Callable[..., ta.Any] = Symbol,
36
+
37
+ list_maker: ta.Callable[..., ta.Any] = List.new,
38
+ vector_maker: ta.Callable[..., ta.Any] = Vector.new,
39
+ set_maker: ta.Callable[..., ta.Any] = Set.new,
40
+ map_maker: ta.Callable[..., ta.Any] = Map.new,
41
+
42
+ tag_handlers: ta.Mapping[str, ta.Callable[..., ta.Any]] | None = None,
43
+ ) -> None:
44
+ super().__init__()
45
+
46
+ self._keyword_maker = keyword_maker
47
+ self._char_maker = char_maker
48
+ self._symbol_maker = symbol_maker
49
+
50
+ self._list_maker = list_maker
51
+ self._vector_maker = vector_maker
52
+ self._set_maker = set_maker
53
+ self._map_maker = map_maker
54
+
55
+ self._tag_handlers = {
56
+ **self.DEFAULT_TAG_HANDLERS,
57
+ **(tag_handlers or {}),
58
+ }
59
+
60
+ self._stack: list[tuple[ListParser._ParseMode | ListParser._StackItem, ta.Any]] = []
61
+ self._mode: ListParser._ParseMode = ListParser._ParseMode.IDLE
62
+ self._state = ''
63
+ self._result: ta.Any = self._UNDEFINED
64
+
65
+ #
66
+
67
+ class _UNDEFINED: # noqa
68
+ def __new__(cls, *args, **kwargs): # noqa
69
+ raise TypeError
70
+
71
+ class _ParseMode(enum.Enum):
72
+ IDLE = 0
73
+ STRING = 1
74
+ ESCAPE = 2
75
+ COMMENT = 3
76
+
77
+ class _StackItem(enum.Enum):
78
+ VECTOR = 0
79
+ LIST = 1
80
+ MAP = 2
81
+ SET = 3
82
+ TAG = 4
83
+
84
+ #
85
+
86
+ def _update_stack(self) -> None:
87
+ if not self._stack or self._result is self._UNDEFINED:
88
+ return
89
+
90
+ stack_item, prev_state = self._stack[-1]
91
+
92
+ if stack_item == ListParser._StackItem.VECTOR:
93
+ prev_state.append(self._result)
94
+
95
+ elif stack_item == ListParser._StackItem.LIST:
96
+ prev_state.append(self._result)
97
+
98
+ elif stack_item == ListParser._StackItem.SET:
99
+ prev_state.append(self._result)
100
+
101
+ elif stack_item == ListParser._StackItem.MAP:
102
+ if len(prev_state[1]) > 0:
103
+ prev_state[0].append([prev_state[1].pop(), self._result])
104
+ else:
105
+ prev_state[1].append(self._result)
106
+
107
+ elif stack_item == ListParser._StackItem.TAG:
108
+ self._stack.pop()
109
+
110
+ if prev_state == '_':
111
+ self._result = self._UNDEFINED
112
+
113
+ else:
114
+ tag_handler = self._tag_handlers.get(prev_state)
115
+ if tag_handler:
116
+ self._result = tag_handler(self._result)
117
+ else:
118
+ self._result = TaggedVal(prev_state, self._result)
119
+
120
+ self._update_stack()
121
+ return
122
+
123
+ # TODO: else error
124
+ # Reset result after updating stack
125
+ self._result = self._UNDEFINED
126
+
127
+ #
128
+
129
+ _INT_PAT = re.compile(r'^[-+]?(0|[1-9][0-9]*)$')
130
+ _BIGINT_PAT = re.compile(r'^[-+]?(0|[1-9][0-9]*)N$')
131
+ _FLOAT_PAT = re.compile(r'^[-+]?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?(0|[1-9][0-9]*))?M?$')
132
+
133
+ def _match(self) -> None:
134
+ if self._state == 'nil':
135
+ self._result = None
136
+
137
+ elif self._state == 'true':
138
+ self._result = True
139
+
140
+ elif self._state == 'false':
141
+ self._result = False
142
+
143
+ elif self._state.startswith(':'):
144
+ # Keyword
145
+ self._result = self._keyword_maker(self._state[1:])
146
+
147
+ elif self._state.startswith('#'):
148
+ # Tag
149
+ self._stack.append((ListParser._StackItem.TAG, self._state[1:]))
150
+ self._result = self._UNDEFINED
151
+
152
+ elif self._INT_PAT.match(self._state):
153
+ # Int
154
+ self._result = int(self._state)
155
+
156
+ elif self._FLOAT_PAT.match(self._state):
157
+ # Float
158
+ self._result = float(self._state)
159
+
160
+ elif self._BIGINT_PAT.match(self._state):
161
+ # BigInt
162
+ self._result = int(self._state[:-1]) # In Python we don't need special handling for bigint
163
+
164
+ elif self._state.startswith('\\'):
165
+ # Char
166
+ check.state(len(self._state) > 1)
167
+ if self._state == '\\space':
168
+ c = ' '
169
+ elif self._state == '\\newline':
170
+ c = '\n'
171
+ elif self._state == '\\return':
172
+ c = '\r'
173
+ elif self._state == '\\tab':
174
+ c = '\t'
175
+ elif self._state == '\\\\':
176
+ c = '\\'
177
+ elif self._state.startswith('\\u'):
178
+ check.state(len(self._state) == 6)
179
+ c = chr(int(self._state[2:], 16))
180
+ else:
181
+ check.state(len(self._state) == 2)
182
+ c = self._state[1:]
183
+
184
+ self._result = self._char_maker(c)
185
+
186
+ elif self._state:
187
+ # Symbol
188
+ self._result = self._symbol_maker(self._state)
189
+
190
+ self._state = ''
191
+
192
+ #
193
+
194
+ _SPACE_CHARS: ta.ClassVar[ta.AbstractSet[str]] = frozenset([',', ' ', '\t', '\n', '\r'])
195
+
196
+ _STRING_ESCAPE_MAP: ta.ClassVar[ta.Mapping[str, str]] = {
197
+ 't': '\t',
198
+ 'r': '\r',
199
+ 'n': '\n',
200
+ '\\': '\\',
201
+ '"': '"',
202
+ }
203
+
204
+ def parse(self, src: str) -> list[ta.Any]:
205
+ values = []
206
+
207
+ i = -1
208
+ for i in range(len(src)):
209
+ if not self._stack and self._result is not self._UNDEFINED:
210
+ values.append(self._result)
211
+ self._result = self._UNDEFINED
212
+
213
+ char = src[i]
214
+
215
+ if self._mode == ListParser._ParseMode.IDLE:
216
+ if char == '"':
217
+ self._match()
218
+ self._update_stack()
219
+ self._mode = ListParser._ParseMode.STRING
220
+ self._state = ''
221
+ continue
222
+
223
+ if char == ';':
224
+ self._mode = ListParser._ParseMode.COMMENT
225
+ continue
226
+
227
+ if char in self._SPACE_CHARS:
228
+ self._match()
229
+ self._update_stack()
230
+ continue
231
+
232
+ if char == '}':
233
+ self._match()
234
+ self._update_stack()
235
+
236
+ if self._stack:
237
+ stack_item, prev_state = self._stack.pop()
238
+
239
+ if stack_item == ListParser._StackItem.MAP:
240
+ check.empty(prev_state[1])
241
+ self._result = self._map_maker(prev_state[0])
242
+
243
+ else: # Set
244
+ # FIXME:
245
+ # check.state(stack_item == ListParser._StackItem.SET)
246
+ self._result = self._set_maker(prev_state)
247
+
248
+ self._update_stack()
249
+ continue
250
+
251
+ if char == ']':
252
+ self._match()
253
+ self._update_stack()
254
+ stack_item, prev_state = self._stack.pop()
255
+ self._result = self._vector_maker(tuple(prev_state))
256
+ self._update_stack()
257
+ continue
258
+
259
+ if char == ')':
260
+ self._match()
261
+ self._update_stack()
262
+ stack_item, prev_state = self._stack.pop()
263
+ self._result = self._list_maker(prev_state)
264
+ self._update_stack()
265
+ continue
266
+
267
+ if char == '[':
268
+ self._match()
269
+ self._update_stack()
270
+ self._stack.append((ListParser._StackItem.VECTOR, []))
271
+ continue
272
+
273
+ if char == '(':
274
+ self._match()
275
+ self._update_stack()
276
+ self._stack.append((ListParser._StackItem.LIST, []))
277
+ continue
278
+
279
+ state_plus_char = self._state + char
280
+ if state_plus_char == '#_':
281
+ self._stack.append((ListParser._StackItem.TAG, char))
282
+ self._result = self._UNDEFINED
283
+ self._state = ''
284
+ continue
285
+
286
+ if state_plus_char.endswith('#{'):
287
+ self._state = self._state[:-1] # Remove the '#'
288
+ self._match()
289
+ self._update_stack()
290
+ self._stack.append((ListParser._StackItem.SET, []))
291
+ self._state = ''
292
+ continue
293
+
294
+ if char == '{':
295
+ self._match()
296
+ self._update_stack()
297
+ self._stack.append((ListParser._StackItem.MAP, [[], []]))
298
+ self._state = ''
299
+ continue
300
+
301
+ self._state += char
302
+ continue
303
+
304
+ elif self._mode == ListParser._ParseMode.STRING: # noqa
305
+ if char == '\\':
306
+ self._stack.append((self._mode, self._state))
307
+ self._mode = ListParser._ParseMode.ESCAPE
308
+ self._state = ''
309
+ continue
310
+
311
+ if char == '"':
312
+ self._mode = ListParser._ParseMode.IDLE
313
+ self._result = self._state
314
+ self._update_stack()
315
+ self._state = ''
316
+ continue
317
+
318
+ self._state += char
319
+
320
+ elif self._mode == ListParser._ParseMode.ESCAPE:
321
+ # TODO what should happen when escaping other char
322
+ escaped_char = self._STRING_ESCAPE_MAP.get(char, char)
323
+ stack_item, prev_state = self._stack.pop()
324
+ self._mode = check.isinstance(stack_item, ListParser._ParseMode)
325
+ self._state = prev_state + escaped_char
326
+
327
+ elif self._mode == ListParser._ParseMode.COMMENT:
328
+ if char == '\n':
329
+ self._mode = ListParser._ParseMode.IDLE
330
+
331
+ else:
332
+ raise RuntimeError(self._mode)
333
+
334
+ if i >= 0:
335
+ self._match()
336
+ self._update_stack()
337
+
338
+ check.state(not self._stack)
339
+
340
+ if self._result is not self._UNDEFINED:
341
+ values.append(self._result)
342
+ return values
343
+
344
+
345
+ #
346
+
347
+
348
+ def parse_list(src: str, **kwargs: ta.Any) -> list[ta.Any]:
349
+ """Parse an edn string and return the corresponding Python object."""
350
+
351
+ parser = ListParser(**kwargs)
352
+ return parser.parse(src)
353
+
354
+
355
+ def parse(src: str, **kwargs: ta.Any) -> ta.Any | None:
356
+ values = parse_list(src, **kwargs)
357
+ if not values:
358
+ return None
359
+ return check.single(values)