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.
Files changed (245) hide show
  1. pixeltable/__init__.py +83 -19
  2. pixeltable/_query.py +1444 -0
  3. pixeltable/_version.py +1 -0
  4. pixeltable/catalog/__init__.py +7 -4
  5. pixeltable/catalog/catalog.py +2394 -119
  6. pixeltable/catalog/column.py +225 -104
  7. pixeltable/catalog/dir.py +38 -9
  8. pixeltable/catalog/globals.py +53 -34
  9. pixeltable/catalog/insertable_table.py +265 -115
  10. pixeltable/catalog/path.py +80 -17
  11. pixeltable/catalog/schema_object.py +28 -43
  12. pixeltable/catalog/table.py +1270 -677
  13. pixeltable/catalog/table_metadata.py +103 -0
  14. pixeltable/catalog/table_version.py +1270 -751
  15. pixeltable/catalog/table_version_handle.py +109 -0
  16. pixeltable/catalog/table_version_path.py +137 -42
  17. pixeltable/catalog/tbl_ops.py +53 -0
  18. pixeltable/catalog/update_status.py +191 -0
  19. pixeltable/catalog/view.py +251 -134
  20. pixeltable/config.py +215 -0
  21. pixeltable/env.py +736 -285
  22. pixeltable/exceptions.py +26 -2
  23. pixeltable/exec/__init__.py +7 -2
  24. pixeltable/exec/aggregation_node.py +39 -21
  25. pixeltable/exec/cache_prefetch_node.py +87 -109
  26. pixeltable/exec/cell_materialization_node.py +268 -0
  27. pixeltable/exec/cell_reconstruction_node.py +168 -0
  28. pixeltable/exec/component_iteration_node.py +25 -28
  29. pixeltable/exec/data_row_batch.py +11 -46
  30. pixeltable/exec/exec_context.py +26 -11
  31. pixeltable/exec/exec_node.py +35 -27
  32. pixeltable/exec/expr_eval/__init__.py +3 -0
  33. pixeltable/exec/expr_eval/evaluators.py +365 -0
  34. pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
  35. pixeltable/exec/expr_eval/globals.py +200 -0
  36. pixeltable/exec/expr_eval/row_buffer.py +74 -0
  37. pixeltable/exec/expr_eval/schedulers.py +413 -0
  38. pixeltable/exec/globals.py +35 -0
  39. pixeltable/exec/in_memory_data_node.py +35 -27
  40. pixeltable/exec/object_store_save_node.py +293 -0
  41. pixeltable/exec/row_update_node.py +44 -29
  42. pixeltable/exec/sql_node.py +414 -115
  43. pixeltable/exprs/__init__.py +8 -5
  44. pixeltable/exprs/arithmetic_expr.py +79 -45
  45. pixeltable/exprs/array_slice.py +5 -5
  46. pixeltable/exprs/column_property_ref.py +40 -26
  47. pixeltable/exprs/column_ref.py +254 -61
  48. pixeltable/exprs/comparison.py +14 -9
  49. pixeltable/exprs/compound_predicate.py +9 -10
  50. pixeltable/exprs/data_row.py +213 -72
  51. pixeltable/exprs/expr.py +270 -104
  52. pixeltable/exprs/expr_dict.py +6 -5
  53. pixeltable/exprs/expr_set.py +20 -11
  54. pixeltable/exprs/function_call.py +383 -284
  55. pixeltable/exprs/globals.py +18 -5
  56. pixeltable/exprs/in_predicate.py +7 -7
  57. pixeltable/exprs/inline_expr.py +37 -37
  58. pixeltable/exprs/is_null.py +8 -4
  59. pixeltable/exprs/json_mapper.py +120 -54
  60. pixeltable/exprs/json_path.py +90 -60
  61. pixeltable/exprs/literal.py +61 -16
  62. pixeltable/exprs/method_ref.py +7 -6
  63. pixeltable/exprs/object_ref.py +19 -8
  64. pixeltable/exprs/row_builder.py +238 -75
  65. pixeltable/exprs/rowid_ref.py +53 -15
  66. pixeltable/exprs/similarity_expr.py +65 -50
  67. pixeltable/exprs/sql_element_cache.py +5 -5
  68. pixeltable/exprs/string_op.py +107 -0
  69. pixeltable/exprs/type_cast.py +25 -13
  70. pixeltable/exprs/variable.py +2 -2
  71. pixeltable/func/__init__.py +9 -5
  72. pixeltable/func/aggregate_function.py +197 -92
  73. pixeltable/func/callable_function.py +119 -35
  74. pixeltable/func/expr_template_function.py +101 -48
  75. pixeltable/func/function.py +375 -62
  76. pixeltable/func/function_registry.py +20 -19
  77. pixeltable/func/globals.py +6 -5
  78. pixeltable/func/mcp.py +74 -0
  79. pixeltable/func/query_template_function.py +151 -35
  80. pixeltable/func/signature.py +178 -49
  81. pixeltable/func/tools.py +164 -0
  82. pixeltable/func/udf.py +176 -53
  83. pixeltable/functions/__init__.py +44 -4
  84. pixeltable/functions/anthropic.py +226 -47
  85. pixeltable/functions/audio.py +148 -11
  86. pixeltable/functions/bedrock.py +137 -0
  87. pixeltable/functions/date.py +188 -0
  88. pixeltable/functions/deepseek.py +113 -0
  89. pixeltable/functions/document.py +81 -0
  90. pixeltable/functions/fal.py +76 -0
  91. pixeltable/functions/fireworks.py +72 -20
  92. pixeltable/functions/gemini.py +249 -0
  93. pixeltable/functions/globals.py +208 -53
  94. pixeltable/functions/groq.py +108 -0
  95. pixeltable/functions/huggingface.py +1088 -95
  96. pixeltable/functions/image.py +155 -84
  97. pixeltable/functions/json.py +8 -11
  98. pixeltable/functions/llama_cpp.py +31 -19
  99. pixeltable/functions/math.py +169 -0
  100. pixeltable/functions/mistralai.py +50 -75
  101. pixeltable/functions/net.py +70 -0
  102. pixeltable/functions/ollama.py +29 -36
  103. pixeltable/functions/openai.py +548 -160
  104. pixeltable/functions/openrouter.py +143 -0
  105. pixeltable/functions/replicate.py +15 -14
  106. pixeltable/functions/reve.py +250 -0
  107. pixeltable/functions/string.py +310 -85
  108. pixeltable/functions/timestamp.py +37 -19
  109. pixeltable/functions/together.py +77 -120
  110. pixeltable/functions/twelvelabs.py +188 -0
  111. pixeltable/functions/util.py +7 -2
  112. pixeltable/functions/uuid.py +30 -0
  113. pixeltable/functions/video.py +1528 -117
  114. pixeltable/functions/vision.py +26 -26
  115. pixeltable/functions/voyageai.py +289 -0
  116. pixeltable/functions/whisper.py +19 -10
  117. pixeltable/functions/whisperx.py +179 -0
  118. pixeltable/functions/yolox.py +112 -0
  119. pixeltable/globals.py +716 -236
  120. pixeltable/index/__init__.py +3 -1
  121. pixeltable/index/base.py +17 -21
  122. pixeltable/index/btree.py +32 -22
  123. pixeltable/index/embedding_index.py +155 -92
  124. pixeltable/io/__init__.py +12 -7
  125. pixeltable/io/datarows.py +140 -0
  126. pixeltable/io/external_store.py +83 -125
  127. pixeltable/io/fiftyone.py +24 -33
  128. pixeltable/io/globals.py +47 -182
  129. pixeltable/io/hf_datasets.py +96 -127
  130. pixeltable/io/label_studio.py +171 -156
  131. pixeltable/io/lancedb.py +3 -0
  132. pixeltable/io/pandas.py +136 -115
  133. pixeltable/io/parquet.py +40 -153
  134. pixeltable/io/table_data_conduit.py +702 -0
  135. pixeltable/io/utils.py +100 -0
  136. pixeltable/iterators/__init__.py +8 -4
  137. pixeltable/iterators/audio.py +207 -0
  138. pixeltable/iterators/base.py +9 -3
  139. pixeltable/iterators/document.py +144 -87
  140. pixeltable/iterators/image.py +17 -38
  141. pixeltable/iterators/string.py +15 -12
  142. pixeltable/iterators/video.py +523 -127
  143. pixeltable/metadata/__init__.py +33 -8
  144. pixeltable/metadata/converters/convert_10.py +2 -3
  145. pixeltable/metadata/converters/convert_13.py +2 -2
  146. pixeltable/metadata/converters/convert_15.py +15 -11
  147. pixeltable/metadata/converters/convert_16.py +4 -5
  148. pixeltable/metadata/converters/convert_17.py +4 -5
  149. pixeltable/metadata/converters/convert_18.py +4 -6
  150. pixeltable/metadata/converters/convert_19.py +6 -9
  151. pixeltable/metadata/converters/convert_20.py +3 -6
  152. pixeltable/metadata/converters/convert_21.py +6 -8
  153. pixeltable/metadata/converters/convert_22.py +3 -2
  154. pixeltable/metadata/converters/convert_23.py +33 -0
  155. pixeltable/metadata/converters/convert_24.py +55 -0
  156. pixeltable/metadata/converters/convert_25.py +19 -0
  157. pixeltable/metadata/converters/convert_26.py +23 -0
  158. pixeltable/metadata/converters/convert_27.py +29 -0
  159. pixeltable/metadata/converters/convert_28.py +13 -0
  160. pixeltable/metadata/converters/convert_29.py +110 -0
  161. pixeltable/metadata/converters/convert_30.py +63 -0
  162. pixeltable/metadata/converters/convert_31.py +11 -0
  163. pixeltable/metadata/converters/convert_32.py +15 -0
  164. pixeltable/metadata/converters/convert_33.py +17 -0
  165. pixeltable/metadata/converters/convert_34.py +21 -0
  166. pixeltable/metadata/converters/convert_35.py +9 -0
  167. pixeltable/metadata/converters/convert_36.py +38 -0
  168. pixeltable/metadata/converters/convert_37.py +15 -0
  169. pixeltable/metadata/converters/convert_38.py +39 -0
  170. pixeltable/metadata/converters/convert_39.py +124 -0
  171. pixeltable/metadata/converters/convert_40.py +73 -0
  172. pixeltable/metadata/converters/convert_41.py +12 -0
  173. pixeltable/metadata/converters/convert_42.py +9 -0
  174. pixeltable/metadata/converters/convert_43.py +44 -0
  175. pixeltable/metadata/converters/util.py +44 -18
  176. pixeltable/metadata/notes.py +21 -0
  177. pixeltable/metadata/schema.py +185 -42
  178. pixeltable/metadata/utils.py +74 -0
  179. pixeltable/mypy/__init__.py +3 -0
  180. pixeltable/mypy/mypy_plugin.py +123 -0
  181. pixeltable/plan.py +616 -225
  182. pixeltable/share/__init__.py +3 -0
  183. pixeltable/share/packager.py +797 -0
  184. pixeltable/share/protocol/__init__.py +33 -0
  185. pixeltable/share/protocol/common.py +165 -0
  186. pixeltable/share/protocol/operation_types.py +33 -0
  187. pixeltable/share/protocol/replica.py +119 -0
  188. pixeltable/share/publish.py +349 -0
  189. pixeltable/store.py +398 -232
  190. pixeltable/type_system.py +730 -267
  191. pixeltable/utils/__init__.py +40 -0
  192. pixeltable/utils/arrow.py +201 -29
  193. pixeltable/utils/av.py +298 -0
  194. pixeltable/utils/azure_store.py +346 -0
  195. pixeltable/utils/coco.py +26 -27
  196. pixeltable/utils/code.py +4 -4
  197. pixeltable/utils/console_output.py +46 -0
  198. pixeltable/utils/coroutine.py +24 -0
  199. pixeltable/utils/dbms.py +92 -0
  200. pixeltable/utils/description_helper.py +11 -12
  201. pixeltable/utils/documents.py +60 -61
  202. pixeltable/utils/exception_handler.py +36 -0
  203. pixeltable/utils/filecache.py +38 -22
  204. pixeltable/utils/formatter.py +88 -51
  205. pixeltable/utils/gcs_store.py +295 -0
  206. pixeltable/utils/http.py +133 -0
  207. pixeltable/utils/http_server.py +14 -13
  208. pixeltable/utils/iceberg.py +13 -0
  209. pixeltable/utils/image.py +17 -0
  210. pixeltable/utils/lancedb.py +90 -0
  211. pixeltable/utils/local_store.py +322 -0
  212. pixeltable/utils/misc.py +5 -0
  213. pixeltable/utils/object_stores.py +573 -0
  214. pixeltable/utils/pydantic.py +60 -0
  215. pixeltable/utils/pytorch.py +20 -20
  216. pixeltable/utils/s3_store.py +527 -0
  217. pixeltable/utils/sql.py +32 -5
  218. pixeltable/utils/system.py +30 -0
  219. pixeltable/utils/transactional_directory.py +4 -3
  220. pixeltable-0.5.7.dist-info/METADATA +579 -0
  221. pixeltable-0.5.7.dist-info/RECORD +227 -0
  222. {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
  223. pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
  224. pixeltable/__version__.py +0 -3
  225. pixeltable/catalog/named_function.py +0 -36
  226. pixeltable/catalog/path_dict.py +0 -141
  227. pixeltable/dataframe.py +0 -894
  228. pixeltable/exec/expr_eval_node.py +0 -232
  229. pixeltable/ext/__init__.py +0 -14
  230. pixeltable/ext/functions/__init__.py +0 -8
  231. pixeltable/ext/functions/whisperx.py +0 -77
  232. pixeltable/ext/functions/yolox.py +0 -157
  233. pixeltable/tool/create_test_db_dump.py +0 -311
  234. pixeltable/tool/create_test_video.py +0 -81
  235. pixeltable/tool/doc_plugins/griffe.py +0 -50
  236. pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
  237. pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
  238. pixeltable/tool/embed_udf.py +0 -9
  239. pixeltable/tool/mypy_plugin.py +0 -55
  240. pixeltable/utils/media_store.py +0 -76
  241. pixeltable/utils/s3.py +0 -16
  242. pixeltable-0.2.26.dist-info/METADATA +0 -400
  243. pixeltable-0.2.26.dist-info/RECORD +0 -156
  244. pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
  245. {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, Iterator, Optional, TypeVar, Union, overload, Iterable
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 _AnnotatedAlias, Self
15
+ from typing_extensions import Self, _AnnotatedAlias
15
16
 
16
- import pixeltable.catalog as catalog
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
- def __init__(self, parent: Optional[ExprScope]):
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 (DataFrame.show())
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: Optional[int]
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: Optional[int]
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, mapper: Optional['exprs.JsonMapper'] = None) -> None:
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.bind_rel_paths(mapper)
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 default_column_name(self) -> Optional[str]:
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) != type(other):
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) -> Expr:
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: Optional[list[Expr]]) -> Optional[list[Expr]]:
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
- return self
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: Optional[set[catalog.Column]] = None) -> Expr:
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
- e for e in result.subexprs()
213
- if isinstance(e, ColumnRef) and e.col.is_computed and (not e.col.is_stored or e.col in resolve_cols)
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 (len(self.components)):
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: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
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: Optional[Callable[[Expr], bool]] = None,
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, expr_class: Optional[type[T]] = None, filter: Optional[Callable[[Expr], bool]] = None,
272
- traverse_matches: bool = True
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: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
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, expr_list: Iterable[Expr], expr_class: type[T], filter: Optional[Callable[[Expr], bool]] = None,
297
- traverse_matches: bool = True
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, expr_list: Iterable[Expr], expr_class: Optional[type[T]] = None,
303
- filter: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
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
- def _contains(self, cls: Optional[type[Expr]] = None, filter: Optional[Callable[[Expr], bool]] = None) -> bool:
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
- return {ref.col.tbl.id for ref in self.subexprs(ColumnRef)} | {ref.tbl.id for ref in self.subexprs(RowidRef)}
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 set(tbl_id for e in exprs_ for tbl_id in e.tbl_ids())
401
+ return {tbl_id for e in exprs_ for tbl_id in e.tbl_ids()}
329
402
 
330
403
  @classmethod
331
- def get_refd_columns(cls, expr_dict: dict[str, Any]) -> list[catalog.Column]:
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: list[catalog.Column] = []
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.append(ColumnRef.get_column(expr_dict))
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.extend(cls.get_refd_columns(component_dict))
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) -> Optional[Expr]:
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
- if isinstance(o, Expr):
349
- return o
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, list):
354
- from .inline_expr import InlineList
355
- return InlineList(o)
356
- if isinstance(o, dict):
357
- from .inline_expr import InlineDict
358
- return InlineDict(o)
359
- obj_type = ts.ColumnType.infer_literal_type(o)
360
- if obj_type is not None:
361
- from .literal import Literal
362
- return Literal(o, col_type=obj_type)
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
- @abc.abstractmethod
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
- pass
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: list[Expr]) -> None:
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(self, expr_list: list[Expr]) -> list[dict]:
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
- assert False, 'not implemented'
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: Union[ts.ColumnType, type, _AnnotatedAlias]) -> 'exprs.TypeCast':
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(self, fn: Callable, *, col_type: Union[ts.ColumnType, type, _AnnotatedAlias, None] = None) -> 'exprs.FunctionCall':
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: Union[Expr, LiteralPythonTypes]
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) -> 'exprs.ArithmeticExpr':
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: Union[Expr, LiteralPythonTypes]
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: Optional[ts.ColumnType]) -> 'func.Function':
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 len(params) >= 2 and \
704
- second_param.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) and \
705
- second_param.default == inspect.Parameter.empty:
706
- raise excs.Error(
707
- f'Function `{fn.__name__}` has multiple required parameters.'
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