diff --git a/docs/development/versioning.md b/docs/development/versioning.md index 4354a4f3ad00..d57bb92fd459 100644 --- a/docs/development/versioning.md +++ b/docs/development/versioning.md @@ -47,6 +47,22 @@ Examples of changes that are _not_ considered breaking: Bug fixes are not considered a breaking change, even though it may impact some users' [workflows](https://xkcd.com/1172/). +### Unstable functionality + +Some parts of the public API are marked as **unstable**. +You can recognize this functionality from the warning in the API reference, or from the warning issued when the configuration option `warn_unstable` is active. +There are a number of reasons functionality may be marked as unstable: + +- We are unsure about the exact API. The name, function signature, or implementation are likely to change in the future. +- The functionality is not tested extensively yet. Bugs may pop up when used in real-world scenarios. +- The functionality does not integrate well with the full Polars API. You may find it works in one context but not in another. + +Releasing functionality as unstable allows us to gather important feedback from users that use Polars in real-world scenarios. +This helps us fine-tune things before giving it the final stamp of approval. +Users are only interested in solid, well-tested functionality can avoid this part of the API. + +Functionality marked as unstable may change at any point without it being considered a breaking change. + ### Deprecation warnings If we decide to introduce a breaking change, the existing behavior is deprecated _if possible_. diff --git a/py-polars/polars/__init__.py b/py-polars/polars/__init__.py index 2ae0aacad752..e0de6cb683c7 100644 --- a/py-polars/polars/__init__.py +++ b/py-polars/polars/__init__.py @@ -85,6 +85,7 @@ SchemaFieldNotFoundError, ShapeError, StructFieldNotFoundError, + UnstableWarning, ) from polars.expr import Expr from polars.functions import ( @@ -221,7 +222,6 @@ "ArrowError", "ColumnNotFoundError", "ComputeError", - "ChronoFormatWarning", "DuplicateError", "InvalidOperationError", "NoDataError", @@ -235,6 +235,8 @@ # warnings "PolarsWarning", "CategoricalRemappingWarning", + "ChronoFormatWarning", + "UnstableWarning", # core classes "DataFrame", "Expr", diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 81e73199f932..889e1cf011b8 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -40,9 +40,10 @@ # note: register all Config-specific environment variable names here; need to constrain -# which 'POLARS_' environment variables are recognised, as there are other lower-level -# and/or experimental settings that should not be saved or reset with the Config vars. +# which 'POLARS_' environment variables are recognized, as there are other lower-level +# and/or unstable settings that should not be saved or reset with the Config vars. _POLARS_CFG_ENV_VARS = { + "POLARS_WARN_UNSTABLE", "POLARS_ACTIVATE_DECIMAL", "POLARS_AUTO_STRUCTIFY", "POLARS_FMT_MAX_COLS", @@ -1260,3 +1261,23 @@ def set_verbose(cls, active: bool | None = True) -> type[Config]: else: os.environ["POLARS_VERBOSE"] = str(int(active)) return cls + + @classmethod + def warn_unstable(cls, active: bool | None = True) -> type[Config]: + """ + Issue a warning when unstable functionality is used. + + Enabling this setting may help avoid functionality that is still evolving, + potentially reducing maintenance burden from API changes and bugs. + + Examples + -------- + >>> pl.Config.warn_unstable(True) # doctest: +SKIP + >>> pl.col("a").qcut(5) # doctest: +SKIP + UnstableWarning: `qcut` is considered unstable. It may be changed at any point without it being considered a breaking change. + """ # noqa: W505 + if active is None: + os.environ.pop("POLARS_WARN_UNSTABLE", None) + else: + os.environ["POLARS_WARN_UNSTABLE"] = str(int(active)) + return cls diff --git a/py-polars/polars/dataframe/frame.py b/py-polars/polars/dataframe/frame.py index f03de8b79b4f..ce489c9375c9 100644 --- a/py-polars/polars/dataframe/frame.py +++ b/py-polars/polars/dataframe/frame.py @@ -106,6 +106,7 @@ deprecate_saturating, issue_deprecation_warning, ) +from polars.utils.unstable import issue_unstable_warning, unstable from polars.utils.various import ( _prepare_row_index_args, _process_null_values, @@ -3272,11 +3273,13 @@ def write_ipc( compression : {'uncompressed', 'lz4', 'zstd'} Compression method. Defaults to "uncompressed". future - WARNING: this argument is unstable and will be removed without it being - considered a breaking change. - Setting this to `True` will write polars' internal data-structures that + Setting this to `True` will write Polars' internal data structures that might not be available by other Arrow implementations. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Examples -------- >>> import pathlib @@ -3300,6 +3303,11 @@ def write_ipc( if compression is None: compression = "uncompressed" + if future: + issue_unstable_warning( + "The `future` parameter of `DataFrame.write_ipc` is considered unstable." + ) + self._df.write_ipc(file, compression, future) return file if return_bytes else None # type: ignore[return-value] @@ -7549,6 +7557,7 @@ def melt( self._df.melt(id_vars, value_vars, value_name, variable_name) ) + @unstable() def unstack( self, step: int, @@ -7559,12 +7568,11 @@ def unstack( """ Unstack a long table to a wide form without doing an aggregation. - This can be much faster than a pivot, because it can skip the grouping phase. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. - Warnings - -------- - This functionality is experimental and may be subject to changes - without it being considered a breaking change. + This can be much faster than a pivot, because it can skip the grouping phase. Parameters ---------- @@ -10389,6 +10397,7 @@ def set_sorted( .collect(_eager=True) ) + @unstable() def update( self, other: DataFrame, @@ -10403,8 +10412,8 @@ def update( Update the values in this `DataFrame` with the values in `other`. .. warning:: - This functionality is experimental and may change without it being - considered a breaking change. + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. By default, null values in the right frame are ignored. Use `include_nulls=False` to overwrite values in this frame with diff --git a/py-polars/polars/datatypes/classes.py b/py-polars/polars/datatypes/classes.py index 00bcfecda4b5..d9e2fde8614c 100644 --- a/py-polars/polars/datatypes/classes.py +++ b/py-polars/polars/datatypes/classes.py @@ -337,7 +337,9 @@ class Decimal(NumericType): Decimal 128-bit type with an optional precision and non-negative scale. .. warning:: - This is an experimental work-in-progress feature and may not work as expected. + This functionality is considered **unstable**. + It is a work-in-progress feature and may not always work as expected. + It may be changed at any point without it being considered a breaking change. """ precision: int | None @@ -348,6 +350,15 @@ def __init__( precision: int | None = None, scale: int = 0, ): + # Issuing the warning on `__init__` does not trigger when the class is used + # without being instantiated, but it's better than nothing + from polars.utils.unstable import issue_unstable_warning + + issue_unstable_warning( + "The Decimal data type is considered unstable." + " It is a work-in-progress feature and may not always work as expected." + ) + self.precision = precision self.scale = scale @@ -528,7 +539,9 @@ class Enum(DataType): A fixed set categorical encoding of a set of strings. .. warning:: - This is an experimental work-in-progress feature and may not work as expected. + This functionality is considered **unstable**. + It is a work-in-progress feature and may not always work as expected. + It may be changed at any point without it being considered a breaking change. """ categories: Series @@ -542,6 +555,15 @@ def __init__(self, categories: Series | Iterable[str]): categories Valid categories in the dataset. """ + # Issuing the warning on `__init__` does not trigger when the class is used + # without being instantiated, but it's better than nothing + from polars.utils.unstable import issue_unstable_warning + + issue_unstable_warning( + "The Enum data type is considered unstable." + " It is a work-in-progress feature and may not always work as expected." + ) + if not isinstance(categories, pl.Series): categories = pl.Series(values=categories) diff --git a/py-polars/polars/exceptions.py b/py-polars/polars/exceptions.py index 608e1e07e6e5..702fb938f7d8 100644 --- a/py-polars/polars/exceptions.py +++ b/py-polars/polars/exceptions.py @@ -95,7 +95,7 @@ class UnsuitableSQLError(PolarsError): # type: ignore[misc] class ChronoFormatWarning(PolarsWarning): # type: ignore[misc] """ - Warning raised when a chrono format string contains dubious patterns. + Warning issued when a chrono format string contains dubious patterns. Polars uses Rust's chrono crate to convert between string data and temporal data. The patterns used by chrono differ slightly from Python's built-in datetime module. @@ -106,11 +106,15 @@ class ChronoFormatWarning(PolarsWarning): # type: ignore[misc] class PolarsInefficientMapWarning(PolarsWarning): # type: ignore[misc] - """Warning raised when a potentially slow `map_*` operation is performed.""" + """Warning issued when a potentially slow `map_*` operation is performed.""" class TimeZoneAwareConstructorWarning(PolarsWarning): # type: ignore[misc] - """Warning raised when constructing Series from non-UTC time-zone-aware inputs.""" + """Warning issued when constructing Series from non-UTC time-zone-aware inputs.""" + + +class UnstableWarning(PolarsWarning): # type: ignore[misc] + """Warning issued when unstable functionality is used.""" class ArrowError(Exception): diff --git a/py-polars/polars/expr/datetime.py b/py-polars/polars/expr/datetime.py index 5a64b7becbe6..3a73c83779e5 100644 --- a/py-polars/polars/expr/datetime.py +++ b/py-polars/polars/expr/datetime.py @@ -16,6 +16,7 @@ issue_deprecation_warning, rename_use_earliest_to_ambiguous, ) +from polars.utils.unstable import unstable if TYPE_CHECKING: from datetime import timedelta @@ -206,6 +207,7 @@ def truncate( ) ) + @unstable() def round( self, every: str | timedelta, @@ -216,6 +218,10 @@ def round( """ Divide the date/datetime range into buckets. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Each date/datetime in the first half of the interval is mapped to the start of its bucket. Each date/datetime in the second half of the interval @@ -241,6 +247,11 @@ def round( .. deprecated: 0.19.3 This is now auto-inferred, you can safely remove this argument. + Returns + ------- + Expr + Expression of data type :class:`Date` or :class:`Datetime`. + Notes ----- The `every` and `offset` argument are created with the @@ -260,21 +271,10 @@ def round( eg: 3d12h4m25s # 3 days, 12 hours, 4 minutes, and 25 seconds - By "calendar day", we mean the corresponding time on the next day (which may not be 24 hours, due to daylight savings). Similarly for "calendar week", "calendar month", "calendar quarter", and "calendar year". - Returns - ------- - Expr - Expression of data type :class:`Date` or :class:`Datetime`. - - Warnings - -------- - This functionality is currently experimental and may - change without it being considered a breaking change. - Examples -------- >>> from datetime import timedelta, datetime diff --git a/py-polars/polars/expr/expr.py b/py-polars/polars/expr/expr.py index 72af7f20bf09..de83d8da4da7 100644 --- a/py-polars/polars/expr/expr.py +++ b/py-polars/polars/expr/expr.py @@ -56,6 +56,7 @@ issue_deprecation_warning, ) from polars.utils.meta import threadpool_size +from polars.utils.unstable import issue_unstable_warning, unstable from polars.utils.various import ( no_default, sphinx_accessor, @@ -3597,6 +3598,7 @@ def quantile( quantile = parse_as_expression(quantile) return self._from_pyexpr(self._pyexpr.quantile(quantile, interpolation)) + @unstable() def cut( self, breaks: Sequence[float], @@ -3608,6 +3610,10 @@ def cut( """ Bin continuous values into discrete categories. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- breaks @@ -3675,6 +3681,7 @@ def cut( self._pyexpr.cut(breaks, labels, left_closed, include_breaks) ) + @unstable() def qcut( self, quantiles: Sequence[float] | int, @@ -3687,6 +3694,10 @@ def qcut( """ Bin continuous values into discrete categories based on their quantiles. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- quantiles @@ -4152,7 +4163,7 @@ def map_elements( pass_name Pass the Series name to the custom function (this is more expensive). strategy : {'thread_local', 'threading'} - This functionality is considered experimental and may be removed/changed. + The threading strategy to use. - 'thread_local': run the python function on a single thread. - 'threading': run the python function on separate threads. Use with @@ -4161,6 +4172,15 @@ def map_elements( and the python function releases the GIL (e.g. via calling a c function) + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + + Warnings + -------- + If `return_dtype` is not provided, this may lead to unexpected results. + We allow this, but it is considered a bug in the user's query. + Notes ----- * Using `map_elements` is strongly discouraged as you will be effectively @@ -4174,11 +4194,6 @@ def map_elements( * Window function application using `over` is considered a GroupBy context here, so `map_elements` can be used to map functions over window groups. - Warnings - -------- - If `return_dtype` is not provided, this may lead to unexpected results. - We allow this, but it is considered a bug in the user's query. - Examples -------- >>> df = pl.DataFrame( @@ -4287,6 +4302,11 @@ def map_elements( ... scaled=(pl.col("val") * pl.col("val").count()).over("key"), ... ).sort("key") # doctest: +IGNORE_RESULT """ + if strategy == "threading": + issue_unstable_warning( + "The 'threading' strategy for `map_elements` is considered unstable." + ) + # input x: Series of type list containing the group values from polars.utils.udfs import warn_on_inefficient_map @@ -5638,6 +5658,7 @@ def interpolate(self, method: InterpolationMethod = "linear") -> Self: """ return self._from_pyexpr(self._pyexpr.interpolate(method)) + @unstable() def rolling_min( self, window_size: int | timedelta | str, @@ -5652,6 +5673,10 @@ def rolling_min( """ Apply a rolling min (moving min) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their min. @@ -5720,12 +5745,6 @@ def rolling_min( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -5854,6 +5873,7 @@ def rolling_min( ) ) + @unstable() def rolling_max( self, window_size: int | timedelta | str, @@ -5868,6 +5888,10 @@ def rolling_max( """ Apply a rolling max (moving max) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their max. @@ -5932,12 +5956,6 @@ def rolling_max( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -6095,6 +6113,7 @@ def rolling_max( ) ) + @unstable() def rolling_mean( self, window_size: int | timedelta | str, @@ -6109,6 +6128,10 @@ def rolling_mean( """ Apply a rolling mean (moving mean) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their mean. @@ -6177,12 +6200,6 @@ def rolling_mean( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -6346,6 +6363,7 @@ def rolling_mean( ) ) + @unstable() def rolling_sum( self, window_size: int | timedelta | str, @@ -6360,6 +6378,10 @@ def rolling_sum( """ Apply a rolling sum (moving sum) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their sum. @@ -6424,12 +6446,6 @@ def rolling_sum( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -6587,6 +6603,7 @@ def rolling_sum( ) ) + @unstable() def rolling_std( self, window_size: int | timedelta | str, @@ -6602,6 +6619,10 @@ def rolling_std( """ Compute a rolling standard deviation. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + If `by` has not been specified (the default), the window at a given row will include the row itself, and the `window_size - 1` elements before it. @@ -6668,12 +6689,6 @@ def rolling_std( "Delta Degrees of Freedom": The divisor for a length N window is N - ddof warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -6838,6 +6853,7 @@ def rolling_std( ) ) + @unstable() def rolling_var( self, window_size: int | timedelta | str, @@ -6853,6 +6869,10 @@ def rolling_var( """ Compute a rolling variance. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + If `by` has not been specified (the default), the window at a given row will include the row itself, and the `window_size - 1` elements before it. @@ -6919,12 +6939,6 @@ def rolling_var( "Delta Degrees of Freedom": The divisor for a length N window is N - ddof warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -7089,6 +7103,7 @@ def rolling_var( ) ) + @unstable() def rolling_median( self, window_size: int | timedelta | str, @@ -7103,6 +7118,10 @@ def rolling_median( """ Compute a rolling median. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + If `by` has not been specified (the default), the window at a given row will include the row itself, and the `window_size - 1` elements before it. @@ -7167,12 +7186,6 @@ def rolling_median( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -7250,6 +7263,7 @@ def rolling_median( ) ) + @unstable() def rolling_quantile( self, quantile: float, @@ -7266,6 +7280,10 @@ def rolling_quantile( """ Compute a rolling quantile. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + If `by` has not been specified (the default), the window at a given row will include the row itself, and the `window_size - 1` elements before it. @@ -7334,12 +7352,6 @@ def rolling_quantile( applicable if `by` has been set. warn_if_unsorted Warn if data is not known to be sorted by `by` column (if passed). - Experimental. - - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. Notes ----- @@ -7453,10 +7465,15 @@ def rolling_quantile( ) ) + @unstable() def rolling_skew(self, window_size: int, *, bias: bool = True) -> Self: """ Compute a rolling skew. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + The window at a given row includes the row itself and the `window_size - 1` elements before it. @@ -7490,6 +7507,7 @@ def rolling_skew(self, window_size: int, *, bias: bool = True) -> Self: """ return self._from_pyexpr(self._pyexpr.rolling_skew(window_size, bias)) + @unstable() def rolling_map( self, function: Callable[[Series], Any], @@ -7503,8 +7521,8 @@ def rolling_map( Compute a custom rolling window function. .. warning:: - Computing custom functions is extremely slow. Use specialized rolling - functions such as :func:`Expr.rolling_sum` if at all possible. + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. Parameters ---------- @@ -7525,6 +7543,11 @@ def rolling_map( center Set the labels at the center of the window. + Warnings + -------- + Computing custom functions is extremely slow. Use specialized rolling + functions such as :func:`Expr.rolling_sum` if at all possible. + Examples -------- >>> from numpy import nansum @@ -8974,12 +8997,17 @@ def entropy(self, base: float = math.e, *, normalize: bool = True) -> Self: """ return self._from_pyexpr(self._pyexpr.entropy(base, normalize)) + @unstable() def cumulative_eval( self, expr: Expr, min_periods: int = 1, *, parallel: bool = False ) -> Self: """ Run an expression over a sliding window that increases `1` slot every iteration. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- expr @@ -8993,9 +9021,6 @@ def cumulative_eval( Warnings -------- - This functionality is experimental and may change without it being considered a - breaking change. - This can be really slow as it can have `O(n^2)` complexity. Don't use this for operations that visit all elements. @@ -9091,6 +9116,7 @@ def shrink_dtype(self) -> Self: """ return self._from_pyexpr(self._pyexpr.shrink_dtype()) + @unstable() def hist( self, bins: IntoExpr | None = None, @@ -9102,6 +9128,10 @@ def hist( """ Bin values into buckets and count their occurrences. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- bins @@ -9119,11 +9149,6 @@ def hist( ------- DataFrame - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. - Examples -------- >>> df = pl.DataFrame({"a": [1, 3, 8, 8, 2, 1, 3]}) diff --git a/py-polars/polars/functions/lazy.py b/py-polars/polars/functions/lazy.py index e723ca8c43fb..6aa675f444cc 100644 --- a/py-polars/polars/functions/lazy.py +++ b/py-polars/polars/functions/lazy.py @@ -17,6 +17,7 @@ deprecate_renamed_function, issue_deprecation_warning, ) +from polars.utils.unstable import issue_unstable_warning, unstable with contextlib.suppress(ImportError): # Module not available when building docs import polars.polars as plr @@ -1635,7 +1636,17 @@ def collect_all( comm_subexpr_elim Common subexpressions will be cached and reused. streaming - Run parts of the query in a streaming fashion (this is in an alpha state) + Process the query in batches to handle larger-than-memory data. + If set to `False` (default), the entire query is processed in a single + batch. + + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + + .. note:: + Use :func:`explain` to see if Polars can process the query in streaming + mode. Returns ------- @@ -1649,6 +1660,10 @@ def collect_all( comm_subplan_elim = False comm_subexpr_elim = False + if streaming: + issue_unstable_warning("Streaming mode is considered unstable.") + comm_subplan_elim = False + prepared = [] for lf in lazy_frames: @@ -1709,6 +1724,7 @@ def collect_all_async( ... +@unstable() def collect_all_async( lazy_frames: Iterable[LazyFrame], *, @@ -1726,6 +1742,10 @@ def collect_all_async( """ Collect multiple LazyFrames at the same time asynchronously in thread pool. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Collects into a list of DataFrame (like :func:`polars.collect_all`), but instead of returning them directly, they are scheduled to be collected inside thread pool, while this method returns almost instantly. @@ -1756,22 +1776,27 @@ def collect_all_async( comm_subexpr_elim Common subexpressions will be cached and reused. streaming - Run parts of the query in a streaming fashion (this is in an alpha state) + Process the query in batches to handle larger-than-memory data. + If set to `False` (default), the entire query is processed in a single + batch. - Notes - ----- - In case of error `set_exception` is used on - `asyncio.Future`/`gevent.event.AsyncResult` and will be reraised by them. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. + .. note:: + Use :func:`explain` to see if Polars can process the query in streaming + mode. See Also -------- polars.collect_all : Collect multiple LazyFrames at the same time. - LazyFrame.collect_async: To collect single frame. + LazyFrame.collect_async : To collect single frame. + + Notes + ----- + In case of error `set_exception` is used on + `asyncio.Future`/`gevent.event.AsyncResult` and will be reraised by them. Returns ------- @@ -1787,6 +1812,10 @@ def collect_all_async( comm_subplan_elim = False comm_subexpr_elim = False + if streaming: + issue_unstable_warning("Streaming mode is considered unstable.") + comm_subplan_elim = False + prepared = [] for lf in lazy_frames: @@ -2030,6 +2059,7 @@ def from_epoch( raise ValueError(msg) +@unstable() def rolling_cov( a: str | Expr, b: str | Expr, @@ -2041,6 +2071,10 @@ def rolling_cov( """ Compute the rolling covariance between two columns/ expressions. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + The window at a given row includes the row itself and the `window_size - 1` elements before it. @@ -2070,6 +2104,7 @@ def rolling_cov( ) +@unstable() def rolling_corr( a: str | Expr, b: str | Expr, @@ -2081,6 +2116,10 @@ def rolling_corr( """ Compute the rolling correlation between two columns/ expressions. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + The window at a given row includes the row itself and the `window_size - 1` elements before it. diff --git a/py-polars/polars/io/pyarrow_dataset/functions.py b/py-polars/polars/io/pyarrow_dataset/functions.py index aad905b6d921..f1d6edf8b1eb 100644 --- a/py-polars/polars/io/pyarrow_dataset/functions.py +++ b/py-polars/polars/io/pyarrow_dataset/functions.py @@ -3,12 +3,14 @@ from typing import TYPE_CHECKING from polars.io.pyarrow_dataset.anonymous_scan import _scan_pyarrow_dataset +from polars.utils.unstable import unstable if TYPE_CHECKING: from polars import LazyFrame from polars.dependencies import pyarrow as pa +@unstable() def scan_pyarrow_dataset( source: pa.dataset.Dataset, *, @@ -18,14 +20,11 @@ def scan_pyarrow_dataset( """ Scan a pyarrow dataset. - This can be useful to connect to cloud or partitioned datasets. - .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. - This method can only can push down predicates that are allowed by PyArrow - (e.g. not the full Polars API). - - If :func:`scan_parquet` works for your source, you should use that instead. + This can be useful to connect to cloud or partitioned datasets. Parameters ---------- @@ -40,8 +39,10 @@ def scan_pyarrow_dataset( Warnings -------- - This API is experimental and may change without it being considered a breaking - change. + This method can only can push down predicates that are allowed by PyArrow + (e.g. not the full Polars API). + + If :func:`scan_parquet` works for your source, you should use that instead. Notes ----- diff --git a/py-polars/polars/lazyframe/frame.py b/py-polars/polars/lazyframe/frame.py index c95e848a3306..c4cc60575d21 100644 --- a/py-polars/polars/lazyframe/frame.py +++ b/py-polars/polars/lazyframe/frame.py @@ -70,6 +70,7 @@ deprecate_saturating, issue_deprecation_warning, ) +from polars.utils.unstable import issue_unstable_warning, unstable from polars.utils.various import ( _in_notebook, _prepare_row_index_args, @@ -1645,7 +1646,8 @@ def collect( batch. .. warning:: - This functionality is currently in an alpha state. + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. .. note:: Use :func:`explain` to see if Polars can process the query in streaming @@ -1711,6 +1713,7 @@ def collect( comm_subexpr_elim = False if streaming: + issue_unstable_warning("Streaming mode is considered unstable.") comm_subplan_elim = False ldf = self._ldf.optimization_toggle( @@ -1780,6 +1783,10 @@ def collect_async( """ Collect DataFrame asynchronously in thread pool. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Collects into a DataFrame (like :func:`collect`), but instead of returning DataFrame directly, they are scheduled to be collected inside thread pool, while this method returns almost instantly. @@ -1808,22 +1815,17 @@ def collect_async( comm_subexpr_elim Common subexpressions will be cached and reused. streaming - Run parts of the query in a streaming fashion (this is in an alpha state) - - Notes - ----- - In case of error `set_exception` is used on - `asyncio.Future`/`gevent.event.AsyncResult` and will be reraised by them. + Process the query in batches to handle larger-than-memory data. + If set to `False` (default), the entire query is processed in a single + batch. - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. - See Also - -------- - polars.collect_all : Collect multiple LazyFrames at the same time. - polars.collect_all_async: Collect multiple LazyFrames at the same time lazily. + .. note:: + Use :func:`explain` to see if Polars can process the query in streaming + mode. Returns ------- @@ -1832,6 +1834,16 @@ def collect_async( If `gevent=True` then returns wrapper that has `.get(block=True, timeout=None)` method. + See Also + -------- + polars.collect_all : Collect multiple LazyFrames at the same time. + polars.collect_all_async: Collect multiple LazyFrames at the same time lazily. + + Notes + ----- + In case of error `set_exception` is used on + `asyncio.Future`/`gevent.event.AsyncResult` and will be reraised by them. + Examples -------- >>> import asyncio @@ -1868,6 +1880,7 @@ def collect_async( comm_subexpr_elim = False if streaming: + issue_unstable_warning("Streaming mode is considered unstable.") comm_subplan_elim = False ldf = self._ldf.optimization_toggle( @@ -1886,6 +1899,7 @@ def collect_async( ldf.collect_with_callback(result._callback) # type: ignore[attr-defined] return result # type: ignore[return-value] + @unstable() def sink_parquet( self, path: str | Path, @@ -1906,6 +1920,10 @@ def sink_parquet( """ Evaluate the query in streaming mode and write to a Parquet file. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + This allows streaming results that are larger than RAM to be written to disk. Parameters @@ -1978,6 +1996,7 @@ def sink_parquet( maintain_order=maintain_order, ) + @unstable() def sink_ipc( self, path: str | Path, @@ -1994,6 +2013,10 @@ def sink_ipc( """ Evaluate the query in streaming mode and write to an IPC file. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + This allows streaming results that are larger than RAM to be written to disk. Parameters @@ -2045,6 +2068,7 @@ def sink_ipc( @deprecate_renamed_parameter("quote", "quote_char", version="0.19.8") @deprecate_renamed_parameter("has_header", "include_header", version="0.19.13") + @unstable() def sink_csv( self, path: str | Path, @@ -2072,6 +2096,10 @@ def sink_csv( """ Evaluate the query in streaming mode and write to a CSV file. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + This allows streaming results that are larger than RAM to be written to disk. Parameters @@ -2182,6 +2210,7 @@ def sink_csv( maintain_order=maintain_order, ) + @unstable() def sink_ndjson( self, path: str | Path, @@ -2197,6 +2226,10 @@ def sink_ndjson( """ Evaluate the query in streaming mode and write to an NDJSON file. + .. warning:: + Streaming mode is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + This allows streaming results that are larger than RAM to be written to disk. Parameters @@ -5736,6 +5769,7 @@ def set_sorted( [wrap_expr(e).set_sorted(descending=descending) for e in columns] ) + @unstable() def update( self, other: LazyFrame, @@ -5750,8 +5784,8 @@ def update( Update the values in this `LazyFrame` with the non-null values in `other`. .. warning:: - This functionality is experimental and may change without it being - considered a breaking change. + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. Parameters ---------- diff --git a/py-polars/polars/series/categorical.py b/py-polars/polars/series/categorical.py index 716be8bbfe51..6ebaec6b7edb 100644 --- a/py-polars/polars/series/categorical.py +++ b/py-polars/polars/series/categorical.py @@ -5,6 +5,7 @@ from polars.series.utils import expr_dispatch from polars.utils._wrap import wrap_s from polars.utils.deprecation import deprecate_function +from polars.utils.unstable import unstable if TYPE_CHECKING: from polars import Series @@ -31,6 +32,11 @@ def set_ordering(self, ordering: CategoricalOrdering) -> Series: """ Determine how this categorical series should be sorted. + .. deprecated:: 0.19.19 + Set the ordering directly on the datatype `pl.Categorical('lexical')` + or `pl.Categorical('physical')` or `cast()` to the intended data type. + This method will be removed in the next breaking change + Parameters ---------- ordering : {'physical', 'lexical'} @@ -114,20 +120,14 @@ def to_local(self) -> Series: """ return wrap_s(self._s.cat_to_local()) + @unstable() def uses_lexical_ordering(self) -> bool: """ Return whether or not the series uses lexical ordering. - This can be set using :func:`set_ordering`. - - Warnings - -------- - This API is experimental and may change without it being considered a breaking - change. - - See Also - -------- - set_ordering + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. Examples -------- diff --git a/py-polars/polars/series/datetime.py b/py-polars/polars/series/datetime.py index 2b2a6e52244d..673bcdd4ee4e 100644 --- a/py-polars/polars/series/datetime.py +++ b/py-polars/polars/series/datetime.py @@ -7,6 +7,7 @@ from polars.utils._wrap import wrap_s from polars.utils.convert import _to_python_date, _to_python_datetime from polars.utils.deprecation import deprecate_function, deprecate_renamed_function +from polars.utils.unstable import unstable if TYPE_CHECKING: import datetime as dt @@ -1687,6 +1688,7 @@ def truncate( ] """ + @unstable() def round( self, every: str | dt.timedelta, @@ -1697,15 +1699,42 @@ def round( """ Divide the date/ datetime range into buckets. - Each date/datetime in the first half of the interval - is mapped to the start of its bucket. - Each date/datetime in the second half of the interval - is mapped to the end of its bucket. - Ambiguous results are localised using the DST offset of the original timestamp - + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + + Each date/datetime in the first half of the interval is mapped to the start of + its bucket. + Each date/datetime in the second half of the interval is mapped to the end of + its bucket. + Ambiguous results are localized using the DST offset of the original timestamp - for example, rounding `'2022-11-06 01:20:00 CST'` by `'1h'` results in `'2022-11-06 01:00:00 CST'`, whereas rounding `'2022-11-06 01:20:00 CDT'` by `'1h'` results in `'2022-11-06 01:00:00 CDT'`. + Parameters + ---------- + every + Every interval start and period length + offset + Offset the window + ambiguous + Determine how to deal with ambiguous datetimes: + + - `'raise'` (default): raise + - `'earliest'`: use the earliest datetime + - `'latest'`: use the latest datetime + + .. deprecated:: 0.19.3 + This is now auto-inferred, you can safely remove this argument. + + Returns + ------- + Series + Series of data type :class:`Date` or :class:`Datetime`. + + Notes + ----- The `every` and `offset` argument are created with the the following string language: @@ -1725,37 +1754,10 @@ def round( - 3d12h4m25s # 3 days, 12 hours, 4 minutes, and 25 seconds - By "calendar day", we mean the corresponding time on the next day (which may not be 24 hours, due to daylight savings). Similarly for "calendar week", "calendar month", "calendar quarter", and "calendar year". - Parameters - ---------- - every - Every interval start and period length - offset - Offset the window - ambiguous - Determine how to deal with ambiguous datetimes: - - - `'raise'` (default): raise - - `'earliest'`: use the earliest datetime - - `'latest'`: use the latest datetime - - .. deprecated:: 0.19.3 - This is now auto-inferred, you can safely remove this argument. - - Returns - ------- - Series - Series of data type :class:`Date` or :class:`Datetime`. - - Warnings - -------- - This functionality is currently experimental and may - change without it being considered a breaking change. - Examples -------- >>> from datetime import timedelta, datetime diff --git a/py-polars/polars/series/series.py b/py-polars/polars/series/series.py index 1bdacbd4e506..9fba03a19d23 100644 --- a/py-polars/polars/series/series.py +++ b/py-polars/polars/series/series.py @@ -99,6 +99,7 @@ issue_deprecation_warning, ) from polars.utils.meta import get_index_type +from polars.utils.unstable import unstable from polars.utils.various import ( _is_generator, no_default, @@ -2217,6 +2218,7 @@ def cut( @deprecate_nonkeyword_arguments(["self", "breaks"], version="0.19.0") @deprecate_renamed_parameter("series", "as_series", version="0.19.0") + @unstable() def cut( self, breaks: Sequence[float], @@ -2231,6 +2233,10 @@ def cut( """ Bin continuous values into discrete categories. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- breaks @@ -2410,6 +2416,7 @@ def qcut( ) -> Series | DataFrame: ... + @unstable() def qcut( self, quantiles: Sequence[float] | int, @@ -2425,6 +2432,10 @@ def qcut( """ Bin continuous values into discrete categories based on their quantiles. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- quantiles @@ -2472,11 +2483,6 @@ def qcut( Series of data type :class:`Categorical` if `include_breaks` is set to `False` (default), otherwise a Series of data type :class:`Struct`. - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. - See Also -------- cut @@ -2653,6 +2659,7 @@ def rle_id(self) -> Series: ] """ + @unstable() def hist( self, bins: list[float] | None = None, @@ -2664,6 +2671,10 @@ def hist( """ Bin values into buckets and count their occurrences. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- bins @@ -2681,11 +2692,6 @@ def hist( ------- DataFrame - Warnings - -------- - This functionality is experimental and may change without it being considered a - breaking change. - Examples -------- >>> a = pl.Series("a", [1, 3, 8, 8, 2, 1, 3]) @@ -2820,12 +2826,17 @@ def entropy(self, base: float = math.e, *, normalize: bool = False) -> float | N .item() ) + @unstable() def cumulative_eval( self, expr: Expr, min_periods: int = 1, *, parallel: bool = False ) -> Series: """ Run an expression over a sliding window that increases `1` slot every iteration. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- expr @@ -2839,9 +2850,6 @@ def cumulative_eval( Warnings -------- - This functionality is experimental and may change without it being considered a - breaking change. - This can be really slow as it can have `O(n^2)` complexity. Don't use this for operations that visit all elements. @@ -5395,6 +5403,7 @@ def zip_with(self, mask: Series, other: Series) -> Self: """ return self._from_pyseries(self._s.zip_with(mask._s, other._s)) + @unstable() def rolling_min( self, window_size: int, @@ -5406,6 +5415,10 @@ def rolling_min( """ Apply a rolling min (moving min) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their min. @@ -5453,6 +5466,7 @@ def rolling_min( .to_series() ) + @unstable() def rolling_max( self, window_size: int, @@ -5464,6 +5478,10 @@ def rolling_max( """ Apply a rolling max (moving max) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their max. @@ -5511,6 +5529,7 @@ def rolling_max( .to_series() ) + @unstable() def rolling_mean( self, window_size: int, @@ -5522,6 +5541,10 @@ def rolling_mean( """ Apply a rolling mean (moving mean) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their mean. @@ -5569,6 +5592,7 @@ def rolling_mean( .to_series() ) + @unstable() def rolling_sum( self, window_size: int, @@ -5580,6 +5604,10 @@ def rolling_sum( """ Apply a rolling sum (moving sum) over the values in this array. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their sum. @@ -5627,6 +5655,7 @@ def rolling_sum( .to_series() ) + @unstable() def rolling_std( self, window_size: int, @@ -5639,6 +5668,10 @@ def rolling_std( """ Compute a rolling std dev. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their std dev. @@ -5689,6 +5722,7 @@ def rolling_std( .to_series() ) + @unstable() def rolling_var( self, window_size: int, @@ -5701,6 +5735,10 @@ def rolling_var( """ Compute a rolling variance. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + A window of length `window_size` will traverse the array. The values that fill this window will (optionally) be multiplied with the weights given by the `weight` vector. The resulting values will be aggregated to their variance. @@ -5751,6 +5789,7 @@ def rolling_var( .to_series() ) + @unstable() def rolling_map( self, function: Callable[[Series], Any], @@ -5764,8 +5803,8 @@ def rolling_map( Compute a custom rolling window function. .. warning:: - Computing custom functions is extremely slow. Use specialized rolling - functions such as :func:`Series.rolling_sum` if at all possible. + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. Parameters ---------- @@ -5788,7 +5827,8 @@ def rolling_map( Warnings -------- - + Computing custom functions is extremely slow. Use specialized rolling + functions such as :func:`Series.rolling_sum` if at all possible. Examples -------- @@ -5806,6 +5846,7 @@ def rolling_map( ] """ + @unstable() def rolling_median( self, window_size: int, @@ -5817,6 +5858,10 @@ def rolling_median( """ Compute a rolling median. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + Parameters ---------- window_size @@ -5864,6 +5909,7 @@ def rolling_median( .to_series() ) + @unstable() def rolling_quantile( self, quantile: float, @@ -5877,6 +5923,10 @@ def rolling_quantile( """ Compute a rolling quantile. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + The window at a given row will include the row itself and the `window_size - 1` elements before it. @@ -5944,10 +5994,15 @@ def rolling_quantile( .to_series() ) + @unstable() def rolling_skew(self, window_size: int, *, bias: bool = True) -> Series: """ Compute a rolling skew. + .. warning:: + This functionality is considered **unstable**. It may be changed + at any point without it being considered a breaking change. + The window at a given row includes the row itself and the `window_size - 1` elements before it. diff --git a/py-polars/polars/sql/context.py b/py-polars/polars/sql/context.py index 7e6aed58c364..afbd1dcea4c1 100644 --- a/py-polars/polars/sql/context.py +++ b/py-polars/polars/sql/context.py @@ -7,6 +7,7 @@ from polars.lazyframe import LazyFrame from polars.type_aliases import FrameType from polars.utils._wrap import wrap_ldf +from polars.utils.unstable import issue_unstable_warning from polars.utils.various import _get_stack_locals with contextlib.suppress(ImportError): # Module not available when building docs @@ -27,10 +28,10 @@ class SQLContext(Generic[FrameType]): """ Run SQL queries against DataFrame/LazyFrame data. - Warnings - -------- - This feature is stabilising, but is still considered experimental and - changes may be made without them necessarily being considered breaking. + .. warning:: + This functionality is considered **unstable**, although it is close to being + considered stable. It may be changed at any point without it being considered + a breaking change. """ _ctxt: PySQLContext @@ -74,7 +75,7 @@ def __init__( **named_frames: DataFrame | LazyFrame | None, ) -> None: """ - Initialise a new `SQLContext`. + Initialize a new `SQLContext`. Parameters ---------- @@ -109,6 +110,10 @@ def __init__( │ z ┆ 6 │ └─────┴───────┘ """ + issue_unstable_warning( + "`SQLContext` is considered **unstable**, although it is close to being considered stable." + ) + self._ctxt = PySQLContext.new() self._eager_execution = eager_execution diff --git a/py-polars/polars/utils/unstable.py b/py-polars/polars/utils/unstable.py new file mode 100644 index 000000000000..e00c9177e06b --- /dev/null +++ b/py-polars/polars/utils/unstable.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import inspect +import os +import warnings +from functools import wraps +from typing import TYPE_CHECKING, Callable, TypeVar + +from polars.exceptions import UnstableWarning +from polars.utils.various import find_stacklevel + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 10): + from typing import ParamSpec + else: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + T = TypeVar("T") + + +def issue_unstable_warning(message: str | None = None) -> None: + """ + Issue a warning for use of unstable functionality. + + The `warn_unstable` setting must be enabled, otherwise no warning is issued. + + Parameters + ---------- + message + The message associated with the warning. + + See Also + -------- + Config.warn_unstable + """ + warnings_enabled = bool(int(os.environ.get("POLARS_WARN_UNSTABLE", 0))) + if not warnings_enabled: + return + + if message is None: + message = "This functionality is considered unstable." + message += ( + " It may be changed at any point without it being considered a breaking change." + ) + + warnings.warn(message, UnstableWarning, stacklevel=find_stacklevel()) + + +def unstable() -> Callable[[Callable[P, T]], Callable[P, T]]: + """Decorator to mark a function as unstable.""" + + def decorate(function: Callable[P, T]) -> Callable[P, T]: + @wraps(function) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + issue_unstable_warning(f"`{function.__name__}` is considered unstable.") + return function(*args, **kwargs) + + wrapper.__signature__ = inspect.signature(function) # type: ignore[attr-defined] + return wrapper + + return decorate diff --git a/py-polars/tests/unit/test_config.py b/py-polars/tests/unit/test_config.py index 3a256a881273..17b58c7201c9 100644 --- a/py-polars/tests/unit/test_config.py +++ b/py-polars/tests/unit/test_config.py @@ -9,6 +9,7 @@ import polars as pl import polars.polars as plr from polars.config import _POLARS_CFG_ENV_VARS +from polars.utils.unstable import issue_unstable_warning @pytest.fixture(autouse=True) @@ -781,6 +782,21 @@ def test_set_fmt_str_lengths_invalid_length() -> None: cfg.set_fmt_str_lengths(-2) +def test_warn_unstable(recwarn: pytest.WarningsRecorder) -> None: + issue_unstable_warning() + assert len(recwarn) == 0 + + pl.Config().warn_unstable(True) + + issue_unstable_warning() + assert len(recwarn) == 1 + + pl.Config().warn_unstable(False) + + issue_unstable_warning() + assert len(recwarn) == 1 + + @pytest.mark.parametrize( ("environment_variable", "config_setting", "value", "expected"), [ @@ -842,6 +858,7 @@ def test_set_fmt_str_lengths_invalid_length() -> None: ("POLARS_STREAMING_CHUNK_SIZE", "set_streaming_chunk_size", 100, "100"), ("POLARS_TABLE_WIDTH", "set_tbl_width_chars", 80, "80"), ("POLARS_VERBOSE", "set_verbose", True, "1"), + ("POLARS_WARN_UNSTABLE", "warn_unstable", True, "1"), ], ) def test_unset_config_env_vars( diff --git a/py-polars/tests/unit/utils/test_unstable.py b/py-polars/tests/unit/utils/test_unstable.py new file mode 100644 index 000000000000..ea9e5d594c9f --- /dev/null +++ b/py-polars/tests/unit/utils/test_unstable.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import pytest + +import polars as pl +from polars.utils.unstable import issue_unstable_warning, unstable + + +def test_issue_unstable_warning(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("POLARS_WARN_UNSTABLE", "1") + + msg = "`func` is considered unstable." + expected = ( + msg + + " It may be changed at any point without it being considered a breaking change." + ) + with pytest.warns(pl.UnstableWarning, match=expected): + issue_unstable_warning(msg) + + +def test_issue_unstable_warning_default(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("POLARS_WARN_UNSTABLE", "1") + + msg = "This functionality is considered unstable." + with pytest.warns(pl.UnstableWarning, match=msg): + issue_unstable_warning() + + +def test_issue_unstable_warning_setting_disabled( + recwarn: pytest.WarningsRecorder, +) -> None: + issue_unstable_warning() + assert len(recwarn) == 0 + + +def test_unstable_decorator(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("POLARS_WARN_UNSTABLE", "1") + + @unstable() + def hello() -> None: + ... + + msg = "`hello` is considered unstable." + with pytest.warns(pl.UnstableWarning, match=msg): + hello() + + +def test_unstable_decorator_setting_disabled(recwarn: pytest.WarningsRecorder) -> None: + @unstable() + def hello() -> None: + ... + + hello() + assert len(recwarn) == 0