PySpark-Column-Selectors 0.1.3__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.
@@ -0,0 +1,1564 @@
1
+ import functools
2
+ import operator
3
+ import re
4
+ from collections.abc import Callable, Sequence
5
+ from typing import Any, Union
6
+
7
+ import pyspark.sql.types as T
8
+ from pyspark.errors import PySparkNotImplementedError, PySparkTypeError, PySparkValueError
9
+ from pyspark.sql import DataFrame
10
+ from pyspark.sql import functions as F
11
+ from pyspark.sql.column import Column
12
+
13
+ from .utils import (
14
+ _get_column_method,
15
+ _quoted_col,
16
+ )
17
+
18
+ """ enable support for many different types of pyspark environments"""
19
+ # if your environment isn't supported then you can add it to this section.
20
+ # Spark supports multiple DataFrame implementations. Keep every available runtime
21
+ # class in the patch set: public/classic DataFrame and Spark Connect DataFrame.
22
+ # using try except as to allow passthrough if environment doesn't support these.
23
+ try:
24
+ from pyspark.sql.classic.dataframe import DataFrame as _ClassicDataFrame
25
+ except ImportError:
26
+ _ClassicDataFrame = None
27
+
28
+ try:
29
+ from pyspark.sql.connect.dataframe import DataFrame as _ConnectDataFrame
30
+ except ImportError:
31
+ _ConnectDataFrame = None
32
+
33
+ _DATAFRAME_CLASSES = tuple(
34
+ dict.fromkeys(cls for cls in (DataFrame, _ClassicDataFrame, _ConnectDataFrame) if cls is not None)
35
+ )
36
+
37
+ try:
38
+ from pyspark.sql.connect.column import Column as _ConnectColumn
39
+ except ImportError:
40
+ _ConnectColumn = None
41
+
42
+ _COLUMN_CLASSES = tuple(dict.fromkeys(cls for cls in (Column, _ConnectColumn) if cls is not None))
43
+
44
+ # --------------------------------------------------------------------------------
45
+ # Make column selectors allow column level transformations
46
+ # including support for arithmetic, comparison, and spark functions
47
+ # --------------------------------------------------------------------------------
48
+
49
+
50
+ class SelectorcolumnOperations:
51
+ """Mixin providing per-column transform chaining for selector objects.
52
+
53
+ Every selector class (`DTypeSelector`, `IndexSelector`, `RegexSelector`, and
54
+ any future selector) inherits from this class (via `BaseSelector`) so that
55
+ arithmetic operators (``+``, ``-``, ``*``, ...), comparison operators
56
+ (``==``, ``>``, ...), and any function available on
57
+ `pyspark.sql.functions` (``.upper()``, ``.sum()``, ``.cast(...)``, ...) can
58
+ be chained directly onto a selector, e.g. ``by_dtype([T.IntegerType]) + 1`` or
59
+ ``by_dtype([T.StringType]).upper()``. Each chained call does not mutate the
60
+ selector in place -- it returns a new, immutable copy with the extra
61
+ transform appended, so the original selector is always safe to reuse.
62
+
63
+ Parameters
64
+ ----------
65
+ transforms : list of callable, optional
66
+ A list of one-argument functions, each accepting and returning a
67
+ `pyspark.sql.Column`, representing the transform pipeline already
68
+ chained onto this selector. Defaults to an empty list when not given.
69
+
70
+ Attributes
71
+ ----------
72
+ transforms : list of callable
73
+ The transform pipeline described above. Applied, in order, to every
74
+ matched column's `pyspark.sql.Column` expression by `resolve_columns`.
75
+
76
+ Examples
77
+ --------
78
+ >>> by_dtype([T.IntegerType]) + 1
79
+ <DTypeSelector ...>
80
+ >>> by_dtype([T.StringType]).upper()
81
+ <DTypeSelector ...>
82
+ """
83
+
84
+ def __init__(self, transforms: list[Callable[[Column], Column]] | None = None) -> None:
85
+ """Initialize the transform pipeline.
86
+
87
+ Parameters
88
+ ----------
89
+ transforms : list of callable, optional
90
+ Existing transform pipeline to start from. Defaults to an empty
91
+ list when not given.
92
+ """
93
+ self.transforms = transforms or []
94
+
95
+ # tracks a pending rename (`.prefix()`/`.suffix()`/`.map_alias()`) as
96
+ # pure name metadata -- a `str -> str` function applied to the
97
+ # originally-matched column name -- kept completely separate from
98
+ # `self.transforms` (the *value* pipeline). This is what
99
+ # `_resolve_selector_exprs` uses to compute each matched column's
100
+ # final output name; it deliberately never touches the `Column`
101
+ # expression itself mid-chain, so it can't be corrupted by whatever
102
+ # value transform text (`.cast(...)`, arithmetic, ...) precedes it,
103
+ # and it never appears in `self.transforms`, so `~`/`&`/`|`'s
104
+ # "does this selector already have a transform chained onto it?"
105
+ # filter-condition check isn't tripped by a rename alone -- see
106
+ # "prefix / suffix and map name fx's" section.
107
+ self._name_transform: Callable[[str], str] | None = None
108
+
109
+ # --------------------------------------------------
110
+ # utility used by every operator/function
111
+ # --------------------------------------------------
112
+
113
+ def _copy(self, transform: Callable[[Column], Column]) -> "SelectorcolumnOperations":
114
+ """Return an immutable copy of this selector with one more transform appended.
115
+
116
+ Parameters
117
+ ----------
118
+ transform : callable
119
+ A one-argument function accepting and returning a
120
+ `pyspark.sql.Column`, appended to the end of the copy's transform
121
+ pipeline.
122
+
123
+ Returns
124
+ -------
125
+ SelectorcolumnOperations
126
+ A new instance of the same concrete class, with all of this
127
+ instance's attributes copied over and `transform` appended to
128
+ `transforms`. `self` itself is left unmodified.
129
+ """
130
+ # allows to immutably create new versions of the class depending on the transformation that occurs.
131
+ new = self.__class__.__new__(self.__class__)
132
+
133
+ new.__dict__.update(self.__dict__)
134
+
135
+ new.transforms = self.transforms + [transform]
136
+
137
+ return new
138
+
139
+ def _with_name_transform(self, name_transform: Callable[[str], str]) -> "SelectorcolumnOperations":
140
+ """Return an immutable copy of this selector with a rename queued.
141
+
142
+ Backs `.prefix()`/`.suffix()`/`.map_alias()`. Unlike `_copy`, this
143
+ never touches `self.transforms` (the value pipeline) or the
144
+ selector's `Column` expressions at all -- it only records how the
145
+ *matched column name* should be transformed once real names are
146
+ known (in `_resolve_selector_exprs`), composing with any
147
+ rename already queued so multiple renames chain in order
148
+ (e.g. ``.prefix('a_').suffix('_b')``).
149
+
150
+ Parameters
151
+ ----------
152
+ name_transform : callable
153
+ A one-argument function accepting a column's current name (`str`)
154
+ and returning its new name (`str`).
155
+
156
+ Returns
157
+ -------
158
+ SelectorcolumnOperations
159
+ A new instance of the same concrete class, with all of this
160
+ instance's attributes copied over and `_name_transform` set to
161
+ `name_transform` composed after any already queued. `self` itself
162
+ is left unmodified.
163
+ """
164
+ new = self.__class__.__new__(self.__class__)
165
+
166
+ new.__dict__.update(self.__dict__)
167
+
168
+ previous = self._name_transform
169
+
170
+ if previous is None:
171
+ new._name_transform = name_transform
172
+ else:
173
+ new._name_transform = lambda name: name_transform(previous(name))
174
+
175
+ return new
176
+
177
+ def prefix(self, prefix: str) -> "SelectorcolumnOperations":
178
+ """Queue a rename prepending `prefix` to every matched column's name.
179
+
180
+ Mirrors polars' `Expr.prefix`, but implemented at the selector level
181
+ (via `_with_name_transform`) rather than by dispatching to
182
+ `pyspark.sql.Column.prefix` -- this keeps the rename as pure name
183
+ metadata instead of an early `.alias(...)` baked into the value
184
+ expression, so it composes correctly with any value transform
185
+ (`.cast(...)`, arithmetic, ...) chained before or after it, and does
186
+ not affect `~`/`&`/`|`'s filter-condition detection.
187
+
188
+ Parameters
189
+ ----------
190
+ prefix : str
191
+ The string to prepend to each matched column's current name.
192
+
193
+ Returns
194
+ -------
195
+ SelectorcolumnOperations
196
+ A new selector with the rename queued.
197
+
198
+ Examples
199
+ --------
200
+ >>> df.select(by_dtype([T.IntegerType]).prefix('int_'))
201
+ DataFrame[...]
202
+ >>> df.select((by_dtype([T.IntegerType]) + 1).prefix('int_'))
203
+ DataFrame[...]
204
+ """
205
+ return self._with_name_transform(lambda name: f"{prefix}{name}")
206
+
207
+ def suffix(self, suffix: str) -> "SelectorcolumnOperations":
208
+ """Queue a rename appending `suffix` to every matched column's name.
209
+
210
+ Mirrors polars' `Expr.suffix`. See `prefix` for why this is
211
+ implemented at the selector level instead of dispatching to
212
+ `pyspark.sql.Column.suffix`.
213
+
214
+ Parameters
215
+ ----------
216
+ suffix : str
217
+ The string to append to each matched column's current name.
218
+
219
+ Returns
220
+ -------
221
+ SelectorcolumnOperations
222
+ A new selector with the rename queued.
223
+
224
+ Examples
225
+ --------
226
+ >>> df.select(by_dtype([T.IntegerType]).suffix('_int'))
227
+ DataFrame[...]
228
+ """
229
+ return self._with_name_transform(lambda name: f"{name}{suffix}")
230
+
231
+ def map_alias(self, func: Callable[[str], str]) -> "SelectorcolumnOperations":
232
+ """Queue a rename computed by applying `func` to each matched column's name.
233
+
234
+ Mirrors polars' `Expr.map_alias`. `.prefix()`/`.suffix()` are both
235
+ just `.map_alias()` with a prepend/append built in. See `prefix` for
236
+ why this is implemented at the selector level instead of dispatching
237
+ to `pyspark.sql.Column.map_alias`.
238
+
239
+ Parameters
240
+ ----------
241
+ func : callable
242
+ A one-argument function accepting a column's current name (`str`)
243
+ and returning its new name (`str`).
244
+
245
+ Returns
246
+ -------
247
+ SelectorcolumnOperations
248
+ A new selector with the rename queued.
249
+
250
+ Examples
251
+ --------
252
+ >>> df.select(by_dtype([T.StringType]).map_alias(lambda n: n.upper()))
253
+ DataFrame[...]
254
+ """
255
+ return self._with_name_transform(func)
256
+
257
+ # --------------------------------------------------
258
+ # operator decorator
259
+ # --------------------------------------------------
260
+
261
+ @staticmethod
262
+ def operator_wrapper(op: Callable[..., Any]) -> Callable[..., "SelectorcolumnOperations"]:
263
+ """Build a dunder-method implementation that chains a Python operator.
264
+
265
+ Used to define every arithmetic/comparison dunder on this class
266
+ (``__add__``, ``__gt__``, ...) in one line each, e.g.
267
+ ``__add__ = operator_wrapper(operator.add)``.
268
+
269
+ Parameters
270
+ ----------
271
+ op : callable
272
+ A function from the standard library `operator` module (or
273
+ anything with the same shape) applied to each matched column's
274
+ `pyspark.sql.Column` expression once the selector is resolved
275
+ against a real dataframe. Binary operators receive
276
+ ``(column, other)``; unary operators such as
277
+ ``operator.invert`` receive only ``column``.
278
+
279
+ Returns
280
+ -------
281
+ callable
282
+ A dunder-method-shaped function that appends the corresponding
283
+ operator call to the selector's transform pipeline via `_copy`
284
+ and returns the resulting new selector. Binary dunders accept
285
+ ``(self, other)``; unary dunders accept only ``self``.
286
+
287
+ Examples
288
+ --------
289
+ >>> __add__ = operator_wrapper(operator.add)
290
+ >>> by_dtype([T.IntegerType]) + 1
291
+ <DTypeSelector ...>
292
+ """
293
+
294
+ def overload(self, other):
295
+
296
+ return self._copy(lambda c: op(c, other))
297
+
298
+ return overload
299
+
300
+ # --------------------------------------------------
301
+ # spark function decorator
302
+ # --------------------------------------------------
303
+
304
+ @staticmethod
305
+ def spark_wrapper(spark_func: Callable[..., Column]) -> Callable[..., "SelectorcolumnOperations"]:
306
+ """Build a chainable method that wraps one `pyspark.sql.functions` function.
307
+
308
+ Used both explicitly (e.g. `cast`) and automatically, via the
309
+ ``for fx in dir(F): ...`` loop below, to make every callable in
310
+ `pyspark.sql.functions` available as a chained method on any selector,
311
+ e.g. ``by_dtype([T.StringType]).upper()`` (wrapping `pyspark.sql.functions.upper`).
312
+
313
+ Parameters
314
+ ----------
315
+ spark_func : callable
316
+ A function from `pyspark.sql.functions` (or any function with the
317
+ same shape, e.g. ``lambda c, dtype: c.cast(dtype)``) whose first
318
+ argument is the `pyspark.sql.Column` to operate on.
319
+
320
+ Returns
321
+ -------
322
+ callable
323
+ A chainable method ``wrapper(self, *args, **kwargs)`` that appends
324
+ ``lambda c: spark_func(c, *args, **kwargs)`` to the selector's
325
+ transform pipeline via `_copy`, and returns the resulting new
326
+ selector. Carries `spark_func`'s own name/docstring (via
327
+ `functools.wraps`) so ``help(some_selector.upper)`` shows the real
328
+ `pyspark.sql.functions.upper` documentation.
329
+
330
+ Examples
331
+ --------
332
+ >>> upper = spark_wrapper(F.upper)
333
+ >>> by_dtype([T.StringType]).upper()
334
+ <DTypeSelector ...>
335
+ """
336
+
337
+ @functools.wraps(spark_func)
338
+ def wrapper(self, *args, **kwargs):
339
+
340
+ return self._copy(lambda c: spark_func(c, *args, **kwargs))
341
+
342
+ return wrapper
343
+
344
+ def _make_dispatching_column_wrapper(method_name: str) -> Callable[..., Column]:
345
+ """Build a wrapper that dispatches to `Column` or `pyspark.sql.functions` by argument type.
346
+
347
+ Resolves the `F`/`Column` name-collision. this is where there are functions
348
+ in `pyspark.sql.functions.<method_name>` and `pyspark.sql.Column.<method_name>`
349
+ share a name but treat a plain (non-`Column`) argument differently. This
350
+ wrapper picks whichever one matches the caller's intent based on the type of
351
+ the argument actually passed, so both usages keep working.
352
+
353
+ Parameters
354
+ ----------
355
+ method_name : str
356
+ Name shared by a `pyspark.sql.functions` function and a `pyspark.sql.Column`
357
+ instance method (e.g. `"contains"`), used to look up both versions.
358
+
359
+ Returns
360
+ -------
361
+ callable
362
+ A function `wrapper(c, *args, **kwargs)` that, when the first
363
+ argument in `args` (if any) is a `pyspark.sql.Column`, calls
364
+ `pyspark.sql.functions.<method_name>(c, *args, **kwargs)` (column-to-column
365
+ semantics); otherwise calls `getattr(c, method_name)(*args, **kwargs)`
366
+ (literal semantics via `Column`'s own bound method). Carries `Column`'s
367
+ docstring for the method so `help()` on the resulting chained selector
368
+ method shows accurate documentation.
369
+ """
370
+ column_method = _get_column_method(method_name)
371
+ spark_func = getattr(F, method_name)
372
+
373
+ def wrapper(c: Column, *args: Any, **kwargs: Any) -> Column:
374
+ # `_COLUMN_CLASSES` (classic + Spark Connect `Column`, when Connect is
375
+ # available) instead of the bare `Column` name -- otherwise a Spark
376
+ # Connect `Column` argument would be misdetected as a plain literal
377
+ # and routed to the wrong (literal-semantics) branch below.
378
+ if args and isinstance(args[0], _COLUMN_CLASSES):
379
+ return spark_func(c, *args, **kwargs)
380
+ return getattr(c, method_name)(*args, **kwargs)
381
+
382
+ wrapper.__name__ = method_name
383
+ wrapper.__doc__ = column_method.__doc__
384
+ # same rename-op marker carry-through as `_make_column_only_wrapper`
385
+ # (defensive -- no current rename op collides with an `F` name).
386
+ # wrapper._is_rename_op = getattr(column_method, "_is_rename_op", False)
387
+ return wrapper
388
+
389
+ def resolve_columns(self: "BaseSelector", df: DataFrame) -> list[Column]:
390
+ """Resolve this selector's matched columns into transformed `Column` expressions.
391
+
392
+ Parameters
393
+ ----------
394
+ df : pyspark.sql.DataFrame
395
+ The dataframe to resolve the selector's matched column names
396
+ against, and to build each `pyspark.sql.Column` expression from.
397
+
398
+ Returns
399
+ -------
400
+ list of pyspark.sql.Column
401
+ One `pyspark.sql.Column` expression per matched column, in
402
+ `resolve`'s order, with every transform in `self.transforms`
403
+ applied in sequence (e.g. ``by_dtype([T.IntegerType]) + 1`` returns
404
+ ``F.col(name) + 1`` for every matched int column).
405
+
406
+ Examples
407
+ --------
408
+ >>> by_dtype([T.IntegerType]).resolve_columns(df)
409
+ [Column<'(integer_col + 0)'>, ...]
410
+ """
411
+ cols = [_quoted_col(c) for c in self.resolve(df)]
412
+
413
+ for transform in self.transforms:
414
+ cols = [transform(c) for c in cols]
415
+
416
+ return cols
417
+
418
+ # --------------------------------------------------
419
+ # apply transforms
420
+ # --------------------------------------------------
421
+
422
+ __add__ = operator_wrapper(operator.add)
423
+ __sub__ = operator_wrapper(operator.sub)
424
+ __mul__ = operator_wrapper(operator.mul)
425
+ __truediv__ = operator_wrapper(operator.truediv)
426
+ __mod__ = operator_wrapper(operator.mod)
427
+ __pow__ = operator_wrapper(operator.pow)
428
+ # __len__ = operator_wrapper(operator.len)
429
+ __floordiv__ = operator_wrapper(operator.floordiv)
430
+
431
+ __eq__ = operator_wrapper(operator.eq)
432
+ __gt__ = operator_wrapper(operator.gt)
433
+ __ge__ = operator_wrapper(operator.ge)
434
+ __lt__ = operator_wrapper(operator.lt)
435
+ __le__ = operator_wrapper(operator.le)
436
+ __ne__ = operator_wrapper(operator.ne)
437
+
438
+ # __contains__ = operator_wrapper(operator.contains)
439
+ # __and__ = operator_wrapper(operator.and)
440
+ # __or__ = operator_wrapper(operator.or)
441
+
442
+ __getitem__ = operator_wrapper(operator.getitem)
443
+ # __setitem__ = operator_wrapper(operator.setitem)
444
+ # __delitem__ = operator_wrapper(operator.delitem)
445
+ __invert__ = operator_wrapper(operator.invert)
446
+
447
+
448
+ # --------------------------------------------------------------------------------
449
+ # Allow support for column selector level operations
450
+ # --------------------------------------------------------------------------------
451
+
452
+
453
+ class SelectorSelectionOperations:
454
+ """Mixin providing set-algebra and filter-condition operators for selectors.
455
+
456
+ Every selector class (`DTypeSelector`, `IndexSelector`, `RegexSelector`,
457
+ and any future selector) inherits from this class (via `BaseSelector`) so
458
+ that ``~ - & ^ |`` compose selectors together, e.g.
459
+ ``by_dtype([T.IntegerType]) | by_dtype([T.StringType])`` (union) or
460
+ ``~by_dtype([T.StringType])`` (every column that is NOT a string column).
461
+
462
+ These same operators (``&``, ``|``, ``~``) switch to "expression mode"
463
+ once a selector already has a comparison/arithmetic transform chained
464
+ onto it (e.g. ``by_dtype([T.IntegerType]) > 0``), or the other operand isn't a
465
+ bare selector at all (a plain `pyspark.sql.Column`). In that mode, they
466
+ combine row-conditions for `filter`/`where`
467
+ (e.g. ``df.filter(by_dtype([T.IntegerType]) > 0)``) instead of intersecting/
468
+ unioning which columns are matched -- see `SelectorFilterCondition`.
469
+
470
+ Parameters
471
+ ----------
472
+ resolver : callable, optional
473
+ A one-argument function ``resolver(df) -> list[str]`` built by
474
+ `_selector_copy` when a set operator (``~ - & ^ |``) combines two
475
+ selectors. When present, `resolve` (defined on `BaseSelector`
476
+ subclasses) must call this instead of its own default matching
477
+ logic. Defaults to `None` when not given.
478
+
479
+ Attributes
480
+ ----------
481
+ _resolver : callable or None
482
+ The resolver function described above, or `None` when this selector
483
+ has not been produced by a set operator.
484
+
485
+ Examples
486
+ --------
487
+ >>> by_dtype([T.IntegerType]) | by_dtype([T.StringType])
488
+ <DTypeSelector ...>
489
+ >>> ~by_dtype([T.StringType])
490
+ <DTypeSelector ...>
491
+ """
492
+
493
+ def __init__(self, resolver: Callable[[DataFrame], list[str]] | None = None) -> None:
494
+ """Initialize the combinator resolver.
495
+
496
+ Parameters
497
+ ----------
498
+ resolver : callable, optional
499
+ Existing resolver function to start from. Defaults to `None` when
500
+ not given.
501
+ """
502
+
503
+ self._resolver = resolver
504
+
505
+ def _selector_copy(self, resolver: Callable[[DataFrame], list[str]]) -> "SelectorSelectionOperations":
506
+ """Return an immutable copy of this selector with a combined resolver.
507
+
508
+ Parameters
509
+ ----------
510
+ resolver : callable
511
+ A one-argument function ``resolver(df) -> list[str]`` implementing
512
+ the combined set-algebra result (union, intersection, ...).
513
+
514
+ Returns
515
+ -------
516
+ SelectorSelectionOperations
517
+ A new instance of the same concrete class, with all of this
518
+ instance's attributes copied over and `_resolver` set to
519
+ `resolver`. `self` itself is left unmodified.
520
+ """
521
+
522
+ new = self.__class__.__new__(self.__class__)
523
+
524
+ new.__dict__.update(self.__dict__)
525
+
526
+ new._resolver = resolver
527
+
528
+ return new
529
+
530
+ def __and__(
531
+ self: "BaseSelector", other: Union["BaseSelector", Column, "SelectorFilterCondition"]
532
+ ) -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
533
+ """Intersect two selectors' matched columns, or AND two row conditions.
534
+
535
+ Parameters
536
+ ----------
537
+ other : BaseSelector, pyspark.sql.Column, or SelectorFilterCondition
538
+ The right-hand operand of ``&``.
539
+
540
+ Returns
541
+ -------
542
+ SelectorSelectionOperations or SelectorFilterCondition
543
+ A new selector matching only columns present in both `self` and
544
+ `other` (set-algebra mode), or a `SelectorFilterCondition` ANDing
545
+ the two row conditions together (expression mode -- triggered
546
+ when `self` already has a chained transform, or `other` isn't a
547
+ bare selector).
548
+
549
+ Examples
550
+ --------
551
+ >>> by_dtype([T.DoubleType, T.IntegerType]) & by_dtype([T.DoubleType, T.StringType])
552
+ <DTypeSelector ...>
553
+ >>> df.filter((by_dtype([T.IntegerType]) > 0) & (by_dtype([T.IntegerType]) < 9))
554
+ DataFrame[...]
555
+ """
556
+
557
+ # expression mode: `self` already has a comparison/arithmetic transform
558
+ # chained onto it (e.g. `by_dtype([T.IntegerType]) == 0`), or `other` isn't a raw
559
+ # selector at all (a plain pyspark Column like `F.col('x') == None`, or
560
+ # another transformed selector). in that case `&` means "AND these row
561
+ # conditions together", not "intersect these two selectors' matched
562
+ # columns" -- so it's deferred into a SelectorFilterCondition instead of
563
+ # the name-set intersection below.
564
+ if self.transforms or not isinstance(other, BaseSelector) or getattr(other, "transforms", None):
565
+ return SelectorFilterCondition(self, "and", other)
566
+
567
+ left = self
568
+ right = other
569
+
570
+ def _resolve(df):
571
+
572
+ right_cols = set(right.resolve(df))
573
+
574
+ return [dim for dim in left.resolve(df) if dim in right_cols]
575
+
576
+ return self._selector_copy(_resolve)
577
+
578
+ def __or__(
579
+ self: "BaseSelector", other: Union["BaseSelector", Column, "SelectorFilterCondition"]
580
+ ) -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
581
+ """Union two selectors' matched columns, or OR two row conditions.
582
+
583
+ Parameters
584
+ ----------
585
+ other : BaseSelector, pyspark.sql.Column, or SelectorFilterCondition
586
+ The right-hand operand of ``|``.
587
+
588
+ Returns
589
+ -------
590
+ SelectorSelectionOperations or SelectorFilterCondition
591
+ A new selector matching every column present in `self` or
592
+ `other` (set-algebra mode), or a `SelectorFilterCondition` ORing
593
+ the two row conditions together (expression mode -- same trigger
594
+ as `__and__`).
595
+
596
+ Examples
597
+ --------
598
+ >>> by_dtype([T.IntegerType]) | by_dtype([T.StringType])
599
+ <DTypeSelector ...>
600
+ """
601
+
602
+ # same expression-mode guard as __and__ -- see the comment there.
603
+ if self.transforms or not isinstance(other, BaseSelector) or getattr(other, "transforms", None):
604
+ return SelectorFilterCondition(self, "or", other)
605
+
606
+ left = self
607
+ right = other
608
+
609
+ def _resolve(df):
610
+
611
+ left_cols = set(left.resolve(df))
612
+ right_cols = set(right.resolve(df))
613
+
614
+ return [c for c in df.columns if c in left_cols or c in right_cols]
615
+
616
+ return self._selector_copy(_resolve)
617
+
618
+ def __sub__(
619
+ self: "BaseSelector", other: "BaseSelector"
620
+ ) -> Union["SelectorSelectionOperations", "SelectorcolumnOperations"]:
621
+ """Return columns matched by `self` but not by `other` (set difference).
622
+
623
+ Parameters
624
+ ----------
625
+ other : BaseSelector
626
+ The selector whose matched columns are excluded from `self`'s.
627
+
628
+ Returns
629
+ -------
630
+ SelectorSelectionOperations or SelectorcolumnOperations
631
+ A new selector matching every column in `self` that is not also
632
+ in `other`. The column-operation result is used when the shared
633
+ `BaseSelector.__sub__` dispatcher receives a non-selector operand.
634
+
635
+ Examples
636
+ --------
637
+ >>> by_dtype([T.StringType, T.IntegerType, T.DoubleType]) - by_dtype([T.IntegerType, T.DoubleType])
638
+ <DTypeSelector ...>
639
+ """
640
+
641
+ left = self
642
+ right = other
643
+
644
+ def _resolve(df):
645
+
646
+ right_cols = set(right.resolve(df))
647
+
648
+ return [dim for dim in left.resolve(df) if dim not in right_cols]
649
+
650
+ return self._selector_copy(_resolve)
651
+
652
+ def __xor__(self: "BaseSelector", other: "BaseSelector") -> "SelectorSelectionOperations":
653
+ """Return columns matched by exactly one of `self`/`other` (symmetric difference).
654
+
655
+ Parameters
656
+ ----------
657
+ other : BaseSelector
658
+ The selector to compare against `self`.
659
+
660
+ Returns
661
+ -------
662
+ SelectorSelectionOperations
663
+ A new selector matching every column present in exactly one of
664
+ `self` or `other`, but not both.
665
+
666
+ Examples
667
+ --------
668
+ >>> by_dtype([T.DoubleType, T.IntegerType]) ^ by_dtype([T.IntegerType, T.StringType])
669
+ <DTypeSelector ...>
670
+ """
671
+
672
+ left = self
673
+ right = other
674
+
675
+ def _resolve(df):
676
+
677
+ left_cols = set(left.resolve(df))
678
+ right_cols = set(right.resolve(df))
679
+
680
+ return [c for c in df.columns if (c in left_cols) != (c in right_cols)]
681
+
682
+ return self._selector_copy(_resolve)
683
+
684
+ def __invert__(self: "BaseSelector") -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
685
+ """Complement this selector's matched columns, or negate a row condition.
686
+
687
+ Returns
688
+ -------
689
+ SelectorSelectionOperations or SelectorFilterCondition
690
+ A new selector matching every column NOT matched by `self`
691
+ (set-algebra mode), or a `SelectorFilterCondition` negating the
692
+ row condition (expression mode -- triggered when `self` already
693
+ has a chained comparison/arithmetic transform, e.g.
694
+ ``~(by_dtype([T.IntegerType]) > 0)``).
695
+
696
+ Examples
697
+ --------
698
+ >>> ~by_dtype([T.StringType])
699
+ <DTypeSelector ...>
700
+ >>> df.filter(~(by_dtype([T.IntegerType]) > 0))
701
+ DataFrame[...]
702
+ """
703
+
704
+ # same expression-mode guard as __and__/__or__ -- if a comparison/arithmetic
705
+ # transform is already chained onto this selector (e.g. `~(by_dtype([T.IntegerType]) > 0)`),
706
+ # `~` has to negate that row condition, not complement the matched
707
+ # *columns*. without this check `~` would silently throw away the
708
+ # `> 0` transform and return "every column NOT of dtype int" instead -- a
709
+ # completely different (and wrong) result.
710
+ if self.transforms:
711
+ return SelectorFilterCondition(self, "invert")
712
+
713
+ original = self
714
+
715
+ return self._selector_copy(lambda df: [c for c in df.columns if c not in original.resolve(df)])
716
+
717
+
718
+ # --------------------------------------------------------------------------------
719
+ # Filter condition for row-level filtering
720
+ # --------------------------------------------------------------------------------
721
+
722
+
723
+ class SelectorFilterCondition:
724
+ """Lazy boolean row-condition combinator used by `filter`/`where`.
725
+
726
+ Companion to `SelectorSelectionOperations`: once ``&``/``|``/``~`` are
727
+ used on a selector that already has a comparison/arithmetic transform
728
+ chained onto it (e.g. ``by_dtype([T.IntegerType]) == 0``), or against a plain
729
+ `pyspark.sql.Column` (e.g. ``F.col('data_column_4') == None``), those
730
+ operators no longer mean "combine which columns are selected" -- they
731
+ mean "AND/OR these row conditions together".
732
+ `SelectorSelectionOperations.__and__`/`__or__`/`__invert__` detect that
733
+ case and return one of these instead of a new selector.
734
+
735
+ Resolving into a real boolean `pyspark.sql.Column` is deferred (via
736
+ `to_column`) since it requires a `DataFrame` to resolve each selector's
737
+ matched columns against -- that dataframe isn't known yet at
738
+ ``&``/``|``/``~`` time, only once `filter`/`where` actually runs. A
739
+ selector operand that matches more than one column (e.g.
740
+ ``by_dtype([T.IntegerType]) > 0`` matching several int columns) is folded down
741
+ with ``&`` across its own matches first, so combining conditions composes
742
+ cleanly no matter how many real columns are on either side.
743
+
744
+ Parameters
745
+ ----------
746
+ left : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
747
+ The left-hand operand of this condition.
748
+ op : {"and", "or", "invert", None}, optional
749
+ The boolean operator combining `left` and `right`. `None` means
750
+ `left` is passed through unchanged (used when a single selector is
751
+ given directly to `filter`).
752
+ right : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column, optional
753
+ The right-hand operand, required when `op` is ``"and"``/``"or"``,
754
+ unused when `op` is ``"invert"``/`None`.
755
+
756
+ Examples
757
+ --------
758
+ >>> df.filter((by_dtype([T.IntegerType]) > 0) & (by_dtype([T.IntegerType]) < 9))
759
+ DataFrame[...]
760
+ >>> df.filter(~(by_dtype([T.IntegerType]) > 0))
761
+ DataFrame[...]
762
+ """
763
+
764
+ def __init__(
765
+ self,
766
+ left: Union["BaseSelector", "SelectorFilterCondition", Column],
767
+ op: str | None = None,
768
+ right: Union["BaseSelector", "SelectorFilterCondition", Column] | None = None,
769
+ ) -> None:
770
+ """Initialize the condition tree node.
771
+
772
+ Parameters
773
+ ----------
774
+ left : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
775
+ The left-hand operand of this condition.
776
+ op : {"and", "or", "invert", None}, optional
777
+ The boolean operator combining `left` and `right`.
778
+ right : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column, optional
779
+ The right-hand operand, when `op` requires one.
780
+ """
781
+
782
+ self._left = left
783
+ self._op = op
784
+ self._right = right
785
+
786
+ @staticmethod
787
+ def _to_expr(operand: Union["BaseSelector", "SelectorFilterCondition", Column, Any], df: DataFrame) -> Column:
788
+ """Resolve one operand (selector, condition, or plain value) into one `Column`.
789
+
790
+ Parameters
791
+ ----------
792
+ operand : BaseSelector, SelectorFilterCondition, or Any
793
+ The operand to resolve. A `SelectorFilterCondition` recurses via
794
+ `to_column`; a `BaseSelector` resolves to all of its matched
795
+ columns' expressions, ANDed together; anything else (a plain
796
+ `pyspark.sql.Column` or literal) passes through unchanged.
797
+ df : pyspark.sql.DataFrame
798
+ The dataframe to resolve selector-matched columns against.
799
+
800
+ Returns
801
+ -------
802
+ pyspark.sql.Column
803
+ The resolved boolean (or other) expression.
804
+
805
+ Raises
806
+ ------
807
+ pyspark.errors.PySparkValueError
808
+ If `operand` is a `BaseSelector` that matches zero columns
809
+ against `df`.
810
+ """
811
+
812
+ if isinstance(operand, SelectorFilterCondition):
813
+ return operand.to_column(df)
814
+
815
+ if isinstance(operand, BaseSelector):
816
+ exprs = operand.resolve_columns(df)
817
+
818
+ if not exprs:
819
+ raise PySparkValueError(
820
+ errorClass="CANNOT_BE_EMPTY",
821
+ messageParameters={"item": "columns matched by the selector"},
822
+ )
823
+
824
+ combined = exprs[0]
825
+
826
+ for expr in exprs[1:]:
827
+ combined = combined & expr
828
+
829
+ return combined
830
+
831
+ return operand # plain pyspark Column / literal condition, passed through as-is
832
+
833
+ def to_column(self, df: DataFrame) -> Column:
834
+ """Resolve this condition tree into one final boolean `pyspark.sql.Column`.
835
+
836
+ Parameters
837
+ ----------
838
+ df : pyspark.sql.DataFrame
839
+ The dataframe to resolve every selector-matched column against.
840
+
841
+ Returns
842
+ -------
843
+ pyspark.sql.Column
844
+ The fully resolved boolean expression, ready to be passed to
845
+ `pyspark.sql.DataFrame.filter`.
846
+
847
+ Raises
848
+ ------
849
+ pyspark.errors.PySparkValueError
850
+ If `self._op` is set to something other than
851
+ ``"and"``/``"or"``/``"invert"``/`None`.
852
+
853
+ Examples
854
+ --------
855
+ >>> SelectorFilterCondition(by_dtype([T.IntegerType]) > 0).to_column(df)
856
+ Column<'(integer_col > 0)'>
857
+ """
858
+
859
+ left_expr = self._to_expr(self._left, df)
860
+
861
+ if self._op is None:
862
+ return left_expr
863
+
864
+ if self._op == "invert":
865
+ return ~left_expr
866
+
867
+ right_expr = self._to_expr(self._right, df)
868
+
869
+ if self._op == "and":
870
+ return left_expr & right_expr
871
+
872
+ if self._op == "or":
873
+ return left_expr | right_expr
874
+
875
+ raise PySparkValueError(
876
+ message=f"unsupported filter condition operator: {self._op!r}",
877
+ )
878
+
879
+ def __and__(self, other: Union["BaseSelector", "SelectorFilterCondition", Column]) -> "SelectorFilterCondition":
880
+ """Return a new condition ANDing `self` with `other`.
881
+
882
+ Parameters
883
+ ----------
884
+ other : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
885
+ The right-hand operand of ``&``.
886
+
887
+ Returns
888
+ -------
889
+ SelectorFilterCondition
890
+ A new condition node combining `self` and `other` with ``"and"``.
891
+ """
892
+ return SelectorFilterCondition(self, "and", other)
893
+
894
+ def __rand__(self, other: Union["BaseSelector", Column]) -> "SelectorFilterCondition":
895
+ """Return a new condition ANDing `other` with `self` (reflected `&`).
896
+
897
+ Parameters
898
+ ----------
899
+ other : BaseSelector or pyspark.sql.Column
900
+ The left-hand operand, when `other & self` is evaluated because
901
+ `other` doesn't implement `__and__` for `self`'s type.
902
+
903
+ Returns
904
+ -------
905
+ SelectorFilterCondition
906
+ A new condition node combining `other` and `self` with ``"and"``.
907
+ """
908
+ return SelectorFilterCondition(other, "and", self)
909
+
910
+ def __or__(self, other: Union["BaseSelector", "SelectorFilterCondition", Column]) -> "SelectorFilterCondition":
911
+ """Return a new condition ORing `self` with `other`.
912
+
913
+ Parameters
914
+ ----------
915
+ other : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
916
+ The right-hand operand of ``|``.
917
+
918
+ Returns
919
+ -------
920
+ SelectorFilterCondition
921
+ A new condition node combining `self` and `other` with ``"or"``.
922
+ """
923
+ return SelectorFilterCondition(self, "or", other)
924
+
925
+ def __ror__(self, other: Union["BaseSelector", Column]) -> "SelectorFilterCondition":
926
+ """Return a new condition ORing `other` with `self` (reflected `|`).
927
+
928
+ Parameters
929
+ ----------
930
+ other : BaseSelector or pyspark.sql.Column
931
+ The left-hand operand, when `other | self` is evaluated because
932
+ `other` doesn't implement `__or__` for `self`'s type.
933
+
934
+ Returns
935
+ -------
936
+ SelectorFilterCondition
937
+ A new condition node combining `other` and `self` with ``"or"``.
938
+ """
939
+ return SelectorFilterCondition(other, "or", self)
940
+
941
+ def __invert__(self) -> "SelectorFilterCondition":
942
+ """Return a new condition negating `self`.
943
+
944
+ Returns
945
+ -------
946
+ SelectorFilterCondition
947
+ A new condition node wrapping `self` with ``"invert"``.
948
+ """
949
+ return SelectorFilterCondition(self, "invert")
950
+
951
+
952
+ # ==========================================================
953
+ # define exactly what a column selector is and is capable of doing
954
+ # ==========================================================
955
+
956
+
957
+ class BaseSelector(SelectorSelectionOperations, SelectorcolumnOperations):
958
+ """Abstract base class for every column selector (`by_dtype`, `by_index`, `matches`).
959
+
960
+ Combines `SelectorSelectionOperations` (set algebra: ``~ - & ^ |``) and
961
+ `SelectorcolumnOperations` (chained column transforms: arithmetic,
962
+ comparisons, every `pyspark.sql.functions` function) into one mixin base
963
+ that every concrete selector class inherits from. A concrete subclass
964
+ only needs to implement `resolve` -- everything else (transform chaining,
965
+ set-algebra, filter-condition composition) is inherited for free.
966
+
967
+ Parameters
968
+ ----------
969
+ transforms : list of callable, optional
970
+ Initial transform pipeline, forwarded to
971
+ `SelectorcolumnOperations.__init__`.
972
+ resolver : callable, optional
973
+ Initial combinator resolver, forwarded to
974
+ `SelectorSelectionOperations.__init__`.
975
+ require_col_match : bool, default True
976
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when the
977
+ selector matches zero columns.
978
+
979
+ Notes
980
+ -----
981
+ MRO is ``BaseSelector -> SelectorSelectionOperations -> SelectorcolumnOperations``,
982
+ so `SelectorSelectionOperations`'s set-algebra `__invert__` (correct unary
983
+ signature) wins by default -- no override needed on this class for that
984
+ one. `__sub__` still needs disambiguation here since both mixins define
985
+ it, and unguarded MRO would make set-difference the silent default,
986
+ breaking arithmetic subtraction (e.g. ``some_selector - 5``, which has no
987
+ ``.resolve()``).
988
+ """
989
+
990
+ def __init__(
991
+ self,
992
+ transforms: list[Callable[[Column], Column]] | None = None,
993
+ resolver: Callable[[DataFrame], list[str]] | None = None,
994
+ require_col_match: bool = True,
995
+ ) -> None:
996
+ """Initialize both parent mixins' state.
997
+
998
+ Parameters
999
+ ----------
1000
+ transforms : list of callable, optional
1001
+ Initial transform pipeline.
1002
+ resolver : callable, optional
1003
+ Initial combinator resolver.
1004
+ require_col_match : bool, default True
1005
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when
1006
+ this selector matches zero columns. Set to `False` to opt out
1007
+ and allow a zero-column match to pass through silently.
1008
+ """
1009
+ SelectorcolumnOperations.__init__(self, transforms)
1010
+ SelectorSelectionOperations.__init__(self, resolver)
1011
+
1012
+ self.require_col_match = require_col_match
1013
+
1014
+ # --------------------------------------------------
1015
+ # each selector implements this
1016
+ # --------------------------------------------------
1017
+
1018
+ def resolve(self, df: DataFrame) -> list[str]:
1019
+ """Resolve this selector's matched column names against a dataframe.
1020
+
1021
+ Must be implemented by every concrete subclass (`DTypeSelector`,
1022
+ `IndexSelector`, `RegexSelector`, ...). Implementations should check
1023
+ `self._resolver` first (set by `_selector_copy` when a set operator
1024
+ ``~ - & ^ |`` combines two selectors) before falling back to their
1025
+ own type-specific matching logic, and should pass their final
1026
+ matched-name list through `_check_matched` before returning it.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ df : pyspark.sql.DataFrame
1031
+ The dataframe to resolve matched column names against.
1032
+
1033
+ Returns
1034
+ -------
1035
+ list of str
1036
+ The matched column names, in dataframe-column order.
1037
+
1038
+ Raises
1039
+ ------
1040
+ pyspark.errors.PySparkNotImplementedError
1041
+ Always, on `BaseSelector` itself -- subclasses must override this.
1042
+ """
1043
+
1044
+ raise PySparkNotImplementedError(
1045
+ errorClass="NOT_IMPLEMENTED",
1046
+ messageParameters={"feature": "BaseSelector.resolve"},
1047
+ )
1048
+
1049
+ def _check_matched(self, matched: list[str]) -> list[str]:
1050
+ """Raise if `matched` is empty, unless `self.require_col_match` is `False`.
1051
+
1052
+ Single shared empty-match guard used by every concrete selector's
1053
+ `resolve` (`DTypeSelector`, `IndexSelector`, `RegexSelector`), on
1054
+ both its own type-specific matching path and its combined-resolver
1055
+ path (`self._resolver`, built by `~ - & ^ |`) -- so a set-algebra
1056
+ combination that ends up matching nothing (e.g. `all() - integer()`
1057
+ on a dataframe with only one integer column) is caught exactly the
1058
+ same way as a plain selector matching nothing (e.g. `numeric()` on a
1059
+ dataframe with no numeric columns at all).
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ matched : list of str
1064
+ The column names this selector resolved to.
1065
+
1066
+ Returns
1067
+ -------
1068
+ list of str
1069
+ `matched`, unchanged, when non-empty or when `self.require_col_match`
1070
+ is `False`.
1071
+
1072
+ Raises
1073
+ ------
1074
+ pyspark.errors.PySparkValueError
1075
+ If `matched` is empty and `self.require_col_match` is `True`.
1076
+
1077
+ Examples
1078
+ --------
1079
+ >>> by_dtype([T.StringType]).resolve(df) # df has no string columns
1080
+ Traceback (most recent call last):
1081
+ ...
1082
+ pyspark.errors.exceptions.base.PySparkValueError: ...
1083
+ >>> by_dtype([T.StringType], require_col_match=False).resolve(df)
1084
+ []
1085
+ """
1086
+
1087
+ if not matched and self.require_col_match:
1088
+ raise PySparkValueError(
1089
+ errorClass="CANNOT_BE_EMPTY",
1090
+ messageParameters={"item": f"columns matched by {self.__class__.__name__}"},
1091
+ )
1092
+
1093
+ return matched
1094
+
1095
+ # --------------------------------------------------
1096
+ # MRO is now BaseSelector -> SelectorSelectionOperations -> SelectorcolumnOperations,
1097
+ # so SelectorSelectionOperations' set-algebra __invert__ (correct unary signature)
1098
+ # wins by default -- no override needed here anymore.
1099
+ #
1100
+ # __sub__ still needs disambiguation: both mixins define it, and unguarded
1101
+ # MRO would now make set-difference the silent default, breaking arithmetic
1102
+ # subtraction (e.g. some_selector - 5) since a plain value has no .resolve().
1103
+ # --------------------------------------------------
1104
+
1105
+ def __sub__(self, other: Any) -> SelectorSelectionOperations | SelectorcolumnOperations:
1106
+ """Dispatch `-` to set-difference (selector operand) or arithmetic (plain value).
1107
+
1108
+ Parameters
1109
+ ----------
1110
+ other : BaseSelector or Any
1111
+ A selector operand triggers set-difference (`SelectorSelectionOperations.__sub__`);
1112
+ anything else (e.g. an int/float) triggers arithmetic subtraction
1113
+ (`SelectorcolumnOperations.__sub__`).
1114
+
1115
+ Returns
1116
+ -------
1117
+ BaseSelector
1118
+ A new selector, either matching the set-difference of columns
1119
+ (selector operand) or with ``- other`` chained as a transform
1120
+ (plain value operand).
1121
+
1122
+ Examples
1123
+ --------
1124
+ >>> by_dtype([T.StringType, T.IntegerType]) - by_dtype([T.IntegerType])
1125
+ <DTypeSelector ...>
1126
+ >>> by_dtype([T.IntegerType]) - 5
1127
+ <DTypeSelector ...>
1128
+ """
1129
+
1130
+ if isinstance(other, SelectorSelectionOperations):
1131
+ return SelectorSelectionOperations.__sub__(self, other)
1132
+
1133
+ return SelectorcolumnOperations.__sub__(self, other)
1134
+
1135
+
1136
+ # --------------------------------------------------------------------------------
1137
+ # Create column selector for selecting columns by their real Spark SQL type
1138
+ # --------------------------------------------------------------------------------
1139
+
1140
+
1141
+ class DTypeSelector(BaseSelector):
1142
+ """Select columns whose real Spark SQL type is (or is a subclass of) any given type.
1143
+
1144
+ Matches against each column's real `pyspark.sql.types.DataType` instance
1145
+ (`df.schema[name].dataType`) via `isinstance`, not against dtype strings.
1146
+ Passing a category base class (e.g. `pyspark.sql.types.NumericType`)
1147
+ matches every concrete subclass of it (`ByteType`, `ShortType`,
1148
+ `IntegerType`, `LongType`, `FloatType`, `DoubleType`, `DecimalType`, ...),
1149
+ the same way `isinstance` already works for any Python class hierarchy --
1150
+ no separate "category" concept needed on top of it.
1151
+
1152
+ Parameters
1153
+ ----------
1154
+ dtypes : type or sequence of type
1155
+ One or more `pyspark.sql.types.DataType` subclasses to match against
1156
+ each column's real datatype (e.g. `[T.IntegerType, T.StringType]`, or
1157
+ a single category base class like `T.NumericType`).
1158
+ transforms : list of callable, optional
1159
+ Initial transform pipeline, forwarded to `BaseSelector.__init__`.
1160
+ require_col_match : bool, default True
1161
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when zero
1162
+ columns match the requested data types.
1163
+
1164
+ Attributes
1165
+ ----------
1166
+ dtypes : tuple of type
1167
+ The requested `DataType` subclasses, deduplicated, ready to pass
1168
+ straight to `isinstance`.
1169
+
1170
+ Examples
1171
+ --------
1172
+ >>> df.select(by_dtype([T.IntegerType, T.DoubleType]))
1173
+ DataFrame[...]
1174
+ >>> df.select(by_dtype([T.DoubleType]).cast('string'))
1175
+ DataFrame[...]
1176
+ >>> df.select(by_dtype([T.NumericType]))
1177
+ DataFrame[...]
1178
+ """
1179
+
1180
+ def __init__(
1181
+ self,
1182
+ dtypes: type | Sequence[type],
1183
+ transforms: list[Callable[[Column], Column]] | None = None,
1184
+ require_col_match: bool = True,
1185
+ ) -> None:
1186
+ """Initialize the requested `DataType` subclass set.
1187
+
1188
+ Parameters
1189
+ ----------
1190
+ dtypes : type or sequence of type
1191
+ One or more `pyspark.sql.types.DataType` subclasses to match.
1192
+ transforms : list of callable, optional
1193
+ Initial transform pipeline.
1194
+ require_col_match : bool, default True
1195
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when
1196
+ zero columns match `dtypes`. Forwarded to `BaseSelector.__init__`.
1197
+
1198
+ Raises
1199
+ ------
1200
+ pyspark.errors.PySparkTypeError
1201
+ If `dtypes` (or any item in it) is not a class, or not a subclass
1202
+ of `pyspark.sql.types.DataType`.
1203
+ """
1204
+
1205
+ super().__init__(transforms, require_col_match=require_col_match)
1206
+
1207
+ # a bare single class (e.g. by_dtype(T.IntegerType)) is convenient
1208
+ # shorthand for a one-item sequence -- normalize it the same way.
1209
+ if isinstance(dtypes, type):
1210
+ dtypes = (dtypes,)
1211
+
1212
+ dtypes = tuple(dict.fromkeys(dtypes)) # dedupe, preserve order
1213
+
1214
+ self._validate_dtype_classes(dtypes)
1215
+
1216
+ self.dtypes = dtypes
1217
+
1218
+ def _validate_dtype_classes(self, dtypes: Sequence[type]) -> None:
1219
+ """Raise if any requested item isn't a real `pyspark.sql.types.DataType` subclass.
1220
+
1221
+ Parameters
1222
+ ----------
1223
+ dtypes : sequence of type
1224
+ The candidate classes to validate.
1225
+
1226
+ Raises
1227
+ ------
1228
+ pyspark.errors.PySparkTypeError
1229
+ If any item is not a class, or is a class but not a subclass of
1230
+ `pyspark.sql.types.DataType`.
1231
+ """
1232
+
1233
+ bad = [d for d in dtypes if not (isinstance(d, type) and issubclass(d, T.DataType))]
1234
+
1235
+ if bad:
1236
+ raise PySparkTypeError(
1237
+ message=(
1238
+ f"by_dtype() received non-DataType item(s): {bad!r} -- every item must "
1239
+ "be a class (not an instance or a string) that subclasses "
1240
+ "pyspark.sql.types.DataType, e.g. T.IntegerType or T.NumericType."
1241
+ ),
1242
+ )
1243
+
1244
+ def resolve(self, df: DataFrame) -> list[str]:
1245
+ """Resolve to every column name on `df` whose real datatype is in `self.dtypes`.
1246
+
1247
+ Parameters
1248
+ ----------
1249
+ df : pyspark.sql.DataFrame
1250
+ The dataframe to resolve matched column names against.
1251
+
1252
+ Returns
1253
+ -------
1254
+ list of str
1255
+ Matched column names, in dataframe-column order.
1256
+
1257
+ Raises
1258
+ ------
1259
+ pyspark.errors.PySparkValueError
1260
+ Raised by `_check_matched` (via `BaseSelector`) if zero columns
1261
+ match and `self.require_col_match` is `True`.
1262
+
1263
+ Examples
1264
+ --------
1265
+ >>> DTypeSelector([T.IntegerType]).resolve(df)
1266
+ ['integer_col']
1267
+ """
1268
+
1269
+ # if a set-operation (~ - & ^ |) built a combined resolver via
1270
+ # _selector_copy, honor it first. otherwise fall back to the
1271
+ # normal dtype-matching behavior. without this check, resolve()
1272
+ # would always recompute off of self.dtypes and silently ignore
1273
+ # any selector-selection-operations combinator applied to it.
1274
+ if self._resolver:
1275
+ return self._check_matched(self._resolver(df))
1276
+
1277
+ return self._check_matched(
1278
+ [field.name for field in df.schema.fields if isinstance(field.dataType, self.dtypes)]
1279
+ )
1280
+
1281
+
1282
+ # --------------------------------------------------------------------------------
1283
+ # create column selector for selecting index position in table
1284
+ # --------------------------------------------------------------------------------
1285
+
1286
+
1287
+ class IndexSelector(BaseSelector):
1288
+ """Select columns by positional index.
1289
+
1290
+ Mirrors Python's own negative-indexing rule: ``-1`` is the last column,
1291
+ ``-2`` is second-to-last, etc, resolved against ``len(df.columns)``
1292
+ since the selector itself has no dataframe until `resolve` runs.
1293
+
1294
+ Parameters
1295
+ ----------
1296
+ indexes : tuple of (int or range)
1297
+ One or more positional indexes to select. Each item is either a bare
1298
+ int, or a `range` object covering a span in one shot (e.g.
1299
+ ``range(1, 4)`` for columns 1-3). Negative ints/ranges are supported
1300
+ (e.g. ``range(-3, 0)`` for the last 3 columns), same rule as a bare
1301
+ negative int.
1302
+ transforms : list of callable, optional
1303
+ Initial transform pipeline, forwarded to `BaseSelector.__init__`.
1304
+ strict_index_bounds : bool, default True
1305
+ Controls what happens when a requested index is out of bounds for
1306
+ the dataframe actually being resolved against: `True` raises
1307
+ immediately, naming the bad index and the dataframe's real column
1308
+ count; `False` silently skips any out-of-bounds index instead of
1309
+ raising, so partial matches are tolerated.
1310
+
1311
+ Attributes
1312
+ ----------
1313
+ indexes : tuple of (int or range)
1314
+ The requested indexes, as given.
1315
+ strict_index_bounds : bool
1316
+ Whether an out-of-bounds index raises (`True`) or is skipped
1317
+ (`False`).
1318
+
1319
+ Notes
1320
+ -----
1321
+ Duplicate positions (e.g. an index reachable both directly and via an
1322
+ overlapping range) are de-duplicated, preserving first-seen order.
1323
+ ``~ - & ^ |`` all work already -- `IndexSelector` only implements
1324
+ `resolve`; the set-algebra (and filter-condition) operators live entirely
1325
+ on `SelectorSelectionOperations`/`BaseSelector`, which every child class
1326
+ (this one, `DTypeSelector`, `RegexSelector`) inherits for free.
1327
+
1328
+ Examples
1329
+ --------
1330
+ >>> df.select(by_index(0, 1))
1331
+ DataFrame[...]
1332
+ >>> df.select(by_index(range(1, 4), -1))
1333
+ DataFrame[...]
1334
+ >>> df.select(by_index(0, strict_index_bounds=False))
1335
+ DataFrame[...]
1336
+ """
1337
+
1338
+ def __init__(
1339
+ self,
1340
+ indexes: tuple[int | range, ...],
1341
+ transforms: list[Callable[[Column], Column]] | None = None,
1342
+ strict_index_bounds: bool = True,
1343
+ require_col_match: bool = True,
1344
+ ) -> None:
1345
+ """Initialize the requested indexes and out-of-bounds behavior.
1346
+
1347
+ Parameters
1348
+ ----------
1349
+ indexes : tuple of (int or range)
1350
+ One or more positional indexes to select.
1351
+ transforms : list of callable, optional
1352
+ Initial transform pipeline.
1353
+ strict_index_bounds : bool, default True
1354
+ Whether an out-of-bounds index raises (`True`) or is skipped
1355
+ (`False`).
1356
+ require_col_match : bool, default True
1357
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when
1358
+ zero columns match (only reachable when `strict_index_bounds` is
1359
+ `False`, or via a combined `~ - & ^ |` resolver). Forwarded to
1360
+ `BaseSelector.__init__`.
1361
+ """
1362
+
1363
+ super().__init__(transforms, require_col_match=require_col_match)
1364
+
1365
+ self.indexes = indexes
1366
+ self.strict_index_bounds = strict_index_bounds
1367
+
1368
+ @staticmethod
1369
+ def _positions_from(item: int | range) -> list[int]:
1370
+ """Expand one raw index arg (an int, or a range) into a flat list of ints.
1371
+
1372
+ Parameters
1373
+ ----------
1374
+ item : int or range
1375
+ A single index, or a `range` covering a span of indexes.
1376
+
1377
+ Returns
1378
+ -------
1379
+ list of int
1380
+ ``[item]`` when `item` is a bare int, or ``list(item)`` when
1381
+ `item` is a `range`.
1382
+ """
1383
+
1384
+ if isinstance(item, range):
1385
+ return list(item)
1386
+
1387
+ return [item]
1388
+
1389
+ def resolve(self, df: DataFrame) -> list[str]:
1390
+ """Resolve to the column name(s) at every requested position on `df`.
1391
+
1392
+ Parameters
1393
+ ----------
1394
+ df : pyspark.sql.DataFrame
1395
+ The dataframe to resolve matched column names against.
1396
+
1397
+ Returns
1398
+ -------
1399
+ list of str
1400
+ Matched column names, in first-seen (requested) order, with
1401
+ duplicate positions removed.
1402
+
1403
+ Raises
1404
+ ------
1405
+ pyspark.errors.PySparkValueError
1406
+ If `self.strict_index_bounds` is `True` and any requested index is out
1407
+ of range for `df`'s column count. Also raised by
1408
+ `_check_matched` (via `BaseSelector`) if zero columns match and
1409
+ `self.require_col_match` is `True`.
1410
+
1411
+ Examples
1412
+ --------
1413
+ >>> IndexSelector((0, -1)).resolve(df)
1414
+ ['string_col', 'integer_col']
1415
+ """
1416
+
1417
+ # if a set-operation (~ - & ^ |) built a combined resolver via
1418
+ # _selector_copy, honor it first -- same pattern as DTypeSelector.resolve.
1419
+ if self._resolver:
1420
+ return self._check_matched(self._resolver(df))
1421
+
1422
+ columns = df.columns
1423
+ n = len(columns)
1424
+
1425
+ seen = set()
1426
+ ordered_positions = []
1427
+
1428
+ for item in self.indexes:
1429
+ for pos in self._positions_from(item):
1430
+ actual = pos if pos >= 0 else n + pos
1431
+
1432
+ if actual < 0 or actual >= n:
1433
+ if self.strict_index_bounds:
1434
+ raise PySparkValueError(
1435
+ errorClass="VALUE_OUT_OF_BOUNDS",
1436
+ messageParameters={
1437
+ "arg_name": "by_index() column index",
1438
+ "lower_bound": str(-n),
1439
+ "upper_bound": str(n - 1),
1440
+ "actual": str(pos),
1441
+ },
1442
+ )
1443
+
1444
+ continue
1445
+
1446
+ if actual not in seen:
1447
+ seen.add(actual)
1448
+ ordered_positions.append(actual)
1449
+
1450
+ return self._check_matched([columns[p] for p in ordered_positions])
1451
+
1452
+
1453
+ # --------------------------------------------------------------------------------
1454
+ # create column selector that can select column's by their column name regex pattern
1455
+ # --------------------------------------------------------------------------------
1456
+
1457
+
1458
+ class RegexSelector(BaseSelector):
1459
+ """Select columns whose name matches a regex pattern.
1460
+
1461
+ pyspark has a native regex column selector, `pyspark.sql.DataFrame.colRegex`
1462
+ -- but it has to be called directly off a `DataFrame` instance
1463
+ (``df.colRegex(...)``, not a free-standing selector built ahead of time),
1464
+ and it returns a single opaque `pyspark.sql.Column` representing "all
1465
+ matching columns" without ever exposing which real column *names*
1466
+ matched. Every override in this notebook (`select`, `agg`, `sort`,
1467
+ `withColumns`, etc, via `_resolve_selector_exprs`) depends on getting back
1468
+ real ``(name, expr)`` pairs so a chained transform (``.cast(...)``,
1469
+ ``.desc()``, ``.upper()``, ...) can be applied per matched column and the
1470
+ result still knows what to call each output column -- `colRegex` can't
1471
+ give us that, so `resolve` instead matches each real column name in
1472
+ `df.columns` against the pattern directly, via `re.search`.
1473
+
1474
+ Parameters
1475
+ ----------
1476
+ pattern : str
1477
+ A regular expression, matched against each column name via
1478
+ `re.search` (not anchored -- matches anywhere in the name unless the
1479
+ pattern itself anchors with ``^``/``$``).
1480
+ transforms : list of callable, optional
1481
+ Initial transform pipeline, forwarded to `BaseSelector.__init__`.
1482
+ require_col_match : bool, default True
1483
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when zero
1484
+ columns match `pattern`.
1485
+
1486
+ Attributes
1487
+ ----------
1488
+ pattern : str
1489
+ The requested regex pattern, as given.
1490
+
1491
+ Notes
1492
+ -----
1493
+ ``~ - & ^ |`` all work already, same reasoning as `IndexSelector`.
1494
+
1495
+ Examples
1496
+ --------
1497
+ >>> df.select(matches(r"_id$"))
1498
+ DataFrame[...]
1499
+ >>> df.select(matches(r"_id$").upper())
1500
+ DataFrame[...]
1501
+ """
1502
+
1503
+ def __init__(
1504
+ self,
1505
+ pattern: str,
1506
+ transforms: list[Callable[[Column], Column]] | None = None,
1507
+ require_col_match: bool = True,
1508
+ ) -> None:
1509
+ """Initialize the requested regex pattern.
1510
+
1511
+ Parameters
1512
+ ----------
1513
+ pattern : str
1514
+ A regular expression, matched against each column name.
1515
+ transforms : list of callable, optional
1516
+ Initial transform pipeline.
1517
+ require_col_match : bool, default True
1518
+ Whether `resolve` raises `pyspark.errors.PySparkValueError` when
1519
+ zero columns match `pattern`. Forwarded to `BaseSelector.__init__`.
1520
+ """
1521
+
1522
+ super().__init__(transforms, require_col_match=require_col_match)
1523
+
1524
+ self.pattern = pattern
1525
+
1526
+ def resolve(self, df: DataFrame) -> list[str]:
1527
+ """Resolve to every column name on `df` matching `self.pattern`.
1528
+
1529
+ Parameters
1530
+ ----------
1531
+ df : pyspark.sql.DataFrame
1532
+ The dataframe to resolve matched column names against.
1533
+
1534
+ Returns
1535
+ -------
1536
+ list of str
1537
+ Matched column names, in dataframe-column order.
1538
+
1539
+ Raises
1540
+ ------
1541
+ pyspark.errors.PySparkValueError
1542
+ Raised by `_check_matched` (via `BaseSelector`) if zero columns
1543
+ match and `self.require_col_match` is `True`.
1544
+
1545
+ Examples
1546
+ --------
1547
+ >>> RegexSelector(r"_id$").resolve(df)
1548
+ ['string_col', 'data_column_5']
1549
+ """
1550
+
1551
+ # if a set-operation (~ - & ^ |) built a combined resolver via
1552
+ # _selector_copy, honor it first -- same pattern as DTypeSelector.resolve.
1553
+ if self._resolver:
1554
+ return self._check_matched(self._resolver(df))
1555
+
1556
+ # case-insensitive by design: pyspark's own column-name resolution
1557
+ # (`df.col_name`, `df['Col_Name']`, `df.select('col_name')`, joins,
1558
+ # ...) is case-insensitive by default, so a case-sensitive regex here
1559
+ # would silently miss columns a plain pyspark reference would still
1560
+ # find. re.IGNORECASE keeps this selector's matching behavior
1561
+ # consistent with that.
1562
+ return self._check_matched(
1563
+ [dim for dim in df.columns if re.search(self.pattern, dim, flags=re.IGNORECASE) is not None]
1564
+ )