pixeltable 0.2.26__py3-none-any.whl → 0.5.7__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.
- pixeltable/__init__.py +83 -19
- pixeltable/_query.py +1444 -0
- pixeltable/_version.py +1 -0
- pixeltable/catalog/__init__.py +7 -4
- pixeltable/catalog/catalog.py +2394 -119
- pixeltable/catalog/column.py +225 -104
- pixeltable/catalog/dir.py +38 -9
- pixeltable/catalog/globals.py +53 -34
- pixeltable/catalog/insertable_table.py +265 -115
- pixeltable/catalog/path.py +80 -17
- pixeltable/catalog/schema_object.py +28 -43
- pixeltable/catalog/table.py +1270 -677
- pixeltable/catalog/table_metadata.py +103 -0
- pixeltable/catalog/table_version.py +1270 -751
- pixeltable/catalog/table_version_handle.py +109 -0
- pixeltable/catalog/table_version_path.py +137 -42
- pixeltable/catalog/tbl_ops.py +53 -0
- pixeltable/catalog/update_status.py +191 -0
- pixeltable/catalog/view.py +251 -134
- pixeltable/config.py +215 -0
- pixeltable/env.py +736 -285
- pixeltable/exceptions.py +26 -2
- pixeltable/exec/__init__.py +7 -2
- pixeltable/exec/aggregation_node.py +39 -21
- pixeltable/exec/cache_prefetch_node.py +87 -109
- pixeltable/exec/cell_materialization_node.py +268 -0
- pixeltable/exec/cell_reconstruction_node.py +168 -0
- pixeltable/exec/component_iteration_node.py +25 -28
- pixeltable/exec/data_row_batch.py +11 -46
- pixeltable/exec/exec_context.py +26 -11
- pixeltable/exec/exec_node.py +35 -27
- pixeltable/exec/expr_eval/__init__.py +3 -0
- pixeltable/exec/expr_eval/evaluators.py +365 -0
- pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
- pixeltable/exec/expr_eval/globals.py +200 -0
- pixeltable/exec/expr_eval/row_buffer.py +74 -0
- pixeltable/exec/expr_eval/schedulers.py +413 -0
- pixeltable/exec/globals.py +35 -0
- pixeltable/exec/in_memory_data_node.py +35 -27
- pixeltable/exec/object_store_save_node.py +293 -0
- pixeltable/exec/row_update_node.py +44 -29
- pixeltable/exec/sql_node.py +414 -115
- pixeltable/exprs/__init__.py +8 -5
- pixeltable/exprs/arithmetic_expr.py +79 -45
- pixeltable/exprs/array_slice.py +5 -5
- pixeltable/exprs/column_property_ref.py +40 -26
- pixeltable/exprs/column_ref.py +254 -61
- pixeltable/exprs/comparison.py +14 -9
- pixeltable/exprs/compound_predicate.py +9 -10
- pixeltable/exprs/data_row.py +213 -72
- pixeltable/exprs/expr.py +270 -104
- pixeltable/exprs/expr_dict.py +6 -5
- pixeltable/exprs/expr_set.py +20 -11
- pixeltable/exprs/function_call.py +383 -284
- pixeltable/exprs/globals.py +18 -5
- pixeltable/exprs/in_predicate.py +7 -7
- pixeltable/exprs/inline_expr.py +37 -37
- pixeltable/exprs/is_null.py +8 -4
- pixeltable/exprs/json_mapper.py +120 -54
- pixeltable/exprs/json_path.py +90 -60
- pixeltable/exprs/literal.py +61 -16
- pixeltable/exprs/method_ref.py +7 -6
- pixeltable/exprs/object_ref.py +19 -8
- pixeltable/exprs/row_builder.py +238 -75
- pixeltable/exprs/rowid_ref.py +53 -15
- pixeltable/exprs/similarity_expr.py +65 -50
- pixeltable/exprs/sql_element_cache.py +5 -5
- pixeltable/exprs/string_op.py +107 -0
- pixeltable/exprs/type_cast.py +25 -13
- pixeltable/exprs/variable.py +2 -2
- pixeltable/func/__init__.py +9 -5
- pixeltable/func/aggregate_function.py +197 -92
- pixeltable/func/callable_function.py +119 -35
- pixeltable/func/expr_template_function.py +101 -48
- pixeltable/func/function.py +375 -62
- pixeltable/func/function_registry.py +20 -19
- pixeltable/func/globals.py +6 -5
- pixeltable/func/mcp.py +74 -0
- pixeltable/func/query_template_function.py +151 -35
- pixeltable/func/signature.py +178 -49
- pixeltable/func/tools.py +164 -0
- pixeltable/func/udf.py +176 -53
- pixeltable/functions/__init__.py +44 -4
- pixeltable/functions/anthropic.py +226 -47
- pixeltable/functions/audio.py +148 -11
- pixeltable/functions/bedrock.py +137 -0
- pixeltable/functions/date.py +188 -0
- pixeltable/functions/deepseek.py +113 -0
- pixeltable/functions/document.py +81 -0
- pixeltable/functions/fal.py +76 -0
- pixeltable/functions/fireworks.py +72 -20
- pixeltable/functions/gemini.py +249 -0
- pixeltable/functions/globals.py +208 -53
- pixeltable/functions/groq.py +108 -0
- pixeltable/functions/huggingface.py +1088 -95
- pixeltable/functions/image.py +155 -84
- pixeltable/functions/json.py +8 -11
- pixeltable/functions/llama_cpp.py +31 -19
- pixeltable/functions/math.py +169 -0
- pixeltable/functions/mistralai.py +50 -75
- pixeltable/functions/net.py +70 -0
- pixeltable/functions/ollama.py +29 -36
- pixeltable/functions/openai.py +548 -160
- pixeltable/functions/openrouter.py +143 -0
- pixeltable/functions/replicate.py +15 -14
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/string.py +310 -85
- pixeltable/functions/timestamp.py +37 -19
- pixeltable/functions/together.py +77 -120
- pixeltable/functions/twelvelabs.py +188 -0
- pixeltable/functions/util.py +7 -2
- pixeltable/functions/uuid.py +30 -0
- pixeltable/functions/video.py +1528 -117
- pixeltable/functions/vision.py +26 -26
- pixeltable/functions/voyageai.py +289 -0
- pixeltable/functions/whisper.py +19 -10
- pixeltable/functions/whisperx.py +179 -0
- pixeltable/functions/yolox.py +112 -0
- pixeltable/globals.py +716 -236
- pixeltable/index/__init__.py +3 -1
- pixeltable/index/base.py +17 -21
- pixeltable/index/btree.py +32 -22
- pixeltable/index/embedding_index.py +155 -92
- pixeltable/io/__init__.py +12 -7
- pixeltable/io/datarows.py +140 -0
- pixeltable/io/external_store.py +83 -125
- pixeltable/io/fiftyone.py +24 -33
- pixeltable/io/globals.py +47 -182
- pixeltable/io/hf_datasets.py +96 -127
- pixeltable/io/label_studio.py +171 -156
- pixeltable/io/lancedb.py +3 -0
- pixeltable/io/pandas.py +136 -115
- pixeltable/io/parquet.py +40 -153
- pixeltable/io/table_data_conduit.py +702 -0
- pixeltable/io/utils.py +100 -0
- pixeltable/iterators/__init__.py +8 -4
- pixeltable/iterators/audio.py +207 -0
- pixeltable/iterators/base.py +9 -3
- pixeltable/iterators/document.py +144 -87
- pixeltable/iterators/image.py +17 -38
- pixeltable/iterators/string.py +15 -12
- pixeltable/iterators/video.py +523 -127
- pixeltable/metadata/__init__.py +33 -8
- pixeltable/metadata/converters/convert_10.py +2 -3
- pixeltable/metadata/converters/convert_13.py +2 -2
- pixeltable/metadata/converters/convert_15.py +15 -11
- pixeltable/metadata/converters/convert_16.py +4 -5
- pixeltable/metadata/converters/convert_17.py +4 -5
- pixeltable/metadata/converters/convert_18.py +4 -6
- pixeltable/metadata/converters/convert_19.py +6 -9
- pixeltable/metadata/converters/convert_20.py +3 -6
- pixeltable/metadata/converters/convert_21.py +6 -8
- pixeltable/metadata/converters/convert_22.py +3 -2
- pixeltable/metadata/converters/convert_23.py +33 -0
- pixeltable/metadata/converters/convert_24.py +55 -0
- pixeltable/metadata/converters/convert_25.py +19 -0
- pixeltable/metadata/converters/convert_26.py +23 -0
- pixeltable/metadata/converters/convert_27.py +29 -0
- pixeltable/metadata/converters/convert_28.py +13 -0
- pixeltable/metadata/converters/convert_29.py +110 -0
- pixeltable/metadata/converters/convert_30.py +63 -0
- pixeltable/metadata/converters/convert_31.py +11 -0
- pixeltable/metadata/converters/convert_32.py +15 -0
- pixeltable/metadata/converters/convert_33.py +17 -0
- pixeltable/metadata/converters/convert_34.py +21 -0
- pixeltable/metadata/converters/convert_35.py +9 -0
- pixeltable/metadata/converters/convert_36.py +38 -0
- pixeltable/metadata/converters/convert_37.py +15 -0
- pixeltable/metadata/converters/convert_38.py +39 -0
- pixeltable/metadata/converters/convert_39.py +124 -0
- pixeltable/metadata/converters/convert_40.py +73 -0
- pixeltable/metadata/converters/convert_41.py +12 -0
- pixeltable/metadata/converters/convert_42.py +9 -0
- pixeltable/metadata/converters/convert_43.py +44 -0
- pixeltable/metadata/converters/util.py +44 -18
- pixeltable/metadata/notes.py +21 -0
- pixeltable/metadata/schema.py +185 -42
- pixeltable/metadata/utils.py +74 -0
- pixeltable/mypy/__init__.py +3 -0
- pixeltable/mypy/mypy_plugin.py +123 -0
- pixeltable/plan.py +616 -225
- pixeltable/share/__init__.py +3 -0
- pixeltable/share/packager.py +797 -0
- pixeltable/share/protocol/__init__.py +33 -0
- pixeltable/share/protocol/common.py +165 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +119 -0
- pixeltable/share/publish.py +349 -0
- pixeltable/store.py +398 -232
- pixeltable/type_system.py +730 -267
- pixeltable/utils/__init__.py +40 -0
- pixeltable/utils/arrow.py +201 -29
- pixeltable/utils/av.py +298 -0
- pixeltable/utils/azure_store.py +346 -0
- pixeltable/utils/coco.py +26 -27
- pixeltable/utils/code.py +4 -4
- pixeltable/utils/console_output.py +46 -0
- pixeltable/utils/coroutine.py +24 -0
- pixeltable/utils/dbms.py +92 -0
- pixeltable/utils/description_helper.py +11 -12
- pixeltable/utils/documents.py +60 -61
- pixeltable/utils/exception_handler.py +36 -0
- pixeltable/utils/filecache.py +38 -22
- pixeltable/utils/formatter.py +88 -51
- pixeltable/utils/gcs_store.py +295 -0
- pixeltable/utils/http.py +133 -0
- pixeltable/utils/http_server.py +14 -13
- pixeltable/utils/iceberg.py +13 -0
- pixeltable/utils/image.py +17 -0
- pixeltable/utils/lancedb.py +90 -0
- pixeltable/utils/local_store.py +322 -0
- pixeltable/utils/misc.py +5 -0
- pixeltable/utils/object_stores.py +573 -0
- pixeltable/utils/pydantic.py +60 -0
- pixeltable/utils/pytorch.py +20 -20
- pixeltable/utils/s3_store.py +527 -0
- pixeltable/utils/sql.py +32 -5
- pixeltable/utils/system.py +30 -0
- pixeltable/utils/transactional_directory.py +4 -3
- pixeltable-0.5.7.dist-info/METADATA +579 -0
- pixeltable-0.5.7.dist-info/RECORD +227 -0
- {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
- pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
- pixeltable/__version__.py +0 -3
- pixeltable/catalog/named_function.py +0 -36
- pixeltable/catalog/path_dict.py +0 -141
- pixeltable/dataframe.py +0 -894
- pixeltable/exec/expr_eval_node.py +0 -232
- pixeltable/ext/__init__.py +0 -14
- pixeltable/ext/functions/__init__.py +0 -8
- pixeltable/ext/functions/whisperx.py +0 -77
- pixeltable/ext/functions/yolox.py +0 -157
- pixeltable/tool/create_test_db_dump.py +0 -311
- pixeltable/tool/create_test_video.py +0 -81
- pixeltable/tool/doc_plugins/griffe.py +0 -50
- pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
- pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
- pixeltable/tool/embed_udf.py +0 -9
- pixeltable/tool/mypy_plugin.py +0 -55
- pixeltable/utils/media_store.py +0 -76
- pixeltable/utils/s3.py +0 -16
- pixeltable-0.2.26.dist-info/METADATA +0 -400
- pixeltable-0.2.26.dist-info/RECORD +0 -156
- pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
- {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
pixeltable/exprs/expr.py
CHANGED
|
@@ -7,29 +7,29 @@ import inspect
|
|
|
7
7
|
import json
|
|
8
8
|
import sys
|
|
9
9
|
import typing
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, TypeVar, overload
|
|
11
11
|
from uuid import UUID
|
|
12
12
|
|
|
13
|
+
import numpy as np
|
|
13
14
|
import sqlalchemy as sql
|
|
14
|
-
from typing_extensions import
|
|
15
|
+
from typing_extensions import Self, _AnnotatedAlias
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
-
import pixeltable.exceptions as excs
|
|
18
|
-
import pixeltable.func as func
|
|
19
|
-
import pixeltable.type_system as ts
|
|
17
|
+
from pixeltable import catalog, exceptions as excs, func, type_system as ts
|
|
20
18
|
|
|
21
19
|
from .data_row import DataRow
|
|
22
|
-
from .globals import ArithmeticOperator, ComparisonOperator, LiteralPythonTypes, LogicalOperator
|
|
20
|
+
from .globals import ArithmeticOperator, ComparisonOperator, LiteralPythonTypes, LogicalOperator, StringOperator
|
|
23
21
|
|
|
24
22
|
if TYPE_CHECKING:
|
|
25
23
|
from pixeltable import exprs
|
|
26
24
|
|
|
25
|
+
|
|
27
26
|
class ExprScope:
|
|
28
27
|
"""
|
|
29
28
|
Representation of the scope in which an Expr needs to be evaluated. Used to determine nesting of scopes.
|
|
30
29
|
parent is None: outermost scope
|
|
31
30
|
"""
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
def __init__(self, parent: ExprScope | None):
|
|
33
33
|
self.parent = parent
|
|
34
34
|
|
|
35
35
|
def is_contained_in(self, other: ExprScope) -> bool:
|
|
@@ -47,7 +47,7 @@ class Expr(abc.ABC):
|
|
|
47
47
|
"""
|
|
48
48
|
Rules for using state in subclasses:
|
|
49
49
|
- all state except for components and slot_idx is shared between copies of an Expr
|
|
50
|
-
- slot_idx is set during analysis (
|
|
50
|
+
- slot_idx is set during analysis (Query.show())
|
|
51
51
|
- during eval(), components can only be accessed via self.components; any Exprs outside of that won't
|
|
52
52
|
have slot_idx set
|
|
53
53
|
"""
|
|
@@ -61,13 +61,15 @@ class Expr(abc.ABC):
|
|
|
61
61
|
# - set by the subclass's __init__()
|
|
62
62
|
# - produced by _create_id()
|
|
63
63
|
# - not expected to survive a serialize()/deserialize() roundtrip
|
|
64
|
-
id:
|
|
64
|
+
id: int | None
|
|
65
65
|
|
|
66
66
|
# index of the expr's value in the data row:
|
|
67
67
|
# - set for all materialized exprs
|
|
68
68
|
# - None: not executable
|
|
69
69
|
# - not set for subexprs that don't need to be materialized because the parent can be materialized via SQL
|
|
70
|
-
slot_idx:
|
|
70
|
+
slot_idx: int | None
|
|
71
|
+
|
|
72
|
+
T = TypeVar('T', bound='Expr')
|
|
71
73
|
|
|
72
74
|
def __init__(self, col_type: ts.ColumnType):
|
|
73
75
|
self.col_type = col_type
|
|
@@ -90,16 +92,33 @@ class Expr(abc.ABC):
|
|
|
90
92
|
result = c_scope
|
|
91
93
|
return result
|
|
92
94
|
|
|
93
|
-
def bind_rel_paths(self
|
|
95
|
+
def bind_rel_paths(self) -> None:
|
|
94
96
|
"""
|
|
95
97
|
Binds relative JsonPaths to mapper.
|
|
96
98
|
This needs to be done in a separate phase after __init__(), because RelativeJsonPath()(-1) cannot be resolved
|
|
97
99
|
by the immediately containing JsonMapper during initialization.
|
|
98
100
|
"""
|
|
101
|
+
self._bind_rel_paths()
|
|
102
|
+
has_rel_path = self._has_relative_path()
|
|
103
|
+
assert not has_rel_path, self._expr_tree()
|
|
104
|
+
assert not self._has_relative_path(), self._expr_tree()
|
|
105
|
+
|
|
106
|
+
def _bind_rel_paths(self, mapper: 'exprs.JsonMapperDispatch' | None = None) -> None:
|
|
99
107
|
for c in self.components:
|
|
100
|
-
c.
|
|
108
|
+
c._bind_rel_paths(mapper)
|
|
109
|
+
|
|
110
|
+
def _expr_tree(self) -> str:
|
|
111
|
+
"""Returns a string representation of this expression as a multi-line tree. Useful for debugging."""
|
|
112
|
+
buf: list[str] = []
|
|
113
|
+
self._expr_tree_r(0, buf)
|
|
114
|
+
return '\n'.join(buf)
|
|
101
115
|
|
|
102
|
-
def
|
|
116
|
+
def _expr_tree_r(self, indent: int, buf: list[str]) -> None:
|
|
117
|
+
buf.append(f'{" " * indent}{type(self).__name__}: {self}'.replace('\n', '\\n'))
|
|
118
|
+
for c in self.components:
|
|
119
|
+
c._expr_tree_r(indent + 2, buf)
|
|
120
|
+
|
|
121
|
+
def default_column_name(self) -> str | None:
|
|
103
122
|
"""
|
|
104
123
|
Returns:
|
|
105
124
|
None if this expression lacks a default name,
|
|
@@ -107,11 +126,29 @@ class Expr(abc.ABC):
|
|
|
107
126
|
"""
|
|
108
127
|
return None
|
|
109
128
|
|
|
129
|
+
@property
|
|
130
|
+
def validation_error(self) -> str | None:
|
|
131
|
+
"""
|
|
132
|
+
Subclasses can override this to indicate that validation has failed after a catalog load.
|
|
133
|
+
|
|
134
|
+
If an Expr (or any of its transitive components) is invalid, then it cannot be evaluated, but its metadata
|
|
135
|
+
will still be preserved in the catalog (so that the user can take appropriate corrective action).
|
|
136
|
+
"""
|
|
137
|
+
for c in self.components:
|
|
138
|
+
error = c.validation_error
|
|
139
|
+
if error is not None:
|
|
140
|
+
return error
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def is_valid(self) -> bool:
|
|
145
|
+
return self.validation_error is None
|
|
146
|
+
|
|
110
147
|
def equals(self, other: Expr) -> bool:
|
|
111
148
|
"""
|
|
112
149
|
Subclass-specific comparison. Implemented as a function because __eq__() is needed to construct Comparisons.
|
|
113
150
|
"""
|
|
114
|
-
if type(self)
|
|
151
|
+
if type(self) is not type(other):
|
|
115
152
|
return False
|
|
116
153
|
if len(self.components) != len(other.components):
|
|
117
154
|
return False
|
|
@@ -137,6 +174,9 @@ class Expr(abc.ABC):
|
|
|
137
174
|
for attr, value in self._id_attrs():
|
|
138
175
|
hasher.update(attr.encode('utf-8'))
|
|
139
176
|
hasher.update(str(value).encode('utf-8'))
|
|
177
|
+
# Include the col_type of the expression to avoid expressions with identical str() representations
|
|
178
|
+
# but different types being considered the same expression, e.g. str(int(4)) == "4"
|
|
179
|
+
hasher.update(repr(self.col_type).encode('utf-8'))
|
|
140
180
|
for expr in self.components:
|
|
141
181
|
hasher.update(str(expr.id).encode('utf-8'))
|
|
142
182
|
# truncate to machine's word size
|
|
@@ -150,12 +190,9 @@ class Expr(abc.ABC):
|
|
|
150
190
|
def list_equals(cls, a: list[Expr], b: list[Expr]) -> bool:
|
|
151
191
|
if len(a) != len(b):
|
|
152
192
|
return False
|
|
153
|
-
for i in range(len(a))
|
|
154
|
-
if not a[i].equals(b[i]):
|
|
155
|
-
return False
|
|
156
|
-
return True
|
|
193
|
+
return all(a[i].equals(b[i]) for i in range(len(a)))
|
|
157
194
|
|
|
158
|
-
def copy(self) ->
|
|
195
|
+
def copy(self: T) -> T:
|
|
159
196
|
"""
|
|
160
197
|
Creates a copy that can be evaluated separately: it doesn't share any eval context (slot_idx)
|
|
161
198
|
but shares everything else (catalog objects, etc.)
|
|
@@ -168,12 +205,12 @@ class Expr(abc.ABC):
|
|
|
168
205
|
return result
|
|
169
206
|
|
|
170
207
|
@classmethod
|
|
171
|
-
def copy_list(cls, expr_list:
|
|
208
|
+
def copy_list(cls, expr_list: list[Expr] | None) -> list[Expr] | None:
|
|
172
209
|
if expr_list is None:
|
|
173
210
|
return None
|
|
174
211
|
return [e.copy() for e in expr_list]
|
|
175
212
|
|
|
176
|
-
def __deepcopy__(self, memo=None) -> Expr:
|
|
213
|
+
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Expr:
|
|
177
214
|
# we don't need to create an actual deep copy because all state other than execution state is read-only
|
|
178
215
|
if memo is None:
|
|
179
216
|
memo = {}
|
|
@@ -183,35 +220,46 @@ class Expr(abc.ABC):
|
|
|
183
220
|
|
|
184
221
|
def substitute(self, spec: dict[Expr, Expr]) -> Expr:
|
|
185
222
|
"""
|
|
186
|
-
Replace 'old' with 'new' recursively
|
|
223
|
+
Replace 'old' with 'new' recursively, and return a new version of the expression
|
|
224
|
+
This method must be used in the form: expr = expr.substitute(spec)
|
|
187
225
|
"""
|
|
226
|
+
from .literal import Literal
|
|
227
|
+
|
|
228
|
+
if isinstance(self, Literal):
|
|
229
|
+
return self
|
|
188
230
|
for old, new in spec.items():
|
|
189
231
|
if self.equals(old):
|
|
190
232
|
return new.copy()
|
|
191
233
|
for i in range(len(self.components)):
|
|
192
234
|
self.components[i] = self.components[i].substitute(spec)
|
|
193
|
-
|
|
235
|
+
result = self.maybe_literal()
|
|
236
|
+
result.id = result._create_id()
|
|
237
|
+
return result
|
|
194
238
|
|
|
195
239
|
@classmethod
|
|
196
240
|
def list_substitute(cls, expr_list: list[Expr], spec: dict[Expr, Expr]) -> None:
|
|
197
241
|
for i in range(len(expr_list)):
|
|
198
242
|
expr_list[i] = expr_list[i].substitute(spec)
|
|
199
243
|
|
|
200
|
-
def resolve_computed_cols(self, resolve_cols:
|
|
244
|
+
def resolve_computed_cols(self, resolve_cols: set[catalog.Column] | None = None) -> Expr:
|
|
201
245
|
"""
|
|
202
246
|
Recursively replace ColRefs to unstored computed columns with their value exprs.
|
|
203
247
|
Also replaces references to stored computed columns in resolve_cols.
|
|
204
248
|
"""
|
|
205
249
|
from .column_ref import ColumnRef
|
|
206
250
|
from .expr_set import ExprSet
|
|
251
|
+
|
|
207
252
|
if resolve_cols is None:
|
|
208
253
|
resolve_cols = set()
|
|
209
254
|
result = self
|
|
210
255
|
while True:
|
|
211
|
-
target_col_refs = ExprSet(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
target_col_refs = ExprSet(
|
|
257
|
+
[
|
|
258
|
+
e
|
|
259
|
+
for e in result.subexprs()
|
|
260
|
+
if isinstance(e, ColumnRef) and e.col.is_computed and (not e.col.is_stored or e.col in resolve_cols)
|
|
261
|
+
]
|
|
262
|
+
)
|
|
215
263
|
if len(target_col_refs) == 0:
|
|
216
264
|
return result
|
|
217
265
|
result = result.substitute({ref: ref.col.value_expr for ref in target_col_refs})
|
|
@@ -219,19 +267,24 @@ class Expr(abc.ABC):
|
|
|
219
267
|
def is_bound_by(self, tbls: list[catalog.TableVersionPath]) -> bool:
|
|
220
268
|
"""Returns True if this expr can be evaluated in the context of tbls."""
|
|
221
269
|
from .column_ref import ColumnRef
|
|
270
|
+
|
|
222
271
|
col_refs = self.subexprs(ColumnRef)
|
|
223
|
-
for col_ref in col_refs
|
|
224
|
-
if not any(tbl.has_column(col_ref.col) for tbl in tbls):
|
|
225
|
-
return False
|
|
226
|
-
return True
|
|
272
|
+
return all(any(tbl.has_column(col_ref.col) for tbl in tbls) for col_ref in col_refs)
|
|
227
273
|
|
|
228
274
|
def retarget(self, tbl: catalog.TableVersionPath) -> Self:
|
|
229
275
|
"""Retarget ColumnRefs in this expr to the specific TableVersions in tbl."""
|
|
230
|
-
tbl_versions = {tbl_version.id: tbl_version for tbl_version in tbl.get_tbl_versions()}
|
|
276
|
+
tbl_versions = {tbl_version.id: tbl_version.get() for tbl_version in tbl.get_tbl_versions()}
|
|
231
277
|
return self._retarget(tbl_versions)
|
|
232
278
|
|
|
279
|
+
@classmethod
|
|
280
|
+
def retarget_list(cls, expr_list: list[Expr], tbl: catalog.TableVersionPath) -> None:
|
|
281
|
+
"""Retarget ColumnRefs in expr_list to the specific TableVersions in tbl."""
|
|
282
|
+
tbl_versions = {tbl_version.id: tbl_version.get() for tbl_version in tbl.get_tbl_versions()}
|
|
283
|
+
for i in range(len(expr_list)):
|
|
284
|
+
expr_list[i] = expr_list[i]._retarget(tbl_versions)
|
|
285
|
+
|
|
233
286
|
def _retarget(self, tbl_versions: dict[UUID, catalog.TableVersion]) -> Self:
|
|
234
|
-
for i in range
|
|
287
|
+
for i in range(len(self.components)):
|
|
235
288
|
self.components[i] = self.components[i]._retarget(tbl_versions)
|
|
236
289
|
return self
|
|
237
290
|
|
|
@@ -254,22 +307,21 @@ class Expr(abc.ABC):
|
|
|
254
307
|
# instances of that subclass; and another that returns all subexpressions that match the given filter.
|
|
255
308
|
# In order for type checking to behave correctly on both forms, we provide two overloaded signatures.
|
|
256
309
|
|
|
257
|
-
T = TypeVar('T', bound='Expr')
|
|
258
|
-
|
|
259
310
|
@overload
|
|
260
311
|
def subexprs(
|
|
261
|
-
self, *, filter:
|
|
312
|
+
self, *, filter: Callable[[Expr], bool] | None = None, traverse_matches: bool = True
|
|
262
313
|
) -> Iterator[Expr]: ...
|
|
263
314
|
|
|
264
315
|
@overload
|
|
265
316
|
def subexprs(
|
|
266
|
-
self, expr_class: type[T], filter:
|
|
267
|
-
traverse_matches: bool = True
|
|
317
|
+
self, expr_class: type[T], filter: Callable[[Expr], bool] | None = None, traverse_matches: bool = True
|
|
268
318
|
) -> Iterator[T]: ...
|
|
269
319
|
|
|
270
320
|
def subexprs(
|
|
271
|
-
self,
|
|
272
|
-
|
|
321
|
+
self,
|
|
322
|
+
expr_class: type[T] | None = None,
|
|
323
|
+
filter: Callable[[Expr], bool] | None = None,
|
|
324
|
+
traverse_matches: bool = True,
|
|
273
325
|
) -> Iterator[T]:
|
|
274
326
|
"""
|
|
275
327
|
Iterate over all subexprs, including self.
|
|
@@ -287,26 +339,41 @@ class Expr(abc.ABC):
|
|
|
287
339
|
@overload
|
|
288
340
|
@classmethod
|
|
289
341
|
def list_subexprs(
|
|
290
|
-
cls, expr_list: Iterable[Expr], *, filter:
|
|
342
|
+
cls, expr_list: Iterable[Expr], *, filter: Callable[[Expr], bool] | None = None, traverse_matches: bool = True
|
|
291
343
|
) -> Iterator[Expr]: ...
|
|
292
344
|
|
|
293
345
|
@overload
|
|
294
346
|
@classmethod
|
|
295
347
|
def list_subexprs(
|
|
296
|
-
cls,
|
|
297
|
-
|
|
348
|
+
cls,
|
|
349
|
+
expr_list: Iterable[Expr],
|
|
350
|
+
expr_class: type[T],
|
|
351
|
+
filter: Callable[[Expr], bool] | None = None,
|
|
352
|
+
traverse_matches: bool = True,
|
|
298
353
|
) -> Iterator[T]: ...
|
|
299
354
|
|
|
300
355
|
@classmethod
|
|
301
356
|
def list_subexprs(
|
|
302
|
-
cls,
|
|
303
|
-
|
|
357
|
+
cls,
|
|
358
|
+
expr_list: Iterable[Expr],
|
|
359
|
+
expr_class: type[T] | None = None,
|
|
360
|
+
filter: Callable[[Expr], bool] | None = None,
|
|
361
|
+
traverse_matches: bool = True,
|
|
304
362
|
) -> Iterator[T]:
|
|
305
363
|
"""Produce subexprs for all exprs in list. Can contain duplicates."""
|
|
306
364
|
for e in expr_list:
|
|
307
365
|
yield from e.subexprs(expr_class=expr_class, filter=filter, traverse_matches=traverse_matches)
|
|
308
366
|
|
|
309
|
-
|
|
367
|
+
@classmethod
|
|
368
|
+
def list_contains(
|
|
369
|
+
cls,
|
|
370
|
+
expr_list: Iterable[Expr],
|
|
371
|
+
expr_class: type[Expr] | None = None,
|
|
372
|
+
filter: Callable[[Expr], bool] | None = None,
|
|
373
|
+
) -> bool:
|
|
374
|
+
return any(e._contains(expr_class, filter) for e in expr_list)
|
|
375
|
+
|
|
376
|
+
def _contains(self, cls: type[Expr] | None = None, filter: Callable[[Expr], bool] | None = None) -> bool:
|
|
310
377
|
"""
|
|
311
378
|
Returns True if any subexpr is an instance of cls and/or matches filter.
|
|
312
379
|
"""
|
|
@@ -317,53 +384,97 @@ class Expr(abc.ABC):
|
|
|
317
384
|
except StopIteration:
|
|
318
385
|
return False
|
|
319
386
|
|
|
387
|
+
def _has_relative_path(self) -> bool:
|
|
388
|
+
return any(c._has_relative_path() for c in self.components)
|
|
389
|
+
|
|
320
390
|
def tbl_ids(self) -> set[UUID]:
|
|
321
391
|
"""Returns table ids referenced by this expr."""
|
|
322
392
|
from .column_ref import ColumnRef
|
|
323
393
|
from .rowid_ref import RowidRef
|
|
324
|
-
|
|
394
|
+
|
|
395
|
+
return {ref.col.get_tbl().id for ref in self.subexprs(ColumnRef)} | {
|
|
396
|
+
ref.tbl.id for ref in self.subexprs(RowidRef)
|
|
397
|
+
}
|
|
325
398
|
|
|
326
399
|
@classmethod
|
|
327
400
|
def all_tbl_ids(cls, exprs_: Iterable[Expr]) -> set[UUID]:
|
|
328
|
-
return
|
|
401
|
+
return {tbl_id for e in exprs_ for tbl_id in e.tbl_ids()}
|
|
329
402
|
|
|
330
403
|
@classmethod
|
|
331
|
-
def
|
|
404
|
+
def get_refd_column_ids(cls, expr_dict: dict[str, Any]) -> set[catalog.QColumnId]:
|
|
332
405
|
"""Return Columns referenced by expr_dict."""
|
|
333
|
-
result:
|
|
406
|
+
result: set[catalog.QColumnId] = set()
|
|
334
407
|
assert '_classname' in expr_dict
|
|
335
408
|
from .column_ref import ColumnRef
|
|
409
|
+
|
|
336
410
|
if expr_dict['_classname'] == 'ColumnRef':
|
|
337
|
-
result.
|
|
411
|
+
result.add(ColumnRef.get_column_id(expr_dict))
|
|
338
412
|
if 'components' in expr_dict:
|
|
339
413
|
for component_dict in expr_dict['components']:
|
|
340
|
-
result.
|
|
414
|
+
result.update(cls.get_refd_column_ids(component_dict))
|
|
341
415
|
return result
|
|
342
416
|
|
|
417
|
+
def as_literal(self) -> Expr | None:
|
|
418
|
+
"""
|
|
419
|
+
Return a Literal expression if this expression can be evaluated to a constant value, otherwise return None.
|
|
420
|
+
"""
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def from_array(cls, elements: Iterable) -> Expr | None:
|
|
425
|
+
from .inline_expr import InlineArray
|
|
426
|
+
from .literal import Literal
|
|
427
|
+
|
|
428
|
+
if isinstance(elements, np.ndarray):
|
|
429
|
+
pxttype = ts.ArrayType.from_literal(elements)
|
|
430
|
+
if pxttype is not None:
|
|
431
|
+
return Literal(elements, col_type=pxttype)
|
|
432
|
+
|
|
433
|
+
inline_array = InlineArray(elements)
|
|
434
|
+
return inline_array.maybe_literal()
|
|
435
|
+
|
|
436
|
+
def maybe_literal(self: Expr) -> Expr:
|
|
437
|
+
"""
|
|
438
|
+
Return a Literal if this expression can be evaluated to a constant value, otherwise return the expression.
|
|
439
|
+
"""
|
|
440
|
+
lit_expr = self.as_literal()
|
|
441
|
+
if lit_expr is not None:
|
|
442
|
+
return lit_expr
|
|
443
|
+
else:
|
|
444
|
+
return self
|
|
445
|
+
|
|
343
446
|
@classmethod
|
|
344
|
-
def from_object(cls, o: object) ->
|
|
447
|
+
def from_object(cls, o: object) -> Expr | None:
|
|
345
448
|
"""
|
|
346
449
|
Try to turn a literal object into an Expr.
|
|
347
450
|
"""
|
|
348
|
-
|
|
349
|
-
|
|
451
|
+
from .inline_expr import InlineDict, InlineList
|
|
452
|
+
from .literal import Literal
|
|
453
|
+
|
|
350
454
|
# Try to create a literal. We need to check for InlineList/InlineDict
|
|
351
455
|
# first, to prevent them from inappropriately being interpreted as JsonType
|
|
352
456
|
# literals.
|
|
353
|
-
if isinstance(o,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if isinstance(o, dict):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
457
|
+
if isinstance(o, Literal):
|
|
458
|
+
return o
|
|
459
|
+
|
|
460
|
+
if isinstance(o, (list, tuple, dict, Expr)):
|
|
461
|
+
expr: Expr
|
|
462
|
+
if isinstance(o, (list, tuple)):
|
|
463
|
+
expr = InlineList(o)
|
|
464
|
+
elif isinstance(o, dict):
|
|
465
|
+
expr = InlineDict(o)
|
|
466
|
+
else:
|
|
467
|
+
expr = o
|
|
468
|
+
|
|
469
|
+
return expr.maybe_literal()
|
|
470
|
+
else:
|
|
471
|
+
# convert scalar to a literal
|
|
472
|
+
obj_type = ts.ColumnType.infer_literal_type(o)
|
|
473
|
+
if obj_type is not None:
|
|
474
|
+
return Literal(o, col_type=obj_type)
|
|
363
475
|
return None
|
|
364
476
|
|
|
365
|
-
|
|
366
|
-
def sql_expr(self, sql_elements: 'exprs.SqlElementCache') -> Optional[sql.ColumnElement]:
|
|
477
|
+
def sql_expr(self, sql_elements: 'exprs.SqlElementCache') -> sql.ColumnElement | None:
|
|
367
478
|
"""
|
|
368
479
|
If this expr can be materialized directly in SQL:
|
|
369
480
|
- returns a ColumnElement
|
|
@@ -372,7 +483,7 @@ class Expr(abc.ABC):
|
|
|
372
483
|
- returns None
|
|
373
484
|
- eval() will be called
|
|
374
485
|
"""
|
|
375
|
-
|
|
486
|
+
return None
|
|
376
487
|
|
|
377
488
|
@abc.abstractmethod
|
|
378
489
|
def eval(self, data_row: DataRow, row_builder: 'exprs.RowBuilder') -> None:
|
|
@@ -382,6 +493,18 @@ class Expr(abc.ABC):
|
|
|
382
493
|
"""
|
|
383
494
|
pass
|
|
384
495
|
|
|
496
|
+
def prepare(self) -> None:
|
|
497
|
+
"""
|
|
498
|
+
Create execution state. This is called before the first eval() call.
|
|
499
|
+
"""
|
|
500
|
+
for c in self.components:
|
|
501
|
+
c.prepare()
|
|
502
|
+
|
|
503
|
+
@classmethod
|
|
504
|
+
def prepare_list(cls, expr_list: Iterable[Expr]) -> None:
|
|
505
|
+
for e in expr_list:
|
|
506
|
+
e.prepare()
|
|
507
|
+
|
|
385
508
|
def release(self) -> None:
|
|
386
509
|
"""
|
|
387
510
|
Allow Expr class to tear down execution state. This is called after the last eval() call.
|
|
@@ -390,7 +513,7 @@ class Expr(abc.ABC):
|
|
|
390
513
|
c.release()
|
|
391
514
|
|
|
392
515
|
@classmethod
|
|
393
|
-
def release_list(cls, expr_list:
|
|
516
|
+
def release_list(cls, expr_list: Iterable[Expr]) -> None:
|
|
394
517
|
for e in expr_list:
|
|
395
518
|
e.release()
|
|
396
519
|
|
|
@@ -402,13 +525,10 @@ class Expr(abc.ABC):
|
|
|
402
525
|
Turn Expr object into a dict that can be passed to json.dumps().
|
|
403
526
|
Subclasses override _as_dict().
|
|
404
527
|
"""
|
|
405
|
-
return {
|
|
406
|
-
'_classname': self.__class__.__name__,
|
|
407
|
-
**self._as_dict(),
|
|
408
|
-
}
|
|
528
|
+
return {'_classname': self.__class__.__name__, **self._as_dict()}
|
|
409
529
|
|
|
410
530
|
@classmethod
|
|
411
|
-
def as_dict_list(
|
|
531
|
+
def as_dict_list(cls, expr_list: list[Expr]) -> list[dict]:
|
|
412
532
|
return [e.as_dict() for e in expr_list]
|
|
413
533
|
|
|
414
534
|
def _as_dict(self) -> dict:
|
|
@@ -439,17 +559,19 @@ class Expr(abc.ABC):
|
|
|
439
559
|
|
|
440
560
|
@classmethod
|
|
441
561
|
def _from_dict(cls, d: dict, components: list[Expr]) -> Self:
|
|
442
|
-
|
|
562
|
+
raise AssertionError(f'not implemented: {cls.__name__}')
|
|
443
563
|
|
|
444
564
|
def isin(self, value_set: Any) -> 'exprs.InPredicate':
|
|
445
565
|
from .in_predicate import InPredicate
|
|
566
|
+
|
|
446
567
|
if isinstance(value_set, Expr):
|
|
447
568
|
return InPredicate(self, value_set_expr=value_set)
|
|
448
569
|
else:
|
|
449
570
|
return InPredicate(self, value_set_literal=value_set)
|
|
450
571
|
|
|
451
|
-
def astype(self, new_type:
|
|
572
|
+
def astype(self, new_type: ts.ColumnType | type | _AnnotatedAlias) -> 'exprs.TypeCast':
|
|
452
573
|
from pixeltable.exprs import TypeCast
|
|
574
|
+
|
|
453
575
|
# Interpret the type argument the same way we would if given in a schema
|
|
454
576
|
col_type = ts.ColumnType.normalize_type(new_type, nullable_default=True, allow_builtin_types=False)
|
|
455
577
|
if not self.col_type.nullable:
|
|
@@ -458,7 +580,9 @@ class Expr(abc.ABC):
|
|
|
458
580
|
col_type = col_type.copy(nullable=False)
|
|
459
581
|
return TypeCast(self, col_type)
|
|
460
582
|
|
|
461
|
-
def apply(
|
|
583
|
+
def apply(
|
|
584
|
+
self, fn: Callable, *, col_type: ts.ColumnType | type | _AnnotatedAlias | None = None
|
|
585
|
+
) -> 'exprs.FunctionCall':
|
|
462
586
|
if col_type is not None:
|
|
463
587
|
col_type = ts.ColumnType.normalize_type(col_type)
|
|
464
588
|
function = self._make_applicator_function(fn, col_type)
|
|
@@ -467,10 +591,7 @@ class Expr(abc.ABC):
|
|
|
467
591
|
|
|
468
592
|
def __dir__(self) -> list[str]:
|
|
469
593
|
attrs = ['isin', 'astype', 'apply']
|
|
470
|
-
attrs += [
|
|
471
|
-
f.name
|
|
472
|
-
for f in func.FunctionRegistry.get().get_type_methods(self.col_type.type_enum)
|
|
473
|
-
]
|
|
594
|
+
attrs += [f.name for f in func.FunctionRegistry.get().get_type_methods(self.col_type.type_enum)]
|
|
474
595
|
return attrs
|
|
475
596
|
|
|
476
597
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
@@ -479,9 +600,11 @@ class Expr(abc.ABC):
|
|
|
479
600
|
def __getitem__(self, index: object) -> Expr:
|
|
480
601
|
if self.col_type.is_json_type():
|
|
481
602
|
from .json_path import JsonPath
|
|
603
|
+
|
|
482
604
|
return JsonPath(self)[index]
|
|
483
605
|
if self.col_type.is_array_type():
|
|
484
606
|
from .array_slice import ArraySlice
|
|
607
|
+
|
|
485
608
|
if not isinstance(index, tuple):
|
|
486
609
|
index = (index,)
|
|
487
610
|
if any(not isinstance(i, (int, slice)) for i in index):
|
|
@@ -495,6 +618,7 @@ class Expr(abc.ABC):
|
|
|
495
618
|
"""
|
|
496
619
|
from .json_path import JsonPath
|
|
497
620
|
from .method_ref import MethodRef
|
|
621
|
+
|
|
498
622
|
if self.col_type.is_json_type():
|
|
499
623
|
return JsonPath(self).__getattr__(name)
|
|
500
624
|
else:
|
|
@@ -509,7 +633,8 @@ class Expr(abc.ABC):
|
|
|
509
633
|
|
|
510
634
|
def __bool__(self) -> bool:
|
|
511
635
|
raise TypeError(
|
|
512
|
-
'Pixeltable expressions cannot be used in conjunction with Python boolean operators (and/or/not)'
|
|
636
|
+
f'Pixeltable expressions cannot be used in conjunction with Python boolean operators (and/or/not)\n{self!r}'
|
|
637
|
+
)
|
|
513
638
|
|
|
514
639
|
def __lt__(self, other: object) -> 'exprs.Comparison':
|
|
515
640
|
return self._make_comparison(ComparisonOperator.LT, other)
|
|
@@ -520,6 +645,7 @@ class Expr(abc.ABC):
|
|
|
520
645
|
def __eq__(self, other: object) -> 'exprs.Expr': # type: ignore[override]
|
|
521
646
|
if other is None:
|
|
522
647
|
from .is_null import IsNull
|
|
648
|
+
|
|
523
649
|
return IsNull(self)
|
|
524
650
|
return self._make_comparison(ComparisonOperator.EQ, other)
|
|
525
651
|
|
|
@@ -527,6 +653,7 @@ class Expr(abc.ABC):
|
|
|
527
653
|
if other is None:
|
|
528
654
|
from .compound_predicate import CompoundPredicate
|
|
529
655
|
from .is_null import IsNull
|
|
656
|
+
|
|
530
657
|
return CompoundPredicate(LogicalOperator.NOT, [IsNull(self)])
|
|
531
658
|
return self._make_comparison(ComparisonOperator.NE, other)
|
|
532
659
|
|
|
@@ -538,11 +665,12 @@ class Expr(abc.ABC):
|
|
|
538
665
|
|
|
539
666
|
def _make_comparison(self, op: ComparisonOperator, other: object) -> 'exprs.Comparison':
|
|
540
667
|
"""
|
|
541
|
-
other:
|
|
668
|
+
other: Expr | LiteralPythonTypes
|
|
542
669
|
"""
|
|
543
670
|
# TODO: check for compatibility
|
|
544
671
|
from .comparison import Comparison
|
|
545
672
|
from .literal import Literal
|
|
673
|
+
|
|
546
674
|
if isinstance(other, Expr):
|
|
547
675
|
return Comparison(op, self, other)
|
|
548
676
|
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
@@ -552,13 +680,17 @@ class Expr(abc.ABC):
|
|
|
552
680
|
def __neg__(self) -> 'exprs.ArithmeticExpr':
|
|
553
681
|
return self._make_arithmetic_expr(ArithmeticOperator.MUL, -1)
|
|
554
682
|
|
|
555
|
-
def __add__(self, other: object) ->
|
|
683
|
+
def __add__(self, other: object) -> exprs.ArithmeticExpr | exprs.StringOp:
|
|
684
|
+
if isinstance(self, str) or (isinstance(self, Expr) and self.col_type.is_string_type()):
|
|
685
|
+
return self._make_string_expr(StringOperator.CONCAT, other)
|
|
556
686
|
return self._make_arithmetic_expr(ArithmeticOperator.ADD, other)
|
|
557
687
|
|
|
558
688
|
def __sub__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
559
689
|
return self._make_arithmetic_expr(ArithmeticOperator.SUB, other)
|
|
560
690
|
|
|
561
|
-
def __mul__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
691
|
+
def __mul__(self, other: object) -> 'exprs.ArithmeticExpr' | 'exprs.StringOp':
|
|
692
|
+
if isinstance(self, str) or (isinstance(self, Expr) and self.col_type.is_string_type()):
|
|
693
|
+
return self._make_string_expr(StringOperator.REPEAT, other)
|
|
562
694
|
return self._make_arithmetic_expr(ArithmeticOperator.MUL, other)
|
|
563
695
|
|
|
564
696
|
def __truediv__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
@@ -570,13 +702,17 @@ class Expr(abc.ABC):
|
|
|
570
702
|
def __floordiv__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
571
703
|
return self._make_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
|
|
572
704
|
|
|
573
|
-
def __radd__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
705
|
+
def __radd__(self, other: object) -> 'exprs.ArithmeticExpr' | 'exprs.StringOp':
|
|
706
|
+
if isinstance(other, str) or (isinstance(other, Expr) and other.col_type.is_string_type()):
|
|
707
|
+
return self._rmake_string_expr(StringOperator.CONCAT, other)
|
|
574
708
|
return self._rmake_arithmetic_expr(ArithmeticOperator.ADD, other)
|
|
575
709
|
|
|
576
710
|
def __rsub__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
577
711
|
return self._rmake_arithmetic_expr(ArithmeticOperator.SUB, other)
|
|
578
712
|
|
|
579
|
-
def __rmul__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
713
|
+
def __rmul__(self, other: object) -> 'exprs.ArithmeticExpr' | 'exprs.StringOp':
|
|
714
|
+
if isinstance(other, str) or (isinstance(other, Expr) and other.col_type.is_string_type()):
|
|
715
|
+
return self._rmake_string_expr(StringOperator.REPEAT, other)
|
|
580
716
|
return self._rmake_arithmetic_expr(ArithmeticOperator.MUL, other)
|
|
581
717
|
|
|
582
718
|
def __rtruediv__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
@@ -588,13 +724,40 @@ class Expr(abc.ABC):
|
|
|
588
724
|
def __rfloordiv__(self, other: object) -> 'exprs.ArithmeticExpr':
|
|
589
725
|
return self._rmake_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
|
|
590
726
|
|
|
727
|
+
def _make_string_expr(self, op: StringOperator, other: object) -> 'exprs.StringOp':
|
|
728
|
+
"""
|
|
729
|
+
Make left-handed version of string expression.
|
|
730
|
+
"""
|
|
731
|
+
from .literal import Literal
|
|
732
|
+
from .string_op import StringOp
|
|
733
|
+
|
|
734
|
+
if isinstance(other, Expr):
|
|
735
|
+
return StringOp(op, self, other)
|
|
736
|
+
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
737
|
+
return StringOp(op, self, Literal(other))
|
|
738
|
+
raise TypeError(f'Other must be Expr or literal: {type(other)}')
|
|
739
|
+
|
|
740
|
+
def _rmake_string_expr(self, op: StringOperator, other: object) -> 'exprs.StringOp':
|
|
741
|
+
"""
|
|
742
|
+
Right-handed version of _make_string_expr. other must be a literal; if it were an Expr,
|
|
743
|
+
the operation would have already been evaluated in its left-handed form.
|
|
744
|
+
"""
|
|
745
|
+
from .literal import Literal
|
|
746
|
+
from .string_op import StringOp
|
|
747
|
+
|
|
748
|
+
assert not isinstance(other, Expr) # Else the left-handed form would have evaluated first
|
|
749
|
+
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
750
|
+
return StringOp(op, Literal(other), self)
|
|
751
|
+
raise TypeError(f'Other must be Expr or literal: {type(other)}')
|
|
752
|
+
|
|
591
753
|
def _make_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'exprs.ArithmeticExpr':
|
|
592
754
|
"""
|
|
593
|
-
other:
|
|
755
|
+
other: Expr | LiteralPythonTypes
|
|
594
756
|
"""
|
|
595
757
|
# TODO: check for compatibility
|
|
596
758
|
from .arithmetic_expr import ArithmeticExpr
|
|
597
759
|
from .literal import Literal
|
|
760
|
+
|
|
598
761
|
if isinstance(other, Expr):
|
|
599
762
|
return ArithmeticExpr(op, self, other)
|
|
600
763
|
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
@@ -609,6 +772,7 @@ class Expr(abc.ABC):
|
|
|
609
772
|
# TODO: check for compatibility
|
|
610
773
|
from .arithmetic_expr import ArithmeticExpr
|
|
611
774
|
from .literal import Literal
|
|
775
|
+
|
|
612
776
|
assert not isinstance(other, Expr) # Else the left-handed form would have evaluated first
|
|
613
777
|
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
614
778
|
return ArithmeticExpr(op, Literal(other), self)
|
|
@@ -620,6 +784,7 @@ class Expr(abc.ABC):
|
|
|
620
784
|
if not other.col_type.is_bool_type():
|
|
621
785
|
raise TypeError(f'Other needs to be an expression that returns a boolean: {other.col_type}')
|
|
622
786
|
from .compound_predicate import CompoundPredicate
|
|
787
|
+
|
|
623
788
|
return CompoundPredicate(LogicalOperator.AND, [self, other])
|
|
624
789
|
|
|
625
790
|
def __or__(self, other: object) -> Expr:
|
|
@@ -628,14 +793,15 @@ class Expr(abc.ABC):
|
|
|
628
793
|
if not other.col_type.is_bool_type():
|
|
629
794
|
raise TypeError(f'Other needs to be an expression that returns a boolean: {other.col_type}')
|
|
630
795
|
from .compound_predicate import CompoundPredicate
|
|
796
|
+
|
|
631
797
|
return CompoundPredicate(LogicalOperator.OR, [self, other])
|
|
632
798
|
|
|
633
799
|
def __invert__(self) -> Expr:
|
|
634
800
|
from .compound_predicate import CompoundPredicate
|
|
801
|
+
|
|
635
802
|
return CompoundPredicate(LogicalOperator.NOT, [self])
|
|
636
803
|
|
|
637
|
-
def split_conjuncts(
|
|
638
|
-
self, condition: Callable[[Expr], bool]) -> tuple[list[Expr], Optional[Expr]]:
|
|
804
|
+
def split_conjuncts(self, condition: Callable[[Expr], bool]) -> tuple[list[Expr], Expr | None]:
|
|
639
805
|
"""
|
|
640
806
|
Returns clauses of a conjunction that meet condition in the first element.
|
|
641
807
|
The second element contains remaining clauses, rolled into a conjunction.
|
|
@@ -646,7 +812,7 @@ class Expr(abc.ABC):
|
|
|
646
812
|
else:
|
|
647
813
|
return [], self
|
|
648
814
|
|
|
649
|
-
def _make_applicator_function(self, fn: Callable, col_type:
|
|
815
|
+
def _make_applicator_function(self, fn: Callable, col_type: ts.ColumnType | None) -> 'func.Function':
|
|
650
816
|
"""
|
|
651
817
|
Creates a unary pixeltable `Function` that encapsulates a python `Callable`. The result type of
|
|
652
818
|
the new `Function` is given by `col_type`, and its parameter type will be `self.col_type`.
|
|
@@ -675,7 +841,8 @@ class Expr(abc.ABC):
|
|
|
675
841
|
if fn_type is None:
|
|
676
842
|
raise excs.Error(
|
|
677
843
|
f'Column type of `{fn.__name__}` cannot be inferred. '
|
|
678
|
-
f'Use `.apply({fn.__name__}, col_type=...)` to specify.'
|
|
844
|
+
f'Use `.apply({fn.__name__}, col_type=...)` to specify.'
|
|
845
|
+
)
|
|
679
846
|
|
|
680
847
|
# TODO(aaron-siegel) Currently we assume that `fn` has exactly one required parameter
|
|
681
848
|
# and all optional parameters take their default values. Should we provide a more
|
|
@@ -695,17 +862,15 @@ class Expr(abc.ABC):
|
|
|
695
862
|
second_param = next(params_iter) if len(params) >= 2 else None
|
|
696
863
|
# Check that fn has at least one positional parameter
|
|
697
864
|
if len(params) == 0 or first_param.kind in (inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.VAR_KEYWORD):
|
|
698
|
-
raise excs.Error(
|
|
699
|
-
f'Function `{fn.__name__}` has no positional parameters.'
|
|
700
|
-
)
|
|
865
|
+
raise excs.Error(f'Function `{fn.__name__}` has no positional parameters.')
|
|
701
866
|
# Check that fn has at most one required parameter, i.e., its second parameter
|
|
702
867
|
# has no default and is not a varargs
|
|
703
|
-
if
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
)
|
|
868
|
+
if (
|
|
869
|
+
len(params) >= 2
|
|
870
|
+
and second_param.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
|
871
|
+
and second_param.default is inspect.Parameter.empty
|
|
872
|
+
):
|
|
873
|
+
raise excs.Error(f'Function `{fn.__name__}` has multiple required parameters.')
|
|
709
874
|
except ValueError:
|
|
710
875
|
# inspect.signature(fn) will raise a `ValueError` if `fn` is a builtin; I don't
|
|
711
876
|
# know of any way to get the signature of a builtin, nor to check for this in
|
|
@@ -719,7 +884,8 @@ class Expr(abc.ABC):
|
|
|
719
884
|
# We also set the display_name explicitly, so that the `FunctionCall` gets the
|
|
720
885
|
# name of `decorated_fn`, not the lambda.
|
|
721
886
|
return func.make_function(
|
|
722
|
-
decorated_fn=lambda x: fn(x), return_type=fn_type, param_types=[self.col_type], function_name=fn.__name__
|
|
887
|
+
decorated_fn=lambda x: fn(x), return_type=fn_type, param_types=[self.col_type], function_name=fn.__name__
|
|
888
|
+
)
|
|
723
889
|
|
|
724
890
|
|
|
725
891
|
# A dictionary of result types of various stdlib functions that are
|