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,298 @@
1
+ """non-column selector imports"""
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any
5
+
6
+ from pyspark.sql import DataFrame
7
+ from pyspark.sql import functions as F
8
+ from pyspark.sql.column import Column
9
+
10
+ """ enable support for many different types of pyspark environments"""
11
+ # if your environment isn't supported then you can add it to this section.
12
+ # Spark supports multiple DataFrame implementations. Keep every available runtime
13
+ # class in the patch set: public/classic DataFrame and Spark Connect DataFrame.
14
+ # using try except as to allow passthrough if environment doesn't support these.
15
+ try:
16
+ from pyspark.sql.classic.dataframe import DataFrame as _ClassicDataFrame
17
+ except ImportError:
18
+ _ClassicDataFrame = None
19
+
20
+ try:
21
+ from pyspark.sql.connect.dataframe import DataFrame as _ConnectDataFrame
22
+ except ImportError:
23
+ _ConnectDataFrame = None
24
+
25
+ _DATAFRAME_CLASSES = tuple(
26
+ dict.fromkeys(cls for cls in (DataFrame, _ClassicDataFrame, _ConnectDataFrame) if cls is not None)
27
+ )
28
+
29
+ try:
30
+ from pyspark.sql.connect.column import Column as _ConnectColumn
31
+ except ImportError:
32
+ _ConnectColumn = None
33
+
34
+ _COLUMN_CLASSES = tuple(dict.fromkeys(cls for cls in (Column, _ConnectColumn) if cls is not None))
35
+
36
+ # --------------------------------------------------
37
+ # return selector names
38
+ # create column names for functions that require columns to have them.
39
+ # --------------------------------------------------
40
+
41
+
42
+ def _resolve_selector_names(df: DataFrame, args: Sequence[Any]) -> list[Any]:
43
+ """Turn any `BaseSelector` in `args` into the plain column names it matches.
44
+
45
+ Shared resolver #1 -- used by the fx overrides that only need bare names
46
+ (`groupBy`, `drop`), the same way `select_patched` originally resolved
47
+ selectors for `select`.
48
+
49
+ Parameters
50
+ ----------
51
+ df : pyspark.sql.DataFrame
52
+ The dataframe to resolve any selector's matched column names against.
53
+ args : sequence of Any
54
+ The positional arguments originally passed to the overridden pyspark
55
+ method (e.g. `DataFrame.groupBy`'s `*cols`). Any `BaseSelector` is
56
+ expanded to the names it matches; anything else passes through
57
+ untouched.
58
+
59
+ Returns
60
+ -------
61
+ list of Any
62
+ `args`, with every `BaseSelector` expanded into its matched column
63
+ name strings, in order.
64
+
65
+ Examples
66
+ --------
67
+ >>> _resolve_selector_names(df, (by_dtype([T.StringType]),))
68
+ ['string_col', 'data_column_6']
69
+ """
70
+ from .models import BaseSelector
71
+
72
+ resolved = []
73
+
74
+ for a in args:
75
+ if isinstance(a, BaseSelector):
76
+ # a resolved name may legally contain a literal dot (see
77
+ # `_quote_identifier`) -- these names get handed straight to a
78
+ # real pyspark method expecting bare name strings (`groupBy`,
79
+ # `drop`), which -- same as calling that method directly with a
80
+ # dotted name -- would otherwise misparse an un-escaped dot as
81
+ # struct-path access. quote defensively here since these names
82
+ # came from our own selector resolution, not the caller's
83
+ # original input.
84
+ resolved.extend(_quote_identifier(name) for name in a.resolve(df))
85
+ else:
86
+ resolved.append(a)
87
+
88
+ return resolved
89
+
90
+
91
+ # --------------------------------------------------
92
+ # return selector expressions
93
+ # create column expressions for functions that require columns to have them.
94
+ # --------------------------------------------------
95
+
96
+
97
+ def _resolve_selector_exprs(df: DataFrame, args: Sequence[Any]) -> list[tuple[str | None, Any]]:
98
+ """Turn any `BaseSelector` in `args` into `(name, Column expr)` pairs.
99
+
100
+ Shared resolver #2 -- applies whatever transforms are chained onto the
101
+ selector (``.sum()``, ``.cast(...)``, ``+1``, ``.desc()``, etc, all
102
+ already available via `SelectorcolumnOperations`). A selector with no
103
+ transforms resolves to plain `F.col(name)` expressions.
104
+
105
+ Parameters
106
+ ----------
107
+ df : pyspark.sql.DataFrame
108
+ The dataframe to resolve any selector's matched columns against.
109
+ args : sequence of Any
110
+ The positional arguments originally passed to the overridden pyspark
111
+ method. Any `BaseSelector` is expanded to `(name, expr)` pairs for
112
+ every column it matches; anything else passes through as
113
+ ``(None, value)``.
114
+
115
+ Returns
116
+ -------
117
+ list of tuple of (str or None, Any)
118
+ One `(name, expr)` pair per resolved column/argument, in order. When
119
+ a rename (`.prefix()`/`.suffix()`/`.map_alias()`) has been queued on
120
+ the selector (`self._name_transform`), `name` is the *new* name
121
+ computed by applying it to the originally-matched column name,
122
+ instead of the originally-matched name itself -- harmless when a
123
+ caller re-aliases to it (it's already the correct name), and
124
+ required by callers like `withColumns_patched` that use `name` as
125
+ the output column's real key.
126
+
127
+ Examples
128
+ --------
129
+ >>> _resolve_selector_exprs(df, (by_dtype([T.IntegerType]) + 1,))
130
+ [('integer_col', Column<'(integer_col + 1)'>)]
131
+ >>> _resolve_selector_exprs(df, (by_dtype([T.IntegerType]).prefix('int_'),))
132
+ [('int_integer_col', Column<'integer_col'>)]
133
+ """
134
+ from .models import BaseSelector
135
+
136
+ pairs = []
137
+
138
+ for a in args:
139
+ if isinstance(a, BaseSelector):
140
+ names = a.resolve(df)
141
+ exprs = a.resolve_columns(df) if a.transforms else [_quoted_col(n) for n in names]
142
+
143
+ # a queued rename (.prefix()/.suffix()/.map_alias()) is pure name
144
+ # metadata -- apply it here to each originally-matched name to get
145
+ # the real output name, without ever having touched the Column
146
+ # expression itself (so it can't be corrupted by whatever value
147
+ # transform text, e.g. .cast(...)/arithmetic, is baked into `expr`).
148
+ if a._name_transform is not None:
149
+ pairs.extend((a._name_transform(name), expr) for name, expr in zip(names, exprs, strict=True))
150
+ else:
151
+ pairs.extend(zip(names, exprs, strict=True))
152
+ else:
153
+ pairs.append((None, a))
154
+
155
+ return pairs
156
+
157
+
158
+ # --------------------------------------------------------------------------------
159
+ # support for edge case column selection, including support for <.>, <`> in column names
160
+ # --------------------------------------------------------------------------------
161
+
162
+
163
+ def _quote_identifier(name: str) -> str:
164
+ """Backtick-quote `name` if needed so pyspark treats it as one flat identifier.
165
+
166
+ Parameters
167
+ ----------
168
+ name : str
169
+ A real, flat column name (as returned by `BaseSelector.resolve`),
170
+ which may legally contain a literal dot or backtick.
171
+
172
+ Returns
173
+ -------
174
+ str
175
+ `name` unchanged if it has no dot/backtick, otherwise the
176
+ backtick-quoted form (with any embedded backtick doubled, Spark
177
+ SQL's own backtick-escaping convention).
178
+
179
+ Examples
180
+ --------
181
+ >>> _quote_identifier('integer_col')
182
+ 'integer_col'
183
+ >>> _quote_identifier('meta.source')
184
+ '`meta.source`'
185
+ """
186
+
187
+ if "." in name or "`" in name:
188
+ escaped = name.replace("`", "``")
189
+ return f"`{escaped}`"
190
+
191
+ return name
192
+
193
+
194
+ def _quoted_col(name: str) -> Column:
195
+ """Build `F.col(name)`, backtick-quoting `name` if needed to keep it flat.
196
+
197
+ Parameters
198
+ ----------
199
+ name : str
200
+ A real, flat column name (as returned by `BaseSelector.resolve`),
201
+ which may legally contain a literal dot or backtick.
202
+
203
+ Returns
204
+ -------
205
+ pyspark.sql.Column
206
+ `F.col` built from `_quote_identifier(name)`, so pyspark always
207
+ resolves it as one flat column name instead of a struct-path
208
+ reference.
209
+
210
+ Examples
211
+ --------
212
+ >>> _quoted_col('integer_col')
213
+ Column<'integer_col'>
214
+ >>> _quoted_col('meta.source')
215
+ Column<'meta.source'>
216
+ """
217
+
218
+ return F.col(_quote_identifier(name))
219
+
220
+
221
+ # --------------------------------------------------
222
+ # allow for checking for columns across different types of pyspark dataframes
223
+ # spark can have multiple different types this handles the changes.
224
+ # --------------------------------------------------
225
+
226
+
227
+ def _get_column_method(method_name: str) -> Callable[..., Any]:
228
+ """Look up a `Column` instance method, checking classic then Spark Connect.
229
+
230
+ Used wherever a method is looked up purely for introspection (its
231
+ `__doc__`, or an existence/callability check) rather than to be bound and
232
+ called directly -- e.g. by `_make_column_only_wrapper`/
233
+ `_make_dispatching_column_wrapper` when building a chained selector
234
+ method. Falls back to Spark Connect's `Column` when the classic
235
+ `pyspark.sql.Column` doesn't define `method_name` at all, so a
236
+ Connect-only method still gets picked up instead of silently never
237
+ getting a selector-chainable wrapper.
238
+
239
+ Parameters
240
+ ----------
241
+ method_name : str
242
+ Name of the `Column` instance method to look up.
243
+
244
+ Returns
245
+ -------
246
+ callable
247
+ The method, from whichever of classic/Connect `Column` actually
248
+ defines it (classic checked first).
249
+
250
+ Raises
251
+ ------
252
+ AttributeError
253
+ If neither classic nor (when available) Spark Connect's `Column`
254
+ defines `method_name`.
255
+ """
256
+ if hasattr(Column, method_name):
257
+ return getattr(Column, method_name)
258
+ return getattr(_ConnectColumn, method_name)
259
+
260
+
261
+ # --------------------------------------------------
262
+ # protect against re-overriding the same functions on packages initialization.
263
+ # --------------------------------------------------
264
+
265
+
266
+ def _true_original(cls: type, method_name: str) -> Callable[..., Any]:
267
+ """Re-run-safe replacement for ``getattr(cls, method_name)`` when capturing an original method.
268
+
269
+ Every override in this notebook does ``_original_x = <the real pyspark
270
+ method>`` once, then reassigns ``SomeClass.method = patched_version``.
271
+ That's fine the first time a cell runs -- but if the SAME cell gets
272
+ re-run (very normal during interactive experimentation),
273
+ ``getattr(cls, method_name)`` on the second run fetches the
274
+ *already-patched* version instead of the real pyspark one, and the newly
275
+ defined patched function ends up calling itself forever the moment it's
276
+ invoked -- causing infinite recursion.
277
+
278
+ Parameters
279
+ ----------
280
+ cls : type
281
+ The class the method lives on (e.g. `pyspark.sql.DataFrame`,
282
+ `pyspark.sql.group.GroupedData`).
283
+ method_name : str
284
+ The name of the method to fetch the true original implementation of.
285
+
286
+ Returns
287
+ -------
288
+ callable
289
+ The true, never-patched method. Every patched function this returns
290
+ the original for gets tagged with ``._cs_original`` pointing at the
291
+ true, never-patched method -- so on a re-run, this peels back
292
+ through any already-applied patch layer(s) and always hands back the
293
+ one real pyspark implementation, no matter how many times the cell
294
+ is executed.
295
+ """
296
+ current = getattr(cls, method_name)
297
+
298
+ return getattr(current, "_cs_original", current)