Skip to content

Commit 44ddbc2

Browse files
feat(python): Enable creation of independently reusable Config instances (#20053)
1 parent ccaf682 commit 44ddbc2

File tree

5 files changed

+210
-34
lines changed

5 files changed

+210
-34
lines changed

py-polars/docs/source/reference/config.rst

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,37 @@ explicitly calling one or more of the available "set\_" methods on it...
8383
Use as a decorator
8484
------------------
8585

86-
In the same vein, you can also use ``Config`` as a function decorator to
87-
temporarily set options for the duration of the function call:
86+
In the same vein, you can also use a ``Config`` instance as a function decorator
87+
to temporarily set options for the duration of the function call:
8888

8989
.. code-block:: python
9090
91-
@pl.Config(set_ascii_tables=True)
92-
def write_ascii_frame_to_stdout(df: pl.DataFrame) -> None:
91+
cfg_ascii_frames = pl.Config(ascii_tables=True, apply_on_context_enter=True)
92+
93+
@cfg_ascii_frames
94+
def write_markdown_frame_to_stdout(df: pl.DataFrame) -> None:
9395
sys.stdout.write(str(df))
96+
97+
Multiple Config instances
98+
-------------------------
99+
You may want to establish related bundles of `Config` options for use in different
100+
parts of your code. Usually options are set immediately on `Config` init, meaning
101+
the `Config` instance cannot be reused; however, you can defer this so that options
102+
are only invoked when entering context scope (which includes function entry if used
103+
as a decorator)._
104+
105+
This allows you to create multiple *reusable* `Config` instances in one place, update
106+
and modify them centrally, and apply them as needed throughout your codebase.
107+
108+
.. code-block:: python
109+
110+
cfg_verbose = pl.Config(verbose=True, apply_on_context_enter=True)
111+
cfg_markdown = pl.Config(tbl_formatting="MARKDOWN", apply_on_context_enter=True)
112+
113+
@cfg_markdown
114+
def write_markdown_frame_to_stdout(df: pl.DataFrame) -> None:
115+
sys.stdout.write(str(df))
116+
117+
@cfg_verbose
118+
def do_various_things():
119+
...

py-polars/polars/api.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ def register_expr_namespace(name: str) -> Callable[[type[NS]], type[NS]]:
7676
7777
See Also
7878
--------
79-
register_dataframe_namespace: Register functionality on a DataFrame.
80-
register_lazyframe_namespace: Register functionality on a LazyFrame.
81-
register_series_namespace: Register functionality on a Series.
79+
register_dataframe_namespace : Register functionality on a DataFrame.
80+
register_lazyframe_namespace : Register functionality on a LazyFrame.
81+
register_series_namespace : Register functionality on a Series.
8282
8383
Examples
8484
--------
@@ -129,9 +129,9 @@ def register_dataframe_namespace(name: str) -> Callable[[type[NS]], type[NS]]:
129129
130130
See Also
131131
--------
132-
register_expr_namespace: Register functionality on an Expr.
133-
register_lazyframe_namespace: Register functionality on a LazyFrame.
134-
register_series_namespace: Register functionality on a Series.
132+
register_expr_namespace : Register functionality on an Expr.
133+
register_lazyframe_namespace : Register functionality on a LazyFrame.
134+
register_series_namespace : Register functionality on a Series.
135135
136136
Examples
137137
--------
@@ -227,9 +227,9 @@ def register_lazyframe_namespace(name: str) -> Callable[[type[NS]], type[NS]]:
227227
228228
See Also
229229
--------
230-
register_expr_namespace: Register functionality on an Expr.
231-
register_dataframe_namespace: Register functionality on a DataFrame.
232-
register_series_namespace: Register functionality on a Series.
230+
register_expr_namespace : Register functionality on an Expr.
231+
register_dataframe_namespace : Register functionality on a DataFrame.
232+
register_series_namespace : Register functionality on a Series.
233233
234234
Examples
235235
--------
@@ -328,9 +328,9 @@ def register_series_namespace(name: str) -> Callable[[type[NS]], type[NS]]:
328328
329329
See Also
330330
--------
331-
register_expr_namespace: Register functionality on an Expr.
332-
register_dataframe_namespace: Register functionality on a DataFrame.
333-
register_lazyframe_namespace: Register functionality on a LazyFrame.
331+
register_expr_namespace : Register functionality on an Expr.
332+
register_dataframe_namespace : Register functionality on a DataFrame.
333+
register_lazyframe_namespace : Register functionality on a LazyFrame.
334334
335335
Examples
336336
--------

py-polars/polars/config.py

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,15 @@ class Config(contextlib.ContextDecorator):
171171
... pass
172172
"""
173173

174+
_context_options: ConfigParameters | None = None
174175
_original_state: str = ""
175176

176177
def __init__(
177-
self, *, restore_defaults: bool = False, **options: Unpack[ConfigParameters]
178+
self,
179+
*,
180+
restore_defaults: bool = False,
181+
apply_on_context_enter: bool = False,
182+
**options: Unpack[ConfigParameters],
178183
) -> None:
179184
"""
180185
Initialise a Config object instance for context manager usage.
@@ -187,12 +192,19 @@ def __init__(
187192
restore_defaults
188193
set all options to their default values (this is applied before
189194
setting any other options).
195+
apply_on_context_enter
196+
defer applying the options until a context is entered. This allows you
197+
to create multiple `Config` instances with different options, and then
198+
reuse them independently as context managers or function decorators
199+
with specific bundles of parameters.
190200
**options
191201
keyword args that will set the option; equivalent to calling the
192202
named "set_<option>" method with the given value.
193203
194204
Examples
195205
--------
206+
Customise Polars table formatting while in context scope:
207+
196208
>>> df = pl.DataFrame({"abc": [1.0, 2.5, 5.0], "xyz": [True, False, True]})
197209
>>> with pl.Config(
198210
... # these options will be set for scope duration
@@ -208,24 +220,51 @@ def __init__(
208220
| 1.0 | true |
209221
| 2.5 | false |
210222
| 5.0 | true |
223+
224+
Establish several independent Config instances for use in different contexts;
225+
setting `apply_on_context_enter=True` defers setting the parameters until a
226+
context (or function, when used as a decorator) is actually entered:
227+
228+
>>> cfg_polars_verbose = pl.Config(
229+
... verbose=True,
230+
... apply_on_context_enter=True,
231+
... )
232+
>>> cfg_polars_detailed_tables = pl.Config(
233+
... tbl_rows=25,
234+
... tbl_cols=25,
235+
... tbl_width_chars=200,
236+
... apply_on_context_enter=True,
237+
... )
238+
239+
These Config instances can now be applied independently and re-used:
240+
241+
>>> @cfg_polars_verbose
242+
... def traced_function(df: pl.DataFrame) -> pl.DataFrame:
243+
... return polars_operations(df)
244+
245+
>>> @cfg_polars_detailed_tables
246+
... def print_detailed_frames(*frames: pl.DataFrame) -> None:
247+
... for df in frames:
248+
... print(df)
211249
"""
212250
# save original state _before_ any changes are made
213251
self._original_state = self.save()
214-
215252
if restore_defaults:
216253
self.restore_defaults()
217254

218-
for opt, value in options.items():
219-
if not hasattr(self, opt) and not opt.startswith("set_"):
220-
opt = f"set_{opt}"
221-
if not hasattr(self, opt):
222-
msg = f"`Config` has no option {opt!r}"
223-
raise AttributeError(msg)
224-
getattr(self, opt)(value)
255+
if apply_on_context_enter:
256+
# defer setting options; apply only on entering a new context
257+
self._context_options = options
258+
else:
259+
# apply the given options immediately
260+
self._set_config_params(**options)
261+
self._context_options = None
225262

226263
def __enter__(self) -> Self:
227-
"""Support setting temporary Config options that are reset on scope exit."""
264+
"""Support setting Config options that are reset on scope exit."""
228265
self._original_state = self._original_state or self.save()
266+
if self._context_options:
267+
self._set_config_params(**self._context_options)
229268
return self
230269

231270
def __exit__(
@@ -238,6 +277,25 @@ def __exit__(
238277
self.restore_defaults().load(self._original_state)
239278
self._original_state = ""
240279

280+
def __eq__(self, other: object) -> bool:
281+
if not isinstance(other, Config):
282+
return False
283+
return (self._original_state == other._original_state) and (
284+
self._context_options == other._context_options
285+
)
286+
287+
def __ne__(self, other: object) -> bool:
288+
return not self.__eq__(other)
289+
290+
def _set_config_params(self, **options: Unpack[ConfigParameters]) -> None:
291+
for opt, value in options.items():
292+
if not hasattr(self, opt) and not opt.startswith("set_"):
293+
opt = f"set_{opt}"
294+
if not hasattr(self, opt):
295+
msg = f"`Config` has no option {opt!r}"
296+
raise AttributeError(msg)
297+
getattr(self, opt)(value)
298+
241299
@classmethod
242300
def load(cls, cfg: str) -> Config:
243301
"""
@@ -251,7 +309,7 @@ def load(cls, cfg: str) -> Config:
251309
See Also
252310
--------
253311
load_from_file : Load (and set) Config options from a JSON file.
254-
save: Save the current set of Config options as a JSON string or file.
312+
save : Save the current set of Config options as a JSON string or file.
255313
"""
256314
try:
257315
options = json.loads(cfg)
@@ -285,7 +343,7 @@ def load_from_file(cls, file: Path | str) -> Config:
285343
See Also
286344
--------
287345
load : Load (and set) Config options from a JSON string.
288-
save: Save the current set of Config options as a JSON string or file.
346+
save : Save the current set of Config options as a JSON string or file.
289347
"""
290348
try:
291349
options = Path(normalize_filepath(file)).read_text()
@@ -389,7 +447,7 @@ def state(
389447
cls, *, if_set: bool = False, env_only: bool = False
390448
) -> dict[str, str | None]:
391449
"""
392-
Show the current state of all Config variables as a dict.
450+
Show the current state of all Config variables in the environment as a dict.
393451
394452
Parameters
395453
----------
@@ -422,7 +480,11 @@ def set_ascii_tables(cls, active: bool | None = True) -> type[Config]:
422480
"""
423481
Use ASCII characters to display table outlines.
424482
425-
Set False to revert to the default UTF8_FULL_CONDENSED formatting style.
483+
Set False to revert to the standard UTF8_FULL_CONDENSED formatting style.
484+
485+
See Also
486+
--------
487+
set_tbl_formatting : Set the table formatting style (includes Markdown option).
426488
427489
Examples
428490
--------
@@ -969,7 +1031,7 @@ def set_tbl_column_data_type_inline(
9691031
cls, active: bool | None = True
9701032
) -> type[Config]:
9711033
"""
972-
Moves the data type inline with the column name (to the right, in parentheses).
1034+
Display the data type next to the column name (to the right, in parentheses).
9731035
9741036
Examples
9751037
--------
@@ -1151,11 +1213,11 @@ def set_tbl_hide_column_names(cls, active: bool | None = True) -> type[Config]:
11511213
@classmethod
11521214
def set_tbl_hide_dtype_separator(cls, active: bool | None = True) -> type[Config]:
11531215
"""
1154-
Hide the '---' separator between the column names and column types.
1216+
Hide the '---' separator displayed between the column names and column types.
11551217
11561218
See Also
11571219
--------
1158-
set_tbl_column_data_type_inline
1220+
set_tbl_column_data_type_inline : Display the data type inline with the colname.
11591221
11601222
Examples
11611223
--------

py-polars/polars/dataframe/frame.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1525,7 +1525,7 @@ def item(self, row: int | None = None, column: int | str | None = None) -> Any:
15251525
15261526
See Also
15271527
--------
1528-
row: Get the values of a single row, either by index or by predicate.
1528+
row : Get the values of a single row, either by index or by predicate.
15291529
15301530
Notes
15311531
-----

py-polars/tests/unit/test_config.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,94 @@ def test_config_load_save_context() -> None:
724724
assert os.environ["POLARS_VERBOSE"]
725725

726726

727+
def test_config_instances() -> None:
728+
# establish two config instances that defer setting their options
729+
cfg_markdown = pl.Config(
730+
tbl_formatting="MARKDOWN",
731+
apply_on_context_enter=True,
732+
)
733+
cfg_compact = pl.Config(
734+
tbl_rows=4,
735+
tbl_cols=4,
736+
tbl_column_data_type_inline=True,
737+
apply_on_context_enter=True,
738+
)
739+
740+
# check instance (in)equality
741+
assert cfg_markdown != cfg_compact
742+
assert cfg_markdown == pl.Config(
743+
tbl_formatting="MARKDOWN", apply_on_context_enter=True
744+
)
745+
746+
# confirm that the options have not been applied yet
747+
assert os.environ.get("POLARS_FMT_TABLE_FORMATTING") is None
748+
749+
# confirm that the deferred options are applied when the instance context
750+
# is entered into, and that they can be re-used without leaking state
751+
@cfg_markdown
752+
def fn1() -> str | None:
753+
return os.environ.get("POLARS_FMT_TABLE_FORMATTING")
754+
755+
assert fn1() == "MARKDOWN"
756+
assert os.environ.get("POLARS_FMT_TABLE_FORMATTING") is None
757+
758+
with cfg_markdown: # can re-use instance as decorator and context
759+
assert os.environ.get("POLARS_FMT_TABLE_FORMATTING") == "MARKDOWN"
760+
assert os.environ.get("POLARS_FMT_TABLE_FORMATTING") is None
761+
762+
@cfg_markdown
763+
def fn2() -> str | None:
764+
return os.environ.get("POLARS_FMT_TABLE_FORMATTING")
765+
766+
assert fn2() == "MARKDOWN"
767+
assert os.environ.get("POLARS_FMT_TABLE_FORMATTING") is None
768+
769+
df = pl.DataFrame({f"c{idx}": [idx] * 10 for idx in range(10)})
770+
771+
@cfg_compact
772+
def fn3(df: pl.DataFrame) -> str:
773+
return repr(df)
774+
775+
# reuse config instance and confirm state does not leak between invocations
776+
for _ in range(3):
777+
assert (
778+
fn3(df)
779+
== dedent("""
780+
shape: (10, 10)
781+
┌──────────┬──────────┬───┬──────────┬──────────┐
782+
│ c0 (i64) ┆ c1 (i64) ┆ … ┆ c8 (i64) ┆ c9 (i64) │
783+
╞══════════╪══════════╪═══╪══════════╪══════════╡
784+
│ 0 ┆ 1 ┆ … ┆ 8 ┆ 9 │
785+
│ 0 ┆ 1 ┆ … ┆ 8 ┆ 9 │
786+
│ … ┆ … ┆ … ┆ … ┆ … │
787+
│ 0 ┆ 1 ┆ … ┆ 8 ┆ 9 │
788+
│ 0 ┆ 1 ┆ … ┆ 8 ┆ 9 │
789+
└──────────┴──────────┴───┴──────────┴──────────┘""").lstrip()
790+
)
791+
792+
assert (
793+
repr(df)
794+
== dedent("""
795+
shape: (10, 10)
796+
┌─────┬─────┬─────┬─────┬───┬─────┬─────┬─────┬─────┐
797+
│ c0 ┆ c1 ┆ c2 ┆ c3 ┆ … ┆ c6 ┆ c7 ┆ c8 ┆ c9 │
798+
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
799+
│ i64 ┆ i64 ┆ i64 ┆ i64 ┆ ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
800+
╞═════╪═════╪═════╪═════╪═══╪═════╪═════╪═════╪═════╡
801+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
802+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
803+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
804+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
805+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
806+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
807+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
808+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
809+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
810+
│ 0 ┆ 1 ┆ 2 ┆ 3 ┆ … ┆ 6 ┆ 7 ┆ 8 ┆ 9 │
811+
└─────┴─────┴─────┴─────┴───┴─────┴─────┴─────┴─────┘""").lstrip()
812+
)
813+
814+
727815
def test_config_scope() -> None:
728816
pl.Config.set_verbose(False)
729817
pl.Config.set_tbl_cols(8)

0 commit comments

Comments
 (0)