diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2077edd811..08855a538c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,11 +36,11 @@ repos: narwhals/dataframe\.py| # TODO: gradually enable narwhals/dependencies\.py| - # TODO: gradually enable - narwhals/stable/v1/dependencies\.py| # private, so less urgent to document too well narwhals/_.*| - ^utils/.* + ^utils/.*| + # Soft-deprecated, so less crucial to document so carefully + narwhals/stable/v1/.* )$ - repo: local hooks: @@ -69,15 +69,15 @@ repos: from\ narwhals\ import\ dtypes| from\ narwhals.dtypes\ import\ [^D_]+| import\ narwhals.stable.v1.dtypes| - from\ narwhals.stable.v1\ import\ dtypes| - from\ narwhals.stable.v1.dtypes\ import + from\ narwhals.stable\.v.\ import\ dtypes| + from\ narwhals.stable\.v.\.dtypes\ import language: pygrep files: ^narwhals/ exclude: | (?x) ^( narwhals/_utils\.py| - narwhals/stable/v1/_dtypes.py| + narwhals/stable/v./_?dtypes.py| narwhals/.*__init__.py| narwhals/.*typing\.py ) diff --git a/docs/api-reference/expr.md b/docs/api-reference/expr.md index f278bc3b2c..dbd613d134 100644 --- a/docs/api-reference/expr.md +++ b/docs/api-reference/expr.md @@ -8,9 +8,6 @@ - alias - all - any - - arg_max - - arg_min - - arg_true - cast - count - cum_count @@ -24,8 +21,6 @@ - exp - fill_null - filter - - gather_every - - head - clip - is_between - is_duplicated @@ -57,14 +52,11 @@ - rolling_sum - rolling_var - round - - sample - shift - - sort - skew - sqrt - std - sum - - tail - unique - var show_source: false diff --git a/docs/api-reference/narwhals.md b/docs/api-reference/narwhals.md index c1c9fe2e64..21551704bc 100644 --- a/docs/api-reference/narwhals.md +++ b/docs/api-reference/narwhals.md @@ -19,7 +19,6 @@ Here are the top-level functions available in Narwhals. - from_native - from_numpy - generate_temporary_column_name - - get_level - get_native_namespace - is_ordered_categorical - len diff --git a/narwhals/__init__.py b/narwhals/__init__.py index 1c6dfbc0e4..cf42b6dc84 100644 --- a/narwhals/__init__.py +++ b/narwhals/__init__.py @@ -57,7 +57,6 @@ from_arrow, from_dict, from_numpy, - get_level, len_ as len, lit, max, @@ -141,7 +140,6 @@ "from_native", "from_numpy", "generate_temporary_column_name", - "get_level", "get_native_namespace", "is_ordered_categorical", "len", diff --git a/narwhals/_compliant/dataframe.py b/narwhals/_compliant/dataframe.py index 94bb1207b0..a052151d78 100644 --- a/narwhals/_compliant/dataframe.py +++ b/narwhals/_compliant/dataframe.py @@ -22,7 +22,7 @@ ToNarwhals, ToNarwhalsT_co, ) -from narwhals._typing_compat import assert_never, deprecated +from narwhals._typing_compat import assert_never from narwhals._utils import ( ValidateBackendVersion, Version, @@ -312,10 +312,6 @@ def drop(self, columns: Sequence[str], *, strict: bool) -> Self: ... def drop_nulls(self, subset: Sequence[str] | None) -> Self: ... def explode(self, columns: Sequence[str]) -> Self: ... def filter(self, predicate: CompliantExprT_contra | Incomplete) -> Self: ... - @deprecated( - "`LazyFrame.gather_every` is deprecated and will be removed in a future version." - ) - def gather_every(self, n: int, offset: int) -> Self: ... def group_by( self, keys: Sequence[str] | Sequence[CompliantExprT_contra], @@ -349,8 +345,6 @@ def sink_parquet(self, file: str | Path | BytesIO) -> None: ... def sort( self, *by: str, descending: bool | Sequence[bool], nulls_last: bool ) -> Self: ... - @deprecated("`LazyFrame.tail` is deprecated and will be removed in a future version.") - def tail(self, n: int) -> Self: ... def unique( self, subset: Sequence[str] | None, *, keep: LazyUniqueKeepStrategy ) -> Self: ... diff --git a/narwhals/_compliant/expr.py b/narwhals/_compliant/expr.py index 0bab7b8e97..3e18663fe4 100644 --- a/narwhals/_compliant/expr.py +++ b/narwhals/_compliant/expr.py @@ -27,7 +27,6 @@ LazyExprT, NativeExprT, ) -from narwhals._typing_compat import deprecated from narwhals._utils import _StoresCompliant from narwhals.dependencies import get_numpy, is_numpy_array @@ -107,9 +106,6 @@ def cast(self, dtype: IntoDType) -> Self: ... def count(self) -> Self: ... def min(self) -> Self: ... def max(self) -> Self: ... - def arg_min(self) -> Self: ... - def arg_max(self) -> Self: ... - def arg_true(self) -> Self: ... def mean(self) -> Self: ... def sum(self) -> Self: ... def median(self) -> Self: ... @@ -134,8 +130,6 @@ def len(self) -> Self: ... def log(self, base: float) -> Self: ... def round(self, decimals: int) -> Self: ... def mode(self) -> Self: ... - def head(self, n: int) -> Self: ... - def tail(self, n: int) -> Self: ... def shift(self, n: int) -> Self: ... def is_finite(self) -> Self: ... def is_nan(self) -> Self: ... @@ -148,7 +142,6 @@ def cum_min(self, *, reverse: bool) -> Self: ... def cum_max(self, *, reverse: bool) -> Self: ... def cum_prod(self, *, reverse: bool) -> Self: ... def is_in(self, other: Any) -> Self: ... - def sort(self, *, descending: bool, nulls_last: bool) -> Self: ... def rank(self, method: RankMethod, *, descending: bool) -> Self: ... def replace_strict( self, @@ -158,14 +151,6 @@ def replace_strict( return_dtype: IntoDType | None, ) -> Self: ... def over(self, partition_by: Sequence[str], order_by: Sequence[str]) -> Self: ... - def sample( - self, - n: int | None, - *, - fraction: float | None, - with_replacement: bool, - seed: int | None, - ) -> Self: ... def quantile( self, quantile: float, interpolation: RollingInterpolationMethod ) -> Self: ... @@ -209,8 +194,6 @@ def rolling_std( self, window_size: int, *, min_samples: int, center: bool, ddof: int ) -> Self: ... - @deprecated("Since `1.22.0`") - def gather_every(self, n: int, offset: int) -> Self: ... def __and__(self, other: Any) -> Self: ... def __or__(self, other: Any) -> Self: ... def __add__(self, other: Any) -> Self: ... diff --git a/narwhals/_utils.py b/narwhals/_utils.py index 2dc0243f03..919bb59063 100644 --- a/narwhals/_utils.py +++ b/narwhals/_utils.py @@ -217,6 +217,7 @@ def _validate_backend_version(self) -> None: class Version(Enum): V1 = auto() + V2 = auto() MAIN = auto() @property @@ -225,6 +226,10 @@ def namespace(self) -> type[Namespace[Any]]: from narwhals.stable.v1._namespace import Namespace as NamespaceV1 return NamespaceV1 + if self is Version.V2: + from narwhals.stable.v2._namespace import Namespace as NamespaceV2 + + return NamespaceV2 from narwhals._namespace import Namespace return Namespace @@ -235,6 +240,10 @@ def dtypes(self) -> DTypes: from narwhals.stable.v1 import dtypes as dtypes_v1 return dtypes_v1 + if self is Version.V2: + from narwhals.stable.v2 import dtypes as dtypes_v2 + + return dtypes_v2 from narwhals import dtypes return dtypes @@ -245,6 +254,10 @@ def dataframe(self) -> type[DataFrame[Any]]: from narwhals.stable.v1 import DataFrame as DataFrameV1 return DataFrameV1 + if self is Version.V2: + from narwhals.stable.v2 import DataFrame as DataFrameV2 + + return DataFrameV2 from narwhals.dataframe import DataFrame return DataFrame @@ -255,6 +268,10 @@ def lazyframe(self) -> type[LazyFrame[Any]]: from narwhals.stable.v1 import LazyFrame as LazyFrameV1 return LazyFrameV1 + if self is Version.V2: + from narwhals.stable.v2 import LazyFrame as LazyFrameV2 + + return LazyFrameV2 from narwhals.dataframe import LazyFrame return LazyFrame @@ -265,6 +282,10 @@ def series(self) -> type[Series[Any]]: from narwhals.stable.v1 import Series as SeriesV1 return SeriesV1 + if self is Version.V2: + from narwhals.stable.v2 import Series as SeriesV2 + + return SeriesV2 from narwhals.series import Series return Series @@ -1452,7 +1473,7 @@ def find_stacklevel() -> int: return n -def issue_deprecation_warning(message: str, _version: str) -> None: +def issue_deprecation_warning(message: str, _version: str) -> None: # pragma: no cover """Issue a deprecation warning. Arguments: @@ -1477,18 +1498,10 @@ def validate_strict_and_pass_though( pass_through: bool | None, # noqa: FBT001 *, pass_through_default: bool, - emit_deprecation_warning: bool, ) -> bool: if strict is None and pass_through is None: pass_through = pass_through_default elif strict is not None and pass_through is None: - if emit_deprecation_warning: - msg = ( - "`strict` in `from_native` is deprecated, please use `pass_through` instead.\n\n" - "Note: `strict` will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.13.0") pass_through = not strict elif strict is None and pass_through is not None: pass diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 3b86d303bc..47a19e1552 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -34,7 +34,6 @@ is_list_of, is_sequence_like, is_slice_none, - issue_deprecation_warning, issue_performance_warning, supports_arrow_c_stream, ) @@ -2867,22 +2866,6 @@ def head(self, n: int = 5) -> Self: """ return super().head(n) - def tail(self, n: int = 5) -> Self: # pragma: no cover - r"""Get the last `n` rows. - - Warning: - `LazyFrame.tail` is deprecated and will be removed in a future version. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Number of rows to return. - - Returns: - A subset of the LazyFrame of shape (n, n_columns). - """ - return super().tail(n) - def drop(self, *columns: str | Iterable[str], strict: bool = True) -> Self: r"""Remove columns from the LazyFrame. @@ -3359,30 +3342,6 @@ def lazy(self) -> Self: """ return self - def gather_every(self, n: int, offset: int = 0) -> Self: - r"""Take every nth row in the DataFrame and return as a new DataFrame. - - Warning: - `LazyFrame.gather_every` is deprecated and will be removed in a future version. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Gather every *n*-th row. - offset: Starting index. - - Returns: - The LazyFrame containing only the selected rows. - """ - msg = ( - "`LazyFrame.gather_every` is deprecated and will be removed in a future version.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.29.0") - - return super().gather_every(n=n, offset=offset) - def unpivot( self, on: str | list[str] | None = None, diff --git a/narwhals/expr.py b/narwhals/expr.py index 53a4c09c63..a6223fa5e9 100644 --- a/narwhals/expr.py +++ b/narwhals/expr.py @@ -10,12 +10,7 @@ combine_metadata, extract_compliant, ) -from narwhals._utils import ( - _validate_rolling_arguments, - ensure_type, - flatten, - issue_deprecation_warning, -) +from narwhals._utils import _validate_rolling_arguments, ensure_type, flatten from narwhals.dtypes import _validate_dtype from narwhals.exceptions import InvalidOperationError from narwhals.expr_cat import ExprCatNamespace @@ -774,48 +769,6 @@ def max(self) -> Self: """ return self._with_aggregation(lambda plx: self._to_compliant_expr(plx).max()) - def arg_min(self) -> Self: - """Returns the index of the minimum value. - - Warning: - `Expr.arg_min` is deprecated and will be removed in a future version. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Returns: - A new expression. - """ - msg = ( - "`Expr.arg_min` is deprecated and will be removed in a future version.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.49.0") - return self._with_orderable_aggregation( - lambda plx: self._to_compliant_expr(plx).arg_min() - ) - - def arg_max(self) -> Self: - """Returns the index of the maximum value. - - Warning: - `Expr.arg_max` is deprecated and will be removed in a future version. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Returns: - A new expression. - """ - msg = ( - "`Expr.arg_max` is deprecated and will be removed in a future version.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.49.0") - return self._with_orderable_aggregation( - lambda plx: self._to_compliant_expr(plx).arg_max() - ) - def count(self) -> Self: """Returns the number of non-null elements in the column. @@ -1091,36 +1044,6 @@ def replace_strict( ) ) - def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self: - """Sort this column. Place null values first. - - Warning: - `Expr.sort` is deprecated and will be removed in a future version. - Hint: instead of `df.select(nw.col('a').sort())`, use - `df.select(nw.col('a')).sort()` instead. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - descending: Sort in descending order. - nulls_last: Place null values last instead of first. - - Returns: - A new expression. - """ - msg = ( - "`Expr.sort` is deprecated and will be removed in a future version.\n\n" - "Hint: instead of `df.select(nw.col('a').sort())`, use `df.select(nw.col('a')).sort()`.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_orderable_window( - lambda plx: self._to_compliant_expr(plx).sort( - descending=descending, nulls_last=nulls_last - ) - ) - # --- transform --- def is_between( self, @@ -1336,25 +1259,6 @@ def is_nan(self) -> Self: """ return self._with_elementwise(lambda plx: self._to_compliant_expr(plx).is_nan()) - def arg_true(self) -> Self: - """Find elements where boolean expression is True. - - Warning: - `Expr.arg_true` is deprecated and will be removed in a future version. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Returns: - A new expression. - """ - msg = ( - "`Expr.arg_true` is deprecated and will be removed in a future version.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_filtration(lambda plx: self._to_compliant_expr(plx).arg_true()) - def fill_null( self, value: Expr | NonNestedLiteral = None, @@ -1493,46 +1397,6 @@ def drop_nulls(self) -> Self: lambda plx: self._to_compliant_expr(plx).drop_nulls() ) - def sample( - self, - n: int | None = None, - *, - fraction: float | None = None, - with_replacement: bool = False, - seed: int | None = None, - ) -> Self: - """Sample randomly from this expression. - - Warning: - `Expr.sample` is deprecated and will be removed in a future version. - Hint: instead of `df.select(nw.col('a').sample())`, use - `df.select(nw.col('a')).sample()` instead. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Number of items to return. Cannot be used with fraction. - fraction: Fraction of items to return. Cannot be used with n. - with_replacement: Allow values to be sampled more than once. - seed: Seed for the random number generator. If set to None (default), a random - seed is generated for each sample operation. - - Returns: - A new expression. - """ - msg = ( - "`Expr.sample` is deprecated and will be removed in a future version.\n\n" - "Hint: instead of `df.select(nw.col('a').sample())`, use `df.select(nw.col('a')).sample()`.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_filtration( - lambda plx: self._to_compliant_expr(plx).sample( - n, fraction=fraction, with_replacement=with_replacement, seed=seed - ) - ) - def over( self, *partition_by: str | Sequence[str], @@ -1781,58 +1645,6 @@ def quantile( lambda plx: self._to_compliant_expr(plx).quantile(quantile, interpolation) ) - def head(self, n: int = 10) -> Self: - r"""Get the first `n` rows. - - Warning: - `Expr.head` is deprecated and will be removed in a future version. - Hint: instead of `df.select(nw.col('a').head())`, use - `df.select(nw.col('a')).head()` instead. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Number of rows to return. - - Returns: - A new expression. - """ - msg = ( - "`Expr.head` is deprecated and will be removed in a future version.\n\n" - "Hint: instead of `df.select(nw.col('a').head())`, use `df.select(nw.col('a')).head()`.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_orderable_filtration( - lambda plx: self._to_compliant_expr(plx).head(n) - ) - - def tail(self, n: int = 10) -> Self: - r"""Get the last `n` rows. - - Warning: - `Expr.tail` is deprecated and will be removed in a future version. - Hint: instead of `df.select(nw.col('a').tail())`, use - `df.select(nw.col('a')).tail()` instead. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Number of rows to return. - - Returns: - A new expression. - """ - msg = ( - "`Expr.tail` is deprecated and will be removed in a future version.\n\n" - "Hint: instead of `df.select(nw.col('a').tail())`, use `df.select(nw.col('a')).tail()`.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_filtration(lambda plx: self._to_compliant_expr(plx).tail(n)) - def round(self, decimals: int = 0) -> Self: r"""Round underlying floating point data by `decimals` digits. @@ -1896,34 +1708,6 @@ def len(self) -> Self: """ return self._with_aggregation(lambda plx: self._to_compliant_expr(plx).len()) - def gather_every(self, n: int, offset: int = 0) -> Self: - r"""Take every nth value in the Series and return as new Series. - - Warning: - `Expr.gather_every` is deprecated and will be removed in a future version. - Hint: instead of `df.select(nw.col('a').gather_every())`, use - `df.select(nw.col('a')).gather_every()` instead. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - n: Gather every *n*-th row. - offset: Starting index. - - Returns: - A new expression. - """ - msg = ( - "`Expr.gather_every` is deprecated and will be removed in a future version.\n\n" - "Hint: instead of `df.select(nw.col('a').gather_every())`, use `df.select(nw.col('a')).gather_every()`.\n\n" - "Note: this will remain available in `narwhals.stable.v1`.\n" - "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n" - ) - issue_deprecation_warning(msg, _version="1.23.0") - return self._with_filtration( - lambda plx: self._to_compliant_expr(plx).gather_every(n=n, offset=offset) - ) - def clip( self, lower_bound: IntoExpr | NumericLiteral | TemporalLiteral | None = None, diff --git a/narwhals/functions.py b/narwhals/functions.py index 9bc7501b9f..c131e6ac8b 100644 --- a/narwhals/functions.py +++ b/narwhals/functions.py @@ -4,7 +4,7 @@ import sys from collections.abc import Iterable, Mapping, Sequence from functools import partial -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any from narwhals._expression_parsing import ( ExprKind, @@ -14,7 +14,6 @@ extract_compliant, is_scalar_like, ) -from narwhals._typing_compat import deprecated from narwhals._utils import ( Implementation, Version, @@ -23,7 +22,6 @@ is_compliant_expr, is_eager_allowed, is_sequence_but_not_str, - issue_deprecation_warning, supports_arrow_c_stream, validate_laziness, ) @@ -53,7 +51,6 @@ FrameT, IntoDType, IntoExpr, - IntoSeriesT, NativeFrame, NativeLazyFrame, NativeSeries, @@ -170,14 +167,12 @@ def concat(items: Iterable[FrameT], *, how: ConcatMethod = "vertical") -> FrameT ) -@deprecate_native_namespace(warn_version="1.31.0", required=True) def new_series( name: str, values: Any, dtype: IntoDType | None = None, *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, # noqa: ARG001 + backend: ModuleType | Implementation | str, ) -> Series[Any]: """Instantiate Narwhals Series from iterable (e.g. list or array). @@ -194,13 +189,6 @@ def new_series( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). Returns: A new Series @@ -221,7 +209,6 @@ def new_series( |Name: a, dtype: int32| └─────────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) return _new_series_impl(name, values, dtype, backend=backend) @@ -285,13 +272,7 @@ def from_dict( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.26.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). + native_namespace: deprecated, same as `backend`. Returns: A new DataFrame. @@ -347,13 +328,11 @@ def _from_dict_no_backend( return data, native_namespace -@deprecate_native_namespace(warn_version="1.31.0", required=True) def from_numpy( data: _2DArray, schema: Mapping[str, DType] | Schema | Sequence[str] | None = None, *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, # noqa: ARG001 + backend: ModuleType | Implementation | str, ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. @@ -374,13 +353,6 @@ def from_numpy( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). Returns: A new DataFrame. @@ -406,7 +378,6 @@ def from_numpy( | e: [[1,3]] | └──────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) if not is_numpy_array_2d(data): msg = "`from_numpy` only accepts 2D numpy arrays" raise ValueError(msg) @@ -447,12 +418,8 @@ def _is_into_schema(obj: Any) -> TypeIs[_IntoSchema]: ) -@deprecate_native_namespace(warn_version="1.31.0", required=True) def from_arrow( - native_frame: IntoArrowTable, - *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, # noqa: ARG001 + native_frame: IntoArrowTable, *, backend: ModuleType | Implementation | str ) -> DataFrame[Any]: # pragma: no cover """Construct a DataFrame from an object which supports the PyCapsule Interface. @@ -466,13 +433,6 @@ def from_arrow( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). Returns: A new DataFrame. @@ -498,7 +458,6 @@ def from_arrow( | └─────┴─────┘ | └──────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) if not (supports_arrow_c_stream(native_frame) or is_pyarrow_table(native_frame)): msg = f"Given object of type {type(native_frame)} does not support PyCapsule interface" raise TypeError(msg) @@ -601,47 +560,8 @@ def show_versions() -> None: print(f"{k:>13}: {stat}") # noqa: T201 -@deprecated( - "`get_level` is deprecated, as Narwhals no longer supports the Dataframe Interchange Protocol." -) -def get_level( - obj: DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT], -) -> Literal["full", "lazy", "interchange"]: - """Level of support Narwhals has for current object. - - Warning: - `get_level` is deprecated and will be removed in a future version. - "DuckDB and Ibis now have full lazy support in Narwhals, and passing - them to `nw.from_native` returns `nw.LazyFrame`. - Note: this will remain available in `narwhals.stable.v1`. - See [stable api](../backcompat.md/) for more information. - - Arguments: - obj: Dataframe or Series. - - Returns: - This can be one of - - - 'full': full Narwhals API support - - 'lazy': only lazy operations are supported. This excludes anything - which involves iterating over rows in Python. - """ - issue_deprecation_warning( - "`get_level` is deprecated, as Narwhals no longer supports the Dataframe Interchange Protocol.\n" - "DuckDB and Ibis now have full lazy support in Narwhals, and passing them to `nw.from_native` \n" - "returns `nw.LazyFrame`.", - "1.43", - ) - return obj._level - - -@deprecate_native_namespace(warn_version="1.27.2", required=True) def read_csv( - source: str, - *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, - **kwargs: Any, + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any ) -> DataFrame[Any]: """Read a CSV file into a DataFrame. @@ -654,13 +574,6 @@ def read_csv( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.27.2) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). kwargs: Extra keyword arguments which are passed to the native CSV reader. For example, you could use `nw.read_csv('file.csv', backend='pandas', engine='pyarrow')`. @@ -679,7 +592,6 @@ def read_csv( | 1 2 5 | └──────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) eager_backend = Implementation.from_backend(backend) native_namespace = eager_backend.to_native_namespace() native_frame: NativeFrame @@ -705,13 +617,8 @@ def read_csv( return from_native(native_frame, eager_only=True) -@deprecate_native_namespace(warn_version="1.31.0", required=True) def scan_csv( - source: str, - *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, - **kwargs: Any, + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any ) -> LazyFrame[Any]: """Lazily read from a CSV file. @@ -727,13 +634,6 @@ def scan_csv( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). kwargs: Extra keyword arguments which are passed to the native CSV reader. For example, you could use `nw.scan_csv('file.csv', backend=pd, engine='pyarrow')`. @@ -755,7 +655,6 @@ def scan_csv( │ z │ 3 │ └─────────┴───────┘ """ - backend = cast("ModuleType | Implementation | str", backend) implementation = Implementation.from_backend(backend) native_namespace = implementation.to_native_namespace() native_frame: NativeFrame | NativeLazyFrame @@ -799,13 +698,8 @@ def scan_csv( return from_native(native_frame).lazy() -@deprecate_native_namespace(warn_version="1.31.0", required=True) def read_parquet( - source: str, - *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, - **kwargs: Any, + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any ) -> DataFrame[Any]: """Read into a DataFrame from a parquet file. @@ -818,13 +712,6 @@ def read_parquet( `POLARS`, `MODIN` or `CUDF`. - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). kwargs: Extra keyword arguments which are passed to the native parquet reader. For example, you could use `nw.read_parquet('file.parquet', backend=pd, engine='pyarrow')`. @@ -848,7 +735,6 @@ def read_parquet( |c: [[0.2,0.1]] | └──────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) implementation = Implementation.from_backend(backend) native_namespace = implementation.to_native_namespace() native_frame: NativeFrame @@ -876,13 +762,8 @@ def read_parquet( return from_native(native_frame, eager_only=True) -@deprecate_native_namespace(warn_version="1.31.0", required=True) def scan_parquet( - source: str, - *, - backend: ModuleType | Implementation | str | None = None, - native_namespace: ModuleType | None = None, - **kwargs: Any, + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any ) -> LazyFrame[Any]: """Lazily read from a parquet file. @@ -912,13 +793,6 @@ def scan_parquet( `"pyspark"` or `"sqlframe"`. - Directly as a module `pandas`, `pyarrow`, `polars`, `modin`, `cudf`, `pyspark.sql` or `sqlframe`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). kwargs: Extra keyword arguments which are passed to the native parquet reader. For example, you could use `nw.scan_parquet('file.parquet', backend=pd, engine='pyarrow')`. @@ -953,7 +827,6 @@ def scan_parquet( | b: [[4,5]] | └──────────────────┘ """ - backend = cast("ModuleType | Implementation | str", backend) implementation = Implementation.from_backend(backend) native_namespace = implementation.to_native_namespace() native_frame: NativeFrame | NativeLazyFrame @@ -1575,9 +1448,7 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When: return When(*predicates) -def all_horizontal( - *exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool | None = None -) -> Expr: +def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: r"""Compute the bitwise AND horizontally across columns. Arguments: @@ -1587,7 +1458,7 @@ def all_horizontal( - If `True`, null values are ignored. If there are no elements, the result is `True`. - - If `False` (default), Kleene logic is followed. Note that this is not allowed for + - If `False`, Kleene logic is followed. Note that this is not allowed for pandas with classical NumPy dtypes when null values are present. Returns: @@ -1622,12 +1493,6 @@ def all_horizontal( if not exprs: msg = "At least one expression must be passed to `all_horizontal`" raise ValueError(msg) - if ignore_nulls is None: - issue_deprecation_warning( - "`ignore_nulls` will become a required argument in Narwhals 2.0. Please specify `ignore_nulls=True` or `ignore_nulls=False` to silence this warning.", - _version="1.45", - ) - ignore_nulls = False flat_exprs = flatten(exprs) return Expr( lambda plx: apply_n_ary_operation( @@ -1679,9 +1544,7 @@ def lit(value: NonNestedLiteral, dtype: IntoDType | None = None) -> Expr: return Expr(lambda plx: plx.lit(value, dtype), ExprMetadata.literal()) -def any_horizontal( - *exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool | None = None -) -> Expr: +def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: r"""Compute the bitwise OR horizontally across columns. Arguments: @@ -1691,7 +1554,7 @@ def any_horizontal( - If `True`, null values are ignored. If there are no elements, the result is `False`. - - If `False` (default), Kleene logic is followed. Note that this is not allowed for + - If `False`, Kleene logic is followed. Note that this is not allowed for pandas with classical NumPy dtypes when null values are present. Returns: @@ -1730,12 +1593,6 @@ def any_horizontal( if not exprs: msg = "At least one expression must be passed to `any_horizontal`" raise ValueError(msg) - if ignore_nulls is None: - issue_deprecation_warning( - "`ignore_nulls` will become a required argument in Narwhals 2.0. Please specify `ignore_nulls=True` or `ignore_nulls=False` to silence this warning.", - _version="1.45", - ) - ignore_nulls = False flat_exprs = flatten(exprs) return Expr( lambda plx: apply_n_ary_operation( diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py index 020f994f71..de89a8ed6b 100644 --- a/narwhals/stable/v1/__init__.py +++ b/narwhals/stable/v1/__init__.py @@ -209,11 +209,7 @@ def is_unique(self) -> Series[Any]: return _stableify(super().is_unique()) def _l1_norm(self) -> Self: - """Private, just used to test the stable API. - - Returns: - A new DataFrame. - """ + # Private, just used to test the stable API. return self.select(all()._l1_norm()) @@ -261,22 +257,11 @@ def collect( return _stableify(super().collect(backend=backend, **kwargs)) def _l1_norm(self) -> Self: - """Private, just used to test the stable API. - - Returns: - A new lazyframe. - """ + # Private, just used to test the stable API. return self.select(all()._l1_norm()) def tail(self, n: int = 5) -> Self: - r"""Get the last `n` rows. - - Arguments: - n: Number of rows to return. - - Returns: - A subset of the LazyFrame of shape (n, n_columns). - """ + r"""Get the last `n` rows.""" return super().tail(n) def gather_every(self, n: int, offset: int = 0) -> Self: @@ -285,26 +270,14 @@ def gather_every(self, n: int, offset: int = 0) -> Self: Arguments: n: Gather every *n*-th row. offset: Starting index. - - Returns: - The LazyFrame containing only the selected rows. """ return self._with_compliant( - self._compliant_frame.gather_every(n=n, offset=offset) + self._compliant_frame.gather_every(n=n, offset=offset) # type: ignore[attr-defined] ) def with_row_index( self, name: str = "index", *, order_by: str | Sequence[str] | None = None ) -> Self: - """Insert column which enumerates rows. - - Arguments: - name: The name of the column as a string. The default is "index". - order_by: Column(s) to order by when computing the row index. - - Returns: - The original object with the column added. - """ order_by_ = [order_by] if isinstance(order_by, str) else order_by return self._with_compliant( self._compliant_frame.with_row_index( @@ -373,29 +346,15 @@ def _l1_norm(self) -> Self: return super()._taxicab_norm() def head(self, n: int = 10) -> Self: - r"""Get the first `n` rows. - - Arguments: - n: Number of rows to return. - - Returns: - A new expression. - """ + r"""Get the first `n` rows.""" return self._with_orderable_filtration( - lambda plx: self._to_compliant_expr(plx).head(n) + lambda plx: self._to_compliant_expr(plx).head(n) # type: ignore[attr-defined] ) def tail(self, n: int = 10) -> Self: - r"""Get the last `n` rows. - - Arguments: - n: Number of rows to return. - - Returns: - A new expression. - """ + r"""Get the last `n` rows.""" return self._with_orderable_filtration( - lambda plx: self._to_compliant_expr(plx).tail(n) + lambda plx: self._to_compliant_expr(plx).tail(n) # type: ignore[attr-defined] ) def gather_every(self, n: int, offset: int = 0) -> Self: @@ -404,25 +363,13 @@ def gather_every(self, n: int, offset: int = 0) -> Self: Arguments: n: Gather every *n*-th row. offset: Starting index. - - Returns: - A new expression. """ return self._with_orderable_filtration( - lambda plx: self._to_compliant_expr(plx).gather_every(n=n, offset=offset) + lambda plx: self._to_compliant_expr(plx).gather_every(n=n, offset=offset) # type: ignore[attr-defined] ) def unique(self, *, maintain_order: bool | None = None) -> Self: - """Return unique values of this expression. - - Arguments: - maintain_order: Keep the same order as the original expression. - This is deprecated and will be removed in a future version, - but will still be kept around in `narwhals.stable.v1`. - - Returns: - A new expression. - """ + """Return unique values of this expression.""" if maintain_order is not None: msg = ( "`maintain_order` has no effect and is only kept around for backwards-compatibility. " @@ -432,49 +379,29 @@ def unique(self, *, maintain_order: bool | None = None) -> Self: return self._with_filtration(lambda plx: self._to_compliant_expr(plx).unique()) def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self: - """Sort this column. Place null values first. - - Arguments: - descending: Sort in descending order. - nulls_last: Place null values last instead of first. - - Returns: - A new expression. - """ + """Sort this column. Place null values first.""" return self._with_window( - lambda plx: self._to_compliant_expr(plx).sort( + lambda plx: self._to_compliant_expr(plx).sort( # type: ignore[attr-defined] descending=descending, nulls_last=nulls_last ) ) def arg_max(self) -> Self: - """Returns the index of the maximum value. - - Returns: - A new expression. - """ + """Returns the index of the maximum value.""" return self._with_orderable_aggregation( - lambda plx: self._to_compliant_expr(plx).arg_max() + lambda plx: self._to_compliant_expr(plx).arg_max() # type: ignore[attr-defined] ) def arg_min(self) -> Self: - """Returns the index of the minimum value. - - Returns: - A new expression. - """ + """Returns the index of the minimum value.""" return self._with_orderable_aggregation( - lambda plx: self._to_compliant_expr(plx).arg_min() + lambda plx: self._to_compliant_expr(plx).arg_min() # type: ignore[attr-defined] ) def arg_true(self) -> Self: - """Find elements where boolean expression is True. - - Returns: - A new expression. - """ + """Find elements where boolean expression is True.""" return self._with_orderable_filtration( - lambda plx: self._to_compliant_expr(plx).arg_true() + lambda plx: self._to_compliant_expr(plx).arg_true() # type: ignore[attr-defined] ) def sample( @@ -498,7 +425,7 @@ def sample( A new expression. """ return self._with_filtration( - lambda plx: self._to_compliant_expr(plx).sample( + lambda plx: self._to_compliant_expr(plx).sample( # type: ignore[attr-defined] n, fraction=fraction, with_replacement=with_replacement, seed=seed ) ) @@ -945,7 +872,7 @@ def from_native( ) -> Any: ... -def from_native( # noqa: D417 +def from_native( native_object: IntoFrameT | IntoFrame | IntoSeriesT | IntoSeries | T, *, strict: bool | None = None, @@ -958,53 +885,9 @@ def from_native( # noqa: D417 ) -> LazyFrame[IntoFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T: """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. - Arguments: - native_object: Raw object from user. - Depending on the other arguments, input object can be - - - a Dataframe / Lazyframe / Series supported by Narwhals (pandas, Polars, PyArrow, ...) - - an object which implements `__narwhals_dataframe__`, `__narwhals_lazyframe__`, - or `__narwhals_series__` - strict: Determine what happens if the object can't be converted to Narwhals - - - `True` or `None` (default): raise an error - - `False`: pass object through as-is - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - pass_through: Determine what happens if the object can't be converted to Narwhals - - - `False` or `None` (default): raise an error - - `True`: pass object through as-is - eager_only: Whether to only allow eager objects - - - `False` (default): don't require `native_object` to be eager - - `True`: only convert to Narwhals if `native_object` is eager - eager_or_interchange_only: Whether to only allow eager objects or objects which - have interchange-level support in Narwhals - - - `False` (default): don't require `native_object` to either be eager or to - have interchange-level support in Narwhals - - `True`: only convert to Narwhals if `native_object` is eager or has - interchange-level support in Narwhals - - See [interchange-only support](../extending.md/#interchange-only-support) - for more details. - series_only: Whether to only allow Series - - - `False` (default): don't require `native_object` to be a Series - - `True`: only convert to Narwhals if `native_object` is a Series - allow_series: Whether to allow Series (default is only Dataframe / Lazyframe) - - - `False` or `None` (default): don't convert to Narwhals if `native_object` is a Series - - `True`: allow `native_object` to be a Series - - Returns: - DataFrame, LazyFrame, Series, or original object, depending - on which combination of parameters was passed. + See `narwhals.from_native` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ # Early returns if isinstance(native_object, (DataFrame, LazyFrame)) and not series_only: @@ -1013,7 +896,7 @@ def from_native( # noqa: D417 return native_object pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=False, emit_deprecation_warning=False + strict, pass_through, pass_through_default=False ) if kwds: msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}" @@ -1070,30 +953,14 @@ def to_native( ) -> IntoFrameT | IntoSeriesT | Any: """Convert Narwhals object to native one. - Arguments: - narwhals_object: Narwhals object. - strict: Determine what happens if `narwhals_object` isn't a Narwhals class - - - `True` (default): raise an error - - `False`: pass object through as-is - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - pass_through: Determine what happens if `narwhals_object` isn't a Narwhals class - - - `False` (default): raise an error - - `True`: pass object through as-is - - Returns: - Object of class that user started with. + See `narwhals.to_native` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ from narwhals._utils import validate_strict_and_pass_though pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=False, emit_deprecation_warning=False + strict, pass_through, pass_through_default=False ) return nw.to_native(narwhals_object, pass_through=pass_through) @@ -1110,58 +977,12 @@ def narwhalify( ) -> Callable[..., Any]: """Decorate function so it becomes dataframe-agnostic. - This will try to convert any dataframe/series-like object into the Narwhals - respective DataFrame/Series, while leaving the other parameters as they are. - Similarly, if the output of the function is a Narwhals DataFrame or Series, it will be - converted back to the original dataframe/series type, while if the output is another - type it will be left as is. - By setting `pass_through=False`, then every input and every output will be required to be a - dataframe/series-like object. - - Arguments: - func: Function to wrap in a `from_native`-`to_native` block. - strict: Determine what happens if the object can't be converted to Narwhals - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - - `True` or `None` (default): raise an error - - `False`: pass object through as-is - pass_through: Determine what happens if the object can't be converted to Narwhals - - - `False` or `None` (default): raise an error - - `True`: pass object through as-is - eager_only: Whether to only allow eager objects - - - `False` (default): don't require `native_object` to be eager - - `True`: only convert to Narwhals if `native_object` is eager - eager_or_interchange_only: Whether to only allow eager objects or objects which - have interchange-level support in Narwhals - - - `False` (default): don't require `native_object` to either be eager or to - have interchange-level support in Narwhals - - `True`: only convert to Narwhals if `native_object` is eager or has - interchange-level support in Narwhals - - See [interchange-only support](../extending.md/#interchange-only-support) - for more details. - series_only: Whether to only allow Series - - - `False` (default): don't require `native_object` to be a Series - - `True`: only convert to Narwhals if `native_object` is a Series - allow_series: Whether to allow Series (default is only Dataframe / Lazyframe) - - - `False` or `None`: don't convert to Narwhals if `native_object` is a Series - - `True` (default): allow `native_object` to be a Series - - Returns: - Decorated function. + See `narwhals.narwhalify` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=True, emit_deprecation_warning=False + strict, pass_through, pass_through_default=True ) def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @@ -1215,254 +1036,74 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: def all() -> Expr: - """Instantiate an expression representing all columns. - - Returns: - A new expression. - """ return _stableify(nw.all()) def col(*names: str | Iterable[str]) -> Expr: - """Creates an expression that references one or more columns by their name(s). - - Arguments: - names: Name(s) of the columns to use. - - Returns: - A new expression. - """ return _stableify(nw.col(*names)) def exclude(*names: str | Iterable[str]) -> Expr: - """Creates an expression that excludes columns by their name(s). - - Arguments: - names: Name(s) of the columns to exclude. - - Returns: - A new expression. - """ return _stableify(nw.exclude(*names)) def nth(*indices: int | Sequence[int]) -> Expr: - """Creates an expression that references one or more columns by their index(es). - - Notes: - `nth` is not supported for Polars version<1.0.0. Please use - [`narwhals.col`][] instead. - - Arguments: - indices: One or more indices representing the columns to retrieve. - - Returns: - A new expression. - """ return _stableify(nw.nth(*indices)) def len() -> Expr: - """Return the number of rows. - - Returns: - A new expression. - """ return _stableify(nw.len()) def lit(value: NonNestedLiteral, dtype: IntoDType | None = None) -> Expr: - """Return an expression representing a literal value. - - Arguments: - value: The value to use as literal. - dtype: The data type of the literal value. If not provided, the data type will - be inferred by the native library. - - Returns: - A new expression. - """ return _stableify(nw.lit(value, dtype)) def min(*columns: str) -> Expr: - """Return the minimum value. - - Note: - Syntactic sugar for ``nw.col(columns).min()``. - - Arguments: - columns: Name(s) of the columns to use in the aggregation function. - - Returns: - A new expression. - """ return _stableify(nw.min(*columns)) def max(*columns: str) -> Expr: - """Return the maximum value. - - Note: - Syntactic sugar for ``nw.col(columns).max()``. - - Arguments: - columns: Name(s) of the columns to use in the aggregation function. - - Returns: - A new expression. - """ return _stableify(nw.max(*columns)) def mean(*columns: str) -> Expr: - """Get the mean value. - - Note: - Syntactic sugar for ``nw.col(columns).mean()`` - - Arguments: - columns: Name(s) of the columns to use in the aggregation function - - Returns: - A new expression. - """ return _stableify(nw.mean(*columns)) def median(*columns: str) -> Expr: - """Get the median value. - - Notes: - - Syntactic sugar for ``nw.col(columns).median()`` - - Results might slightly differ across backends due to differences in the - underlying algorithms used to compute the median. - - Arguments: - columns: Name(s) of the columns to use in the aggregation function - - Returns: - A new expression. - """ return _stableify(nw.median(*columns)) def sum(*columns: str) -> Expr: - """Sum all values. - - Note: - Syntactic sugar for ``nw.col(columns).sum()`` - - Arguments: - columns: Name(s) of the columns to use in the aggregation function - - Returns: - A new expression. - """ return _stableify(nw.sum(*columns)) def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: - """Sum all values horizontally across columns. - - Warning: - Unlike Polars, we support horizontal sum over numeric columns only. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - - Returns: - A new expression. - """ return _stableify(nw.sum_horizontal(*exprs)) def all_horizontal( *exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool = False ) -> Expr: - r"""Compute the bitwise AND horizontally across columns. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - ignore_nulls: Whether to ignore nulls: - - - If `True`, null values are ignored. If there are no elements, the result - is `True`. - - If `False` (default), Kleene logic is followed. Note that this is not allowed for - pandas with classical NumPy dtypes when null values are present. - - Returns: - A new expression. - """ return _stableify(nw.all_horizontal(*exprs, ignore_nulls=ignore_nulls)) def any_horizontal( *exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool = False ) -> Expr: - r"""Compute the bitwise OR horizontally across columns. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - ignore_nulls: Whether to ignore nulls: - - - If `True`, null values are ignored. If there are no elements, the result - is `False`. - - If `False` (default), Kleene logic is followed. Note that this is not allowed for - pandas with classical NumPy dtypes when null values are present. - - Returns: - A new expression. - """ return _stableify(nw.any_horizontal(*exprs, ignore_nulls=ignore_nulls)) def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: - """Compute the mean of all values horizontally across columns. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - - Returns: - A new expression. - """ return _stableify(nw.mean_horizontal(*exprs)) def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: - """Get the minimum value horizontally across columns. - - Notes: - We support `min_horizontal` over numeric columns only. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - - Returns: - A new expression. - """ return _stableify(nw.min_horizontal(*exprs)) def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: - """Get the maximum value horizontally across columns. - - Notes: - We support `max_horizontal` over numeric columns only. - - Arguments: - exprs: Name(s) of the columns to use in the aggregation function. Accepts - expression input. - - Returns: - A new expression. - """ return _stableify(nw.max_horizontal(*exprs)) @@ -1472,43 +1113,12 @@ def concat_str( separator: str = "", ignore_nulls: bool = False, ) -> Expr: - r"""Horizontally concatenate columns into a single string column. - - Arguments: - exprs: Columns to concatenate into a single string column. Accepts expression - input. Strings are parsed as column names, other non-expression inputs are - parsed as literals. Non-`String` columns are cast to `String`. - *more_exprs: Additional columns to concatenate into a single string column, - specified as positional arguments. - separator: String that will be used to separate the values of each column. - ignore_nulls: Ignore null values (default is `False`). - If set to `False`, null values will be propagated and if the row contains any - null values, the output is null. - - Returns: - A new expression. - """ return _stableify( nw.concat_str(exprs, *more_exprs, separator=separator, ignore_nulls=ignore_nulls) ) def coalesce(exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr) -> Expr: - """Folds the columns from left to right, keeping the first non-null value. - - Arguments: - exprs: Columns to coalesce, must be a str, nw.Expr, or nw.Series - where strings are parsed as column names and both nw.Expr/nw.Series - are passed through as-is. Scalar values must be wrapped in `nw.lit`. - - *more_exprs: Additional columns to coalesce, specified as positional arguments. - - Raises: - TypeError: If any of the inputs are not a str, nw.Expr, or nw.Series. - - Returns: - A new expression. - """ return _stableify(nw.coalesce(exprs, *more_exprs)) @@ -1550,26 +1160,6 @@ def otherwise(self, value: IntoExpr | NonNestedLiteral | _1DArray) -> Expr: def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When: - """Start a `when-then-otherwise` expression. - - Expression similar to an `if-else` statement in Python. Always initiated by a - `pl.when().then()`, and optionally followed by a - `.otherwise()` can be appended at the end. If not - appended, and the condition is not `True`, `None` will be returned. - - Info: - Chaining multiple `.when().then()` statements is currently - not supported. - See [Narwhals#668](https://github.com/narwhals-dev/narwhals/issues/668). - - Arguments: - predicates: Condition(s) that must be met in order to apply the subsequent - statement. Accepts one or more boolean expressions, which are implicitly - combined with `&`. String input is parsed as a column name. - - Returns: - A "when" object, which `.then` can be called on. - """ return When.from_when(nw_f.when(*predicates)) @@ -1584,29 +1174,9 @@ def new_series( ) -> Series[Any]: """Instantiate Narwhals Series from iterable (e.g. list or array). - Arguments: - name: Name of resulting Series. - values: Values of make Series from. - dtype: (Narwhals) dtype. If not provided, the native library - may auto-infer it from `values`. - backend: specifies which eager backend instantiate to. - - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - Returns: - A new Series + See `narwhals.new_series` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(_new_series_impl(name, values, dtype, backend=backend)) @@ -1621,26 +1191,9 @@ def from_arrow( ) -> DataFrame[Any]: """Construct a DataFrame from an object which supports the PyCapsule Interface. - Arguments: - native_frame: Object which implements `__arrow_c_stream__`. - backend: specifies which eager backend instantiate to. - - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - Returns: - A new DataFrame. + See `narwhals.from_arrow` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.from_arrow(native_frame, backend=backend)) @@ -1656,36 +1209,9 @@ def from_dict( ) -> DataFrame[Any]: """Instantiate DataFrame from dictionary. - Indexes (if present, for pandas-like backends) are aligned following - the [left-hand-rule](../concepts/pandas_index.md/). - - Notes: - For pandas-like dataframes, conversion to schema is applied after dataframe - creation. - - Arguments: - data: Dictionary to create DataFrame from. - schema: The DataFrame schema as Schema or dict of {name: type}. If not - specified, the schema will be inferred by the native library. - backend: specifies which eager backend instantiate to. Only - necessary if inputs are not Narwhals Series. - - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.26.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - Returns: - A new DataFrame. + See `narwhals.from_dict` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ return _stableify(nw_f.from_dict(data, schema, backend=backend)) @@ -1700,33 +1226,9 @@ def from_numpy( ) -> DataFrame[Any]: """Construct a DataFrame from a NumPy ndarray. - Notes: - Only row orientation is currently supported. - - For pandas-like dataframes, conversion to schema is applied after dataframe - creation. - - Arguments: - data: Two-dimensional data represented as a NumPy ndarray. - schema: The DataFrame schema as Schema, dict of {name: type}, or a sequence of str. - backend: specifies which eager backend instantiate to. - - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - Returns: - A new DataFrame. + See `narwhals.from_numpy` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.from_numpy(data, schema, backend=backend)) @@ -1742,28 +1244,9 @@ def read_csv( ) -> DataFrame[Any]: """Read a CSV file into a DataFrame. - Arguments: - source: Path to a file. - backend: The eager backend for DataFrame creation. - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.27.2) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - kwargs: Extra keyword arguments which are passed to the native CSV reader. - For example, you could use - `nw.read_csv('file.csv', backend='pandas', engine='pyarrow')`. - - Returns: - DataFrame. + See `narwhals.read_csv` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.read_csv(source, backend=backend, **kwargs)) @@ -1779,31 +1262,9 @@ def scan_csv( ) -> LazyFrame[Any]: """Lazily read from a CSV file. - For the libraries that do not support lazy dataframes, the function reads - a csv file eagerly and then converts the resulting dataframe to a lazyframe. - - Arguments: - source: Path to a file. - backend: The eager backend for DataFrame creation. - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - kwargs: Extra keyword arguments which are passed to the native CSV reader. - For example, you could use - `nw.scan_csv('file.csv', backend=pd, engine='pyarrow')`. - - Returns: - LazyFrame. + See `narwhals.scan_csv` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.scan_csv(source, backend=backend, **kwargs)) @@ -1819,28 +1280,9 @@ def read_parquet( ) -> DataFrame[Any]: """Read into a DataFrame from a parquet file. - Arguments: - source: Path to a file. - backend: The eager backend for DataFrame creation. - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN` or `CUDF`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - kwargs: Extra keyword arguments which are passed to the native parquet reader. - For example, you could use - `nw.read_parquet('file.parquet', backend=pd, engine='pyarrow')`. - - Returns: - DataFrame. + See `narwhals.read_parquet` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.read_parquet(source, backend=backend, **kwargs)) @@ -1856,45 +1298,9 @@ def scan_parquet( ) -> LazyFrame[Any]: """Lazily read from a parquet file. - For the libraries that do not support lazy dataframes, the function reads - a parquet file eagerly and then converts the resulting dataframe to a lazyframe. - - Note: - Spark like backends require a session object to be passed in `kwargs`. - - For instance: - - ```py - import narwhals as nw - from sqlframe.duckdb import DuckDBSession - - nw.scan_parquet(source, backend="sqlframe", session=DuckDBSession()) - ``` - - Arguments: - source: Path to a file. - backend: The eager backend for DataFrame creation. - `backend` can be specified in various ways - - - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, - `POLARS`, `MODIN`, `CUDF`, `PYSPARK` or `SQLFRAME`. - - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"`, `"cudf"`, - `"pyspark"` or `"sqlframe"`. - - Directly as a module `pandas`, `pyarrow`, `polars`, `modin`, `cudf`, - `pyspark.sql` or `sqlframe`. - native_namespace: The native library to use for DataFrame creation. - - *Deprecated* (v1.31.0) - - Please use `backend` instead. Note that `native_namespace` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - kwargs: Extra keyword arguments which are passed to the native parquet reader. - For example, you could use - `nw.scan_parquet('file.parquet', backend=pd, engine='pyarrow')`. - - Returns: - LazyFrame. + See `narwhals.scan_parquet` for full docstring. Note that `native_namespace` is + an is the same as `backend` but only accepts module types - for new code, we + recommend using `backend`, as that's available beyond just `narwhals.stable.v1`. """ backend = cast("ModuleType | Implementation | str", backend) return _stableify(nw_f.scan_parquet(source, backend=backend, **kwargs)) diff --git a/narwhals/stable/v2/__init__.py b/narwhals/stable/v2/__init__.py new file mode 100644 index 0000000000..93cd912b8a --- /dev/null +++ b/narwhals/stable/v2/__init__.py @@ -0,0 +1,1257 @@ +from __future__ import annotations + +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable, Literal, cast, overload + +import narwhals as nw +from narwhals import exceptions, functions as nw_f +from narwhals._typing_compat import TypeVar, assert_never +from narwhals._utils import ( + Implementation, + Version, + generate_temporary_column_name, + inherit_doc, + is_ordered_categorical, + maybe_align_index, + maybe_convert_dtypes, + maybe_get_index, + maybe_reset_index, + maybe_set_index, + not_implemented, +) +from narwhals.dataframe import DataFrame as NwDataFrame, LazyFrame as NwLazyFrame +from narwhals.dtypes import ( + Array, + Binary, + Boolean, + Categorical, + Date, + Datetime, + Decimal, + Duration, + Enum, + Field, + Float32, + Float64, + Int8, + Int16, + Int32, + Int64, + Int128, + List, + Object, + String, + Struct, + Time, + UInt8, + UInt16, + UInt32, + UInt64, + UInt128, + Unknown, +) +from narwhals.expr import Expr as NwExpr +from narwhals.functions import _new_series_impl, concat, show_versions +from narwhals.schema import Schema as NwSchema +from narwhals.series import Series as NwSeries +from narwhals.stable.v2 import dependencies, dtypes, selectors +from narwhals.translate import _from_native_impl, get_native_namespace, to_py_scalar +from narwhals.typing import IntoDataFrameT, IntoFrameT + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + from types import ModuleType + + from typing_extensions import ParamSpec, Self + + from narwhals._translate import IntoArrowTable + from narwhals.dataframe import MultiColSelector, MultiIndexSelector + from narwhals.dtypes import DType + from narwhals.typing import ( + IntoDType, + IntoExpr, + IntoFrame, + IntoSeries, + NonNestedLiteral, + SingleColSelector, + SingleIndexSelector, + _1DArray, + _2DArray, + ) + + DataFrameT = TypeVar("DataFrameT", bound="DataFrame[Any]") + LazyFrameT = TypeVar("LazyFrameT", bound="LazyFrame[Any]") + SeriesT = TypeVar("SeriesT", bound="Series[Any]") + T = TypeVar("T", default=Any) + P = ParamSpec("P") + R = TypeVar("R") + +IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries", default=Any) + + +class DataFrame(NwDataFrame[IntoDataFrameT]): + @inherit_doc(NwDataFrame) + def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + assert df._version is Version.V2 # noqa: S101 + super().__init__(df, level=level) + + # We need to override any method which don't return Self so that type + # annotations are correct. + + @property + def _series(self) -> type[Series[Any]]: + return cast("type[Series[Any]]", Series) + + @property + def _lazyframe(self) -> type[LazyFrame[Any]]: + return cast("type[LazyFrame[Any]]", LazyFrame) + + @overload + def __getitem__(self, item: tuple[SingleIndexSelector, SingleColSelector]) -> Any: ... + + @overload + def __getitem__( # type: ignore[overload-overlap] + self, item: str | tuple[MultiIndexSelector, SingleColSelector] + ) -> Series[Any]: ... + + @overload + def __getitem__( + self, + item: ( + SingleIndexSelector + | MultiIndexSelector + | MultiColSelector + | tuple[SingleIndexSelector, MultiColSelector] + | tuple[MultiIndexSelector, MultiColSelector] + ), + ) -> Self: ... + def __getitem__( + self, + item: ( + SingleIndexSelector + | SingleColSelector + | MultiColSelector + | MultiIndexSelector + | tuple[SingleIndexSelector, SingleColSelector] + | tuple[SingleIndexSelector, MultiColSelector] + | tuple[MultiIndexSelector, SingleColSelector] + | tuple[MultiIndexSelector, MultiColSelector] + ), + ) -> Series[Any] | Self | Any: + return super().__getitem__(item) + + def get_column(self, name: str) -> Series: + # Type checkers complain that `nw.Series` is not assignable to `nw.v2.stable.Series`. + # However the return type actually is `nw.v2.stable.Series`, check `tests/v2_test.py`. + return super().get_column(name) # type: ignore[return-value] + + def lazy( + self, backend: ModuleType | Implementation | str | None = None + ) -> LazyFrame[Any]: + return _stableify(super().lazy(backend=backend)) + + @overload # type: ignore[override] + def to_dict(self, *, as_series: Literal[True] = ...) -> dict[str, Series[Any]]: ... + @overload + def to_dict(self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ... + @overload + def to_dict( + self, *, as_series: bool + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: ... + def to_dict( + self, *, as_series: bool = True + ) -> dict[str, Series[Any]] | dict[str, list[Any]]: + # Type checkers complain that `nw.Series` is not assignable to `nw.v2.stable.Series`. + # However the return type actually is `nw.v2.stable.Series`, check `tests/v2_test.py::test_to_dict_as_series`. + return super().to_dict(as_series=as_series) # type: ignore[return-value] + + def is_duplicated(self) -> Series[Any]: + return _stableify(super().is_duplicated()) + + def is_unique(self) -> Series[Any]: + return _stableify(super().is_unique()) + + +class LazyFrame(NwLazyFrame[IntoFrameT]): + @inherit_doc(NwLazyFrame) + def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: + assert df._version is Version.V2 # noqa: S101 + super().__init__(df, level=level) + + @property + def _dataframe(self) -> type[DataFrame[Any]]: + return DataFrame + + def collect( + self, backend: ModuleType | Implementation | str | None = None, **kwargs: Any + ) -> DataFrame[Any]: + return _stableify(super().collect(backend=backend, **kwargs)) + + +class Series(NwSeries[IntoSeriesT]): + @inherit_doc(NwSeries) + def __init__( + self, series: Any, *, level: Literal["full", "lazy", "interchange"] + ) -> None: + assert series._version is Version.V2 # noqa: S101 + super().__init__(series, level=level) + + # We need to override any method which don't return Self so that type + # annotations are correct. + + @property + def _dataframe(self) -> type[DataFrame[Any]]: + return DataFrame + + def to_frame(self) -> DataFrame[Any]: + return _stableify(super().to_frame()) + + def value_counts( + self, + *, + sort: bool = False, + parallel: bool = False, + name: str | None = None, + normalize: bool = False, + ) -> DataFrame[Any]: + return _stableify( + super().value_counts( + sort=sort, parallel=parallel, name=name, normalize=normalize + ) + ) + + # Too unstable to consider including here. + hist: Any = not_implemented() + + +class Expr(NwExpr): ... + + +class Schema(NwSchema): + _version = Version.V2 + + @inherit_doc(NwSchema) + def __init__( + self, schema: Mapping[str, DType] | Iterable[tuple[str, DType]] | None = None + ) -> None: + super().__init__(schema) + + +@overload +def _stableify(obj: NwDataFrame[IntoFrameT]) -> DataFrame[IntoFrameT]: ... +@overload +def _stableify(obj: NwLazyFrame[IntoFrameT]) -> LazyFrame[IntoFrameT]: ... +@overload +def _stableify(obj: NwSeries[IntoSeriesT]) -> Series[IntoSeriesT]: ... +@overload +def _stableify(obj: NwExpr) -> Expr: ... + + +def _stableify( + obj: NwDataFrame[IntoFrameT] + | NwLazyFrame[IntoFrameT] + | NwSeries[IntoSeriesT] + | NwExpr, +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT] | Expr: + if isinstance(obj, NwDataFrame): + return DataFrame(obj._compliant_frame._with_version(Version.V2), level=obj._level) + if isinstance(obj, NwLazyFrame): + return LazyFrame(obj._compliant_frame._with_version(Version.V2), level=obj._level) + if isinstance(obj, NwSeries): + return Series(obj._compliant_series._with_version(Version.V2), level=obj._level) + if isinstance(obj, NwExpr): + return Expr(obj._to_compliant_expr, obj._metadata) + assert_never(obj) + + +@overload +def from_native(native_object: SeriesT, **kwds: Any) -> SeriesT: ... + + +@overload +def from_native(native_object: DataFrameT, **kwds: Any) -> DataFrameT: ... + + +@overload +def from_native(native_object: LazyFrameT, **kwds: Any) -> LazyFrameT: ... + + +@overload +def from_native( + native_object: DataFrameT | LazyFrameT, **kwds: Any +) -> DataFrameT | LazyFrameT: ... + + +@overload +def from_native( + native_object: IntoDataFrameT | IntoSeries, + *, + pass_through: Literal[True], + eager_only: Literal[False] = ..., + series_only: Literal[False] = ..., + allow_series: Literal[True], +) -> DataFrame[IntoDataFrameT]: ... + + +@overload +def from_native( + native_object: IntoDataFrameT | IntoSeriesT, + *, + pass_through: Literal[True], + eager_only: Literal[True], + series_only: Literal[False] = ..., + allow_series: Literal[True], +) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... + + +@overload +def from_native( + native_object: IntoDataFrameT, + *, + pass_through: Literal[True], + eager_only: Literal[False] = ..., + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> DataFrame[IntoDataFrameT]: ... + + +@overload +def from_native( + native_object: T, + *, + pass_through: Literal[True], + eager_only: Literal[False] = ..., + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> T: ... + + +@overload +def from_native( + native_object: IntoDataFrameT, + *, + pass_through: Literal[True], + eager_only: Literal[True], + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> DataFrame[IntoDataFrameT]: ... + + +@overload +def from_native( + native_object: T, + *, + pass_through: Literal[True], + eager_only: Literal[True], + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> T: ... + + +@overload +def from_native( + native_object: IntoSeriesT, + *, + pass_through: Literal[True], + eager_only: Literal[False] = ..., + series_only: Literal[True], + allow_series: None = ..., +) -> Series[IntoSeriesT]: ... + + +@overload +def from_native( + native_object: IntoDataFrameT, + *, + pass_through: Literal[False] = ..., + eager_only: Literal[False] = ..., + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> DataFrame[IntoDataFrameT]: ... + + +@overload +def from_native( + native_object: IntoDataFrameT, + *, + pass_through: Literal[False] = ..., + eager_only: Literal[True], + series_only: Literal[False] = ..., + allow_series: None = ..., +) -> DataFrame[IntoDataFrameT]: ... + + +@overload +def from_native( + native_object: IntoFrame | IntoSeries, + *, + pass_through: Literal[False] = ..., + eager_only: Literal[False] = ..., + series_only: Literal[False] = ..., + allow_series: Literal[True], +) -> DataFrame[Any] | LazyFrame[Any] | Series[Any]: ... + + +@overload +def from_native( + native_object: IntoSeriesT, + *, + pass_through: Literal[False] = ..., + eager_only: Literal[False] = ..., + series_only: Literal[True], + allow_series: None = ..., +) -> Series[IntoSeriesT]: ... + + +# All params passed in as variables +@overload +def from_native( + native_object: Any, + *, + pass_through: bool, + eager_only: bool, + series_only: bool, + allow_series: bool | None, +) -> Any: ... + + +def from_native( # noqa: D417 + native_object: IntoFrameT | IntoFrame | IntoSeriesT | IntoSeries | T, + *, + pass_through: bool = False, + eager_only: bool = False, + series_only: bool = False, + allow_series: bool | None = None, + **kwds: Any, +) -> LazyFrame[IntoFrameT] | DataFrame[IntoFrameT] | Series[IntoSeriesT] | T: + """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. + + Arguments: + native_object: Raw object from user. + Depending on the other arguments, input object can be + + - a Dataframe / Lazyframe / Series supported by Narwhals (pandas, Polars, PyArrow, ...) + - an object which implements `__narwhals_dataframe__`, `__narwhals_lazyframe__`, + or `__narwhals_series__` + pass_through: Determine what happens if the object can't be converted to Narwhals + + - `False` (default): raise an error + - `True`: pass object through as-is + eager_only: Whether to only allow eager objects + + - `False` (default): don't require `native_object` to be eager + - `True`: only convert to Narwhals if `native_object` is eager + series_only: Whether to only allow Series + + - `False` (default): don't require `native_object` to be a Series + - `True`: only convert to Narwhals if `native_object` is a Series + allow_series: Whether to allow Series (default is only Dataframe / Lazyframe) + + - `False` or `None` (default): don't convert to Narwhals if `native_object` is a Series + - `True`: allow `native_object` to be a Series + + Returns: + DataFrame, LazyFrame, Series, or original object, depending + on which combination of parameters was passed. + """ + # Early returns + if isinstance(native_object, (DataFrame, LazyFrame)) and not series_only: + return native_object + if isinstance(native_object, Series) and (series_only or allow_series): + return native_object + + if kwds: + msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}" + raise TypeError(msg) + + return _from_native_impl( # type: ignore[no-any-return] + native_object, + pass_through=pass_through, + eager_only=eager_only, + series_only=series_only, + allow_series=allow_series, + version=Version.V2, + ) + + +@overload +def to_native( + narwhals_object: DataFrame[IntoDataFrameT], *, pass_through: Literal[False] = ... +) -> IntoDataFrameT: ... +@overload +def to_native( + narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ... +) -> IntoFrameT: ... +@overload +def to_native( + narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... +) -> IntoSeriesT: ... +@overload +def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... + + +def to_native( + narwhals_object: DataFrame[IntoDataFrameT] + | LazyFrame[IntoFrameT] + | Series[IntoSeriesT], + *, + pass_through: bool = False, +) -> IntoFrameT | IntoSeriesT | Any: + """Convert Narwhals object to native one. + + Arguments: + narwhals_object: Narwhals object. + pass_through: Determine what happens if `narwhals_object` isn't a Narwhals class + + - `False` (default): raise an error + - `True`: pass object through as-is + + Returns: + Object of class that user started with. + """ + return nw.to_native(narwhals_object, pass_through=pass_through) + + +def narwhalify( + func: Callable[..., Any] | None = None, + *, + pass_through: bool = True, + eager_only: bool = False, + series_only: bool = False, + allow_series: bool | None = True, +) -> Callable[..., Any]: + """Decorate function so it becomes dataframe-agnostic. + + This will try to convert any dataframe/series-like object into the Narwhals + respective DataFrame/Series, while leaving the other parameters as they are. + Similarly, if the output of the function is a Narwhals DataFrame or Series, it will be + converted back to the original dataframe/series type, while if the output is another + type it will be left as is. + By setting `pass_through=False`, then every input and every output will be required to be a + dataframe/series-like object. + + Arguments: + func: Function to wrap in a `from_native`-`to_native` block. + pass_through: Determine what happens if the object can't be converted to Narwhals + + - `False`: raise an error + - `True` (default): pass object through as-is + eager_only: Whether to only allow eager objects + + - `False` (default): don't require `native_object` to be eager + - `True`: only convert to Narwhals if `native_object` is eager + series_only: Whether to only allow Series + + - `False` (default): don't require `native_object` to be a Series + - `True`: only convert to Narwhals if `native_object` is a Series + allow_series: Whether to allow Series (default is only Dataframe / Lazyframe) + + - `False` or `None`: don't convert to Narwhals if `native_object` is a Series + - `True` (default): allow `native_object` to be a Series + + Returns: + Decorated function. + """ + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + args = [ + from_native( + arg, + pass_through=pass_through, + eager_only=eager_only, + series_only=series_only, + allow_series=allow_series, + ) + for arg in args + ] # type: ignore[assignment] + + kwargs = { + name: from_native( + value, + pass_through=pass_through, + eager_only=eager_only, + series_only=series_only, + allow_series=allow_series, + ) + for name, value in kwargs.items() + } + + backends = { + b() + for v in (*args, *kwargs.values()) + if (b := getattr(v, "__native_namespace__", None)) + } + + if backends.__len__() > 1: + msg = "Found multiple backends. Make sure that all dataframe/series inputs come from the same backend." + raise ValueError(msg) + + result = func(*args, **kwargs) + + return to_native(result, pass_through=pass_through) + + return wrapper + + if func is None: + return decorator + else: + # If func is not None, it means the decorator is used without arguments + return decorator(func) + + +def all() -> Expr: + """Instantiate an expression representing all columns. + + Returns: + A new expression. + """ + return _stableify(nw.all()) + + +def col(*names: str | Iterable[str]) -> Expr: + """Creates an expression that references one or more columns by their name(s). + + Arguments: + names: Name(s) of the columns to use. + + Returns: + A new expression. + """ + return _stableify(nw.col(*names)) + + +def exclude(*names: str | Iterable[str]) -> Expr: + """Creates an expression that excludes columns by their name(s). + + Arguments: + names: Name(s) of the columns to exclude. + + Returns: + A new expression. + """ + return _stableify(nw.exclude(*names)) + + +def nth(*indices: int | Sequence[int]) -> Expr: + """Creates an expression that references one or more columns by their index(es). + + Notes: + `nth` is not supported for Polars version<1.0.0. Please use + [`narwhals.col`][] instead. + + Arguments: + indices: One or more indices representing the columns to retrieve. + + Returns: + A new expression. + """ + return _stableify(nw.nth(*indices)) + + +def len() -> Expr: + """Return the number of rows. + + Returns: + A new expression. + """ + return _stableify(nw.len()) + + +def lit(value: NonNestedLiteral, dtype: IntoDType | None = None) -> Expr: + """Return an expression representing a literal value. + + Arguments: + value: The value to use as literal. + dtype: The data type of the literal value. If not provided, the data type will + be inferred by the native library. + + Returns: + A new expression. + """ + return _stableify(nw.lit(value, dtype)) + + +def min(*columns: str) -> Expr: + """Return the minimum value. + + Note: + Syntactic sugar for ``nw.col(columns).min()``. + + Arguments: + columns: Name(s) of the columns to use in the aggregation function. + + Returns: + A new expression. + """ + return _stableify(nw.min(*columns)) + + +def max(*columns: str) -> Expr: + """Return the maximum value. + + Note: + Syntactic sugar for ``nw.col(columns).max()``. + + Arguments: + columns: Name(s) of the columns to use in the aggregation function. + + Returns: + A new expression. + """ + return _stableify(nw.max(*columns)) + + +def mean(*columns: str) -> Expr: + """Get the mean value. + + Note: + Syntactic sugar for ``nw.col(columns).mean()`` + + Arguments: + columns: Name(s) of the columns to use in the aggregation function + + Returns: + A new expression. + """ + return _stableify(nw.mean(*columns)) + + +def median(*columns: str) -> Expr: + """Get the median value. + + Notes: + - Syntactic sugar for ``nw.col(columns).median()`` + - Results might slightly differ across backends due to differences in the + underlying algorithms used to compute the median. + + Arguments: + columns: Name(s) of the columns to use in the aggregation function + + Returns: + A new expression. + """ + return _stableify(nw.median(*columns)) + + +def sum(*columns: str) -> Expr: + """Sum all values. + + Note: + Syntactic sugar for ``nw.col(columns).sum()`` + + Arguments: + columns: Name(s) of the columns to use in the aggregation function + + Returns: + A new expression. + """ + return _stableify(nw.sum(*columns)) + + +def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: + """Sum all values horizontally across columns. + + Warning: + Unlike Polars, we support horizontal sum over numeric columns only. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + + Returns: + A new expression. + """ + return _stableify(nw.sum_horizontal(*exprs)) + + +def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: + r"""Compute the bitwise AND horizontally across columns. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + ignore_nulls: Whether to ignore nulls: + + - If `True`, null values are ignored. If there are no elements, the result + is `True`. + - If `False`, Kleene logic is followed. Note that this is not allowed for + pandas with classical NumPy dtypes when null values are present. + + Returns: + A new expression. + """ + return _stableify(nw.all_horizontal(*exprs, ignore_nulls=ignore_nulls)) + + +def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: + r"""Compute the bitwise OR horizontally across columns. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + ignore_nulls: Whether to ignore nulls: + + - If `True`, null values are ignored. If there are no elements, the result + is `False`. + - If `False`, Kleene logic is followed. Note that this is not allowed for + pandas with classical NumPy dtypes when null values are present. + + Returns: + A new expression. + """ + return _stableify(nw.any_horizontal(*exprs, ignore_nulls=ignore_nulls)) + + +def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: + """Compute the mean of all values horizontally across columns. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + + Returns: + A new expression. + """ + return _stableify(nw.mean_horizontal(*exprs)) + + +def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: + """Get the minimum value horizontally across columns. + + Notes: + We support `min_horizontal` over numeric columns only. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + + Returns: + A new expression. + """ + return _stableify(nw.min_horizontal(*exprs)) + + +def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: + """Get the maximum value horizontally across columns. + + Notes: + We support `max_horizontal` over numeric columns only. + + Arguments: + exprs: Name(s) of the columns to use in the aggregation function. Accepts + expression input. + + Returns: + A new expression. + """ + return _stableify(nw.max_horizontal(*exprs)) + + +def concat_str( + exprs: IntoExpr | Iterable[IntoExpr], + *more_exprs: IntoExpr, + separator: str = "", + ignore_nulls: bool = False, +) -> Expr: + r"""Horizontally concatenate columns into a single string column. + + Arguments: + exprs: Columns to concatenate into a single string column. Accepts expression + input. Strings are parsed as column names, other non-expression inputs are + parsed as literals. Non-`String` columns are cast to `String`. + *more_exprs: Additional columns to concatenate into a single string column, + specified as positional arguments. + separator: String that will be used to separate the values of each column. + ignore_nulls: Ignore null values (default is `False`). + If set to `False`, null values will be propagated and if the row contains any + null values, the output is null. + + Returns: + A new expression. + """ + return _stableify( + nw.concat_str(exprs, *more_exprs, separator=separator, ignore_nulls=ignore_nulls) + ) + + +def coalesce(exprs: IntoExpr | Iterable[IntoExpr], *more_exprs: IntoExpr) -> Expr: + """Folds the columns from left to right, keeping the first non-null value. + + Arguments: + exprs: Columns to coalesce, must be a str, nw.Expr, or nw.Series + where strings are parsed as column names and both nw.Expr/nw.Series + are passed through as-is. Scalar values must be wrapped in `nw.lit`. + + *more_exprs: Additional columns to coalesce, specified as positional arguments. + + Raises: + TypeError: If any of the inputs are not a str, nw.Expr, or nw.Series. + + Returns: + A new expression. + """ + return _stableify(nw.coalesce(exprs, *more_exprs)) + + +class When(nw_f.When): + @classmethod + def from_when(cls, when: nw_f.When) -> When: + return cls(when._predicate) + + def then(self, value: IntoExpr | NonNestedLiteral | _1DArray) -> Then: + return Then.from_then(super().then(value)) + + +class Then(nw_f.Then, Expr): + @classmethod + def from_then(cls, then: nw_f.Then) -> Then: + return cls(then._to_compliant_expr, then._metadata) + + def otherwise(self, value: IntoExpr | NonNestedLiteral | _1DArray) -> Expr: + return _stableify(super().otherwise(value)) + + +def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When: + """Start a `when-then-otherwise` expression. + + Expression similar to an `if-else` statement in Python. Always initiated by a + `pl.when().then()`, and optionally followed by a + `.otherwise()` can be appended at the end. If not + appended, and the condition is not `True`, `None` will be returned. + + Info: + Chaining multiple `.when().then()` statements is currently + not supported. + See [Narwhals#668](https://github.com/narwhals-dev/narwhals/issues/668). + + Arguments: + predicates: Condition(s) that must be met in order to apply the subsequent + statement. Accepts one or more boolean expressions, which are implicitly + combined with `&`. String input is parsed as a column name. + + Returns: + A "when" object, which `.then` can be called on. + """ + return When.from_when(nw_f.when(*predicates)) + + +def new_series( + name: str, + values: Any, + dtype: IntoDType | None = None, + *, + backend: ModuleType | Implementation | str, +) -> Series[Any]: + """Instantiate Narwhals Series from iterable (e.g. list or array). + + Arguments: + name: Name of resulting Series. + values: Values of make Series from. + dtype: (Narwhals) dtype. If not provided, the native library + may auto-infer it from `values`. + backend: specifies which eager backend instantiate to. + + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + + Returns: + A new Series + """ + return _stableify(_new_series_impl(name, values, dtype, backend=backend)) + + +def from_arrow( + native_frame: IntoArrowTable, *, backend: ModuleType | Implementation | str +) -> DataFrame[Any]: + """Construct a DataFrame from an object which supports the PyCapsule Interface. + + Arguments: + native_frame: Object which implements `__arrow_c_stream__`. + backend: specifies which eager backend instantiate to. + + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + + Returns: + A new DataFrame. + """ + return _stableify(nw_f.from_arrow(native_frame, backend=backend)) + + +def from_dict( + data: Mapping[str, Any], + schema: Mapping[str, DType] | Schema | None = None, + *, + backend: ModuleType | Implementation | str | None = None, +) -> DataFrame[Any]: + """Instantiate DataFrame from dictionary. + + Indexes (if present, for pandas-like backends) are aligned following + the [left-hand-rule](../concepts/pandas_index.md/). + + Notes: + For pandas-like dataframes, conversion to schema is applied after dataframe + creation. + + Arguments: + data: Dictionary to create DataFrame from. + schema: The DataFrame schema as Schema or dict of {name: type}. If not + specified, the schema will be inferred by the native library. + backend: specifies which eager backend instantiate to. Only + necessary if inputs are not Narwhals Series. + + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + + Returns: + A new DataFrame. + """ + return _stableify(nw_f.from_dict(data, schema, backend=backend)) + + +def from_numpy( + data: _2DArray, + schema: Mapping[str, DType] | Schema | Sequence[str] | None = None, + *, + backend: ModuleType | Implementation | str, +) -> DataFrame[Any]: + """Construct a DataFrame from a NumPy ndarray. + + Notes: + Only row orientation is currently supported. + + For pandas-like dataframes, conversion to schema is applied after dataframe + creation. + + Arguments: + data: Two-dimensional data represented as a NumPy ndarray. + schema: The DataFrame schema as Schema, dict of {name: type}, or a sequence of str. + backend: specifies which eager backend instantiate to. + + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + + Returns: + A new DataFrame. + """ + return _stableify(nw_f.from_numpy(data, schema, backend=backend)) + + +def read_csv( + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any +) -> DataFrame[Any]: + """Read a CSV file into a DataFrame. + + Arguments: + source: Path to a file. + backend: The eager backend for DataFrame creation. + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + kwargs: Extra keyword arguments which are passed to the native CSV reader. + For example, you could use + `nw.read_csv('file.csv', backend='pandas', engine='pyarrow')`. + + Returns: + DataFrame. + """ + return _stableify(nw_f.read_csv(source, backend=backend, **kwargs)) + + +def scan_csv( + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any +) -> LazyFrame[Any]: + """Lazily read from a CSV file. + + For the libraries that do not support lazy dataframes, the function reads + a csv file eagerly and then converts the resulting dataframe to a lazyframe. + + Arguments: + source: Path to a file. + backend: The eager backend for DataFrame creation. + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + kwargs: Extra keyword arguments which are passed to the native CSV reader. + For example, you could use + `nw.scan_csv('file.csv', backend=pd, engine='pyarrow')`. + + Returns: + LazyFrame. + """ + return _stableify(nw_f.scan_csv(source, backend=backend, **kwargs)) + + +def read_parquet( + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any +) -> DataFrame[Any]: + """Read into a DataFrame from a parquet file. + + Arguments: + source: Path to a file. + backend: The eager backend for DataFrame creation. + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN` or `CUDF`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`. + kwargs: Extra keyword arguments which are passed to the native parquet reader. + For example, you could use + `nw.read_parquet('file.parquet', backend=pd, engine='pyarrow')`. + + Returns: + DataFrame. + """ + return _stableify(nw_f.read_parquet(source, backend=backend, **kwargs)) + + +def scan_parquet( + source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any +) -> LazyFrame[Any]: + """Lazily read from a parquet file. + + For the libraries that do not support lazy dataframes, the function reads + a parquet file eagerly and then converts the resulting dataframe to a lazyframe. + + Note: + Spark like backends require a session object to be passed in `kwargs`. + + For instance: + + ```py + import narwhals as nw + from sqlframe.duckdb import DuckDBSession + + nw.scan_parquet(source, backend="sqlframe", session=DuckDBSession()) + ``` + + Arguments: + source: Path to a file. + backend: The eager backend for DataFrame creation. + `backend` can be specified in various ways + + - As `Implementation.` with `BACKEND` being `PANDAS`, `PYARROW`, + `POLARS`, `MODIN`, `CUDF`, `PYSPARK` or `SQLFRAME`. + - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"`, `"cudf"`, + `"pyspark"` or `"sqlframe"`. + - Directly as a module `pandas`, `pyarrow`, `polars`, `modin`, `cudf`, + `pyspark.sql` or `sqlframe`. + kwargs: Extra keyword arguments which are passed to the native parquet reader. + For example, you could use + `nw.scan_parquet('file.parquet', backend=pd, engine='pyarrow')`. + + Returns: + LazyFrame. + """ + return _stableify(nw_f.scan_parquet(source, backend=backend, **kwargs)) + + +__all__ = [ + "Array", + "Binary", + "Boolean", + "Categorical", + "DataFrame", + "Date", + "Datetime", + "Decimal", + "Duration", + "Enum", + "Expr", + "Field", + "Float32", + "Float64", + "Implementation", + "Int8", + "Int16", + "Int32", + "Int64", + "Int128", + "LazyFrame", + "List", + "Object", + "Schema", + "Series", + "String", + "Struct", + "Time", + "UInt8", + "UInt16", + "UInt32", + "UInt64", + "UInt128", + "Unknown", + "all", + "all_horizontal", + "any_horizontal", + "coalesce", + "col", + "concat", + "concat_str", + "dependencies", + "dtypes", + "dtypes", + "exceptions", + "exclude", + "from_arrow", + "from_dict", + "from_native", + "from_numpy", + "generate_temporary_column_name", + "get_native_namespace", + "is_ordered_categorical", + "len", + "lit", + "max", + "max_horizontal", + "maybe_align_index", + "maybe_convert_dtypes", + "maybe_get_index", + "maybe_reset_index", + "maybe_set_index", + "mean", + "mean_horizontal", + "median", + "min", + "min_horizontal", + "narwhalify", + "new_series", + "nth", + "read_csv", + "read_parquet", + "scan_csv", + "scan_parquet", + "selectors", + "selectors", + "show_versions", + "sum", + "sum_horizontal", + "to_native", + "to_py_scalar", + "when", +] diff --git a/narwhals/stable/v2/_namespace.py b/narwhals/stable/v2/_namespace.py new file mode 100644 index 0000000000..b30093cc6c --- /dev/null +++ b/narwhals/stable/v2/_namespace.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from narwhals._compliant.typing import CompliantNamespaceT_co +from narwhals._namespace import Namespace as NwNamespace +from narwhals._utils import Version + +__all__ = ["Namespace"] + + +class Namespace(NwNamespace[CompliantNamespaceT_co], version=Version.V2): ... diff --git a/narwhals/stable/v2/dependencies.py b/narwhals/stable/v2/dependencies.py new file mode 100644 index 0000000000..09dac96892 --- /dev/null +++ b/narwhals/stable/v2/dependencies.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from narwhals.dependencies import * # noqa: F403 diff --git a/narwhals/stable/v2/dtypes.py b/narwhals/stable/v2/dtypes.py new file mode 100644 index 0000000000..b2d9cb0a21 --- /dev/null +++ b/narwhals/stable/v2/dtypes.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from narwhals.dtypes import * # noqa: F403 diff --git a/narwhals/stable/v2/selectors.py b/narwhals/stable/v2/selectors.py new file mode 100644 index 0000000000..79089a2761 --- /dev/null +++ b/narwhals/stable/v2/selectors.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from narwhals.selectors import * # noqa: F403 diff --git a/narwhals/stable/v2/typing.py b/narwhals/stable/v2/typing.py new file mode 100644 index 0000000000..16d42804c2 --- /dev/null +++ b/narwhals/stable/v2/typing.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union + +if TYPE_CHECKING: + import sys + + from narwhals.stable.v2 import DataFrame, LazyFrame + + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + + from narwhals.stable.v2 import Expr, Series + + # All dataframes supported by Narwhals have a + # `columns` property. Their similarities don't extend + # _that_ much further unfortunately... + class NativeFrame(Protocol): + @property + def columns(self) -> Any: ... + + def join(self, *args: Any, **kwargs: Any) -> Any: ... + + class NativeSeries(Protocol): + def __len__(self) -> int: ... + + +IntoExpr: TypeAlias = Union["Expr", str, "Series[Any]"] +"""Anything which can be converted to an expression. + +Use this to mean "either a Narwhals expression, or something +which can be converted into one". For example, `exprs` in `DataFrame.select` is +typed to accept `IntoExpr`, as it can either accept a `nw.Expr` +(e.g. `df.select(nw.col('a'))`) or a string which will be interpreted as a +`nw.Expr`, e.g. `df.select('a')`. +""" + +IntoDataFrame: TypeAlias = Union["NativeFrame", "DataFrame[Any]"] +"""Anything which can be converted to a Narwhals DataFrame. + +Use this if your function accepts a narwhalifiable object but doesn't care about its backend. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import IntoDataFrame + >>> def agnostic_shape(df_native: IntoDataFrame) -> tuple[int, int]: + ... df = nw.from_native(df_native, eager_only=True) + ... return df.shape +""" + +IntoFrame: TypeAlias = Union["NativeFrame", "DataFrame[Any]", "LazyFrame[Any]"] +"""Anything which can be converted to a Narwhals DataFrame or LazyFrame. + +Use this if your function can accept an object which can be converted to either +`nw.DataFrame` or `nw.LazyFrame` and it doesn't care about its backend. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import IntoFrame + >>> def agnostic_columns(df_native: IntoFrame) -> list[str]: + ... df = nw.from_native(df_native) + ... return df.collect_schema().names() +""" + +Frame: TypeAlias = Union["DataFrame[Any]", "LazyFrame[Any]"] +"""Narwhals DataFrame or Narwhals LazyFrame. + +Use this if your function can work with either and your function doesn't care +about its backend. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import Frame + >>> @nw.narwhalify + ... def agnostic_columns(df: Frame) -> list[str]: + ... return df.columns +""" + +IntoSeries: TypeAlias = Union["Series[Any]", "NativeSeries"] +"""Anything which can be converted to a Narwhals Series. + +Use this if your function can accept an object which can be converted to `nw.Series` +and it doesn't care about its backend. + +Examples: + >>> from typing import Any + >>> import narwhals as nw + >>> from narwhals.typing import IntoSeries + >>> def agnostic_to_list(s_native: IntoSeries) -> list[Any]: + ... s = nw.from_native(s_native) + ... return s.to_list() +""" + +IntoFrameT = TypeVar("IntoFrameT", bound="IntoFrame") +"""TypeVar bound to object convertible to Narwhals DataFrame or Narwhals LazyFrame. + +Use this if your function accepts an object which is convertible to `nw.DataFrame` +or `nw.LazyFrame` and returns an object of the same type. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import IntoFrameT + >>> def agnostic_func(df_native: IntoFrameT) -> IntoFrameT: + ... df = nw.from_native(df_native) + ... return df.with_columns(c=nw.col("a") + 1).to_native() +""" + +IntoDataFrameT = TypeVar("IntoDataFrameT", bound="IntoDataFrame") +"""TypeVar bound to object convertible to Narwhals DataFrame. + +Use this if your function accepts an object which can be converted to `nw.DataFrame` +and returns an object of the same class. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import IntoDataFrameT + >>> def agnostic_func(df_native: IntoDataFrameT) -> IntoDataFrameT: + ... df = nw.from_native(df_native, eager_only=True) + ... return df.with_columns(c=df["a"] + 1).to_native() +""" + +FrameT = TypeVar("FrameT", "DataFrame[Any]", "LazyFrame[Any]") +"""TypeVar bound to Narwhals DataFrame or Narwhals LazyFrame. + +Use this if your function accepts either `nw.DataFrame` or `nw.LazyFrame` and returns +an object of the same kind. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import FrameT + >>> @nw.narwhalify + ... def agnostic_func(df: FrameT) -> FrameT: + ... return df.with_columns(c=nw.col("a") + 1) +""" + +DataFrameT = TypeVar("DataFrameT", bound="DataFrame[Any]") +"""TypeVar bound to Narwhals DataFrame. + +Use this if your function can accept a Narwhals DataFrame and returns a Narwhals +DataFrame backed by the same backend. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import DataFrameT + >>> @nw.narwhalify + >>> def func(df: DataFrameT) -> DataFrameT: + ... return df.with_columns(c=df["a"] + 1) +""" + +IntoSeriesT = TypeVar("IntoSeriesT", bound="IntoSeries") +"""TypeVar bound to object convertible to Narwhals Series. + +Use this if your function accepts an object which can be converted to `nw.Series` +and returns an object of the same class. + +Examples: + >>> import narwhals as nw + >>> from narwhals.typing import IntoSeriesT + >>> def agnostic_abs(s_native: IntoSeriesT) -> IntoSeriesT: + ... s = nw.from_native(s_native, series_only=True) + ... return s.abs().to_native() +""" + + +__all__ = [ + "DataFrameT", + "Frame", + "FrameT", + "IntoDataFrame", + "IntoDataFrameT", + "IntoExpr", + "IntoFrame", + "IntoFrameT", + "IntoSeries", + "IntoSeriesT", +] diff --git a/narwhals/translate.py b/narwhals/translate.py index 497874aa7e..6b7329d989 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -71,23 +71,12 @@ def to_native( | LazyFrame[IntoFrameT] | Series[IntoSeriesT], *, - strict: bool | None = None, - pass_through: bool | None = None, + pass_through: bool = False, ) -> IntoDataFrameT | IntoFrameT | IntoSeriesT | Any: """Convert Narwhals object to native one. Arguments: narwhals_object: Narwhals object. - strict: Determine what happens if `narwhals_object` isn't a Narwhals class - - - `True` (default): raise an error - - `False`: pass object through as-is - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). pass_through: Determine what happens if `narwhals_object` isn't a Narwhals class - `False` (default): raise an error @@ -96,14 +85,9 @@ def to_native( Returns: Object of class that user started with. """ - from narwhals._utils import validate_strict_and_pass_though from narwhals.dataframe import BaseFrame from narwhals.series import Series - pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=False, emit_deprecation_warning=True - ) - if isinstance(narwhals_object, BaseFrame): return narwhals_object._compliant_frame._native_frame if isinstance(narwhals_object, Series): @@ -277,8 +261,7 @@ def from_native( def from_native( # noqa: D417 native_object: IntoLazyFrameT | IntoFrameT | IntoSeriesT | IntoFrame | IntoSeries | T, *, - strict: bool | None = None, - pass_through: bool | None = None, + pass_through: bool = False, eager_only: bool = False, series_only: bool = False, allow_series: bool | None = None, @@ -293,19 +276,9 @@ def from_native( # noqa: D417 - a Dataframe / Lazyframe / Series supported by Narwhals (pandas, Polars, PyArrow, ...) - an object which implements `__narwhals_dataframe__`, `__narwhals_lazyframe__`, or `__narwhals_series__` - strict: Determine what happens if the object can't be converted to Narwhals - - - `True` or `None` (default): raise an error - - `False`: pass object through as-is - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). pass_through: Determine what happens if the object can't be converted to Narwhals - - `False` or `None` (default): raise an error + - `False` (default): raise an error - `True`: pass object through as-is eager_only: Whether to only allow eager objects @@ -324,11 +297,6 @@ def from_native( # noqa: D417 DataFrame, LazyFrame, Series, or original object, depending on which combination of parameters was passed. """ - from narwhals._utils import validate_strict_and_pass_though - - pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=False, emit_deprecation_warning=True - ) if kwds: msg = f"from_native() got an unexpected keyword argument {next(iter(kwds))!r}" raise TypeError(msg) @@ -625,8 +593,7 @@ def _get_native_namespace_single_obj( def narwhalify( func: Callable[..., Any] | None = None, *, - strict: bool | None = None, - pass_through: bool | None = None, + pass_through: bool = True, eager_only: bool = False, series_only: bool = False, allow_series: bool | None = True, @@ -643,20 +610,10 @@ def narwhalify( Arguments: func: Function to wrap in a `from_native`-`to_native` block. - strict: Determine what happens if the object can't be converted to Narwhals - - *Deprecated* (v1.13.0) - - Please use `pass_through` instead. Note that `strict` is still available - (and won't emit a deprecation warning) if you use `narwhals.stable.v1`, - see [perfect backwards compatibility policy](../backcompat.md/). - - - `True` or `None` (default): raise an error - - `False`: pass object through as-is pass_through: Determine what happens if the object can't be converted to Narwhals - - `False` or `None` (default): raise an error - - `True`: pass object through as-is + - `False`: raise an error + - `True` (default): pass object through as-is eager_only: Whether to only allow eager objects - `False` (default): don't require `native_object` to be eager @@ -688,11 +645,6 @@ def narwhalify( ... def agnostic_group_by_sum(df): ... return df.group_by("a").agg(nw.col("b").sum()) """ - from narwhals._utils import validate_strict_and_pass_though - - pass_through = validate_strict_and_pass_though( - strict, pass_through, pass_through_default=True, emit_deprecation_warning=True - ) def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) diff --git a/pyproject.toml b/pyproject.toml index c8ae0989c5..12e0456a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,6 +266,7 @@ fail_under = 80 # This is just for local development, in CI we set it to 100 omit = [ 'narwhals/typing.py', 'narwhals/stable/v1/typing.py', + 'narwhals/stable/v2/typing.py', 'narwhals/this.py', 'narwhals/_arrow/typing.py', 'narwhals/_duckdb/typing.py', diff --git a/tests/from_dict_test.py b/tests/from_dict_test.py index 3c44464af9..a550d38443 100644 --- a/tests/from_dict_test.py +++ b/tests/from_dict_test.py @@ -34,6 +34,13 @@ def test_from_dict_schema(backend: Implementation | str) -> None: schema = {"c": nw.Int16(), "d": nw.Float32()} result = nw.from_dict({"c": [1, 2], "d": [5, 6]}, backend=backend, schema=schema) assert result.collect_schema() == schema + with pytest.deprecated_call(): + result = nw.from_dict( + {"c": [1, 2], "d": [5, 6]}, + native_namespace=backend, # type: ignore[arg-type] + schema=schema, + ) + assert result.collect_schema() == schema @pytest.mark.parametrize("backend", [Implementation.POLARS, "polars"]) @@ -63,17 +70,6 @@ def test_from_dict_with_backend_invalid() -> None: nw.from_dict({"c": [1, 2], "d": [5, 6]}, backend="duckdb") -def test_from_dict_both_backend_and_namespace(constructor: Constructor) -> None: - df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})) - native_namespace = nw.get_native_namespace(df) - with pytest.raises(ValueError, match="Can't pass both"): - nw.from_dict( - {"c": [1, 2], "d": [5, 6]}, - backend="pandas", - native_namespace=native_namespace, - ) - - @pytest.mark.parametrize("backend", [Implementation.POLARS, "polars"]) def test_from_dict_one_native_one_narwhals( constructor: Constructor, backend: Implementation | str diff --git a/tests/stable_api_test.py b/tests/stable_api_test.py index a6fab6142d..86474750f6 100644 --- a/tests/stable_api_test.py +++ b/tests/stable_api_test.py @@ -6,7 +6,7 @@ import pytest import narwhals as nw -import narwhals.stable.v1 as nw_v1 +import narwhals.stable.v2 as nw_v2 if TYPE_CHECKING: from collections.abc import Iterator @@ -19,30 +19,26 @@ def remove_docstring_examples(doc: str) -> str: def test_stable_api_completeness() -> None: - v_1_api = nw_v1.__all__ + v2_api = nw_v2.__all__ main_namespace_api = nw.__all__ - extra = set(v_1_api).difference(main_namespace_api) + extra = set(v2_api).difference(main_namespace_api) assert not extra - missing = set(main_namespace_api).difference(v_1_api).difference({"stable"}) + missing = set(main_namespace_api).difference(v2_api).difference({"stable"}) assert not missing def test_stable_api_docstrings() -> None: main_namespace_api = nw.__all__ for item in main_namespace_api: - if (doc := getdoc(getattr(nw, item))) is None: - continue - if item in {"from_native", "narwhalify", "get_level"}: - # `eager_or_interchange` param was removed from main namespace, - # but is still present in v1 docstring. + if item in {"from_dict"}: + # We keep `native_namespace` around in the main namespace + # until at least hierarchical forecast make a release continue - if item == "Enum": - # In v1 this was Polars-only, after that pandas ordered categoricals - # started to be mapped to it too, so the docstring changed. + if (doc := getdoc(getattr(nw, item))) is None: continue - v1_doc = getdoc(getattr(nw_v1, item)) - assert v1_doc is not None - assert remove_docstring_examples(v1_doc) == remove_docstring_examples(doc), item + v2_doc = getdoc(getattr(nw_v2, item)) + assert v2_doc is not None + assert remove_docstring_examples(v2_doc) == remove_docstring_examples(doc), item def _iter_api_method_docs(obj: Any, *exclude: str) -> Iterator[tuple[str, str]]: @@ -59,35 +55,38 @@ def test_dataframe_docstrings() -> None: pytest.importorskip("polars") import polars as pl - df_v1 = nw_v1.from_native(pl.DataFrame()) + df_v2 = nw_v2.from_native(pl.DataFrame()) df = nw.from_native(pl.DataFrame()) for method_name, doc in _iter_api_method_docs(df): - doc_v1 = getdoc(getattr(df_v1, method_name)) - assert doc_v1 - assert remove_docstring_examples(doc_v1) == remove_docstring_examples(doc) + doc_v2 = getdoc(getattr(df_v2, method_name)) + assert doc_v2 + assert remove_docstring_examples(doc_v2) == remove_docstring_examples(doc) def test_lazyframe_docstrings() -> None: pytest.importorskip("polars") import polars as pl - ldf_v1 = nw_v1.from_native(pl.LazyFrame()) + ldf_v2 = nw_v2.from_native(pl.LazyFrame()) ldf = nw.from_native(pl.LazyFrame()) performance_warning = {"schema", "columns"} deprecated = {"tail", "gather_every"} for method_name, doc in _iter_api_method_docs(ldf, *performance_warning, *deprecated): - doc_v1 = getdoc(getattr(ldf_v1, method_name)) - assert doc_v1 - assert remove_docstring_examples(doc_v1) == remove_docstring_examples(doc) + doc_v2 = getdoc(getattr(ldf_v2, method_name)) + assert doc_v2 + assert remove_docstring_examples(doc_v2) == remove_docstring_examples(doc) def test_series_docstrings() -> None: pytest.importorskip("polars") import polars as pl - ser_v1 = nw_v1.from_native(pl.Series(), series_only=True) + ser_v2 = nw_v2.from_native(pl.Series(), series_only=True) ser = nw.from_native(pl.Series(), series_only=True) for method_name, doc in _iter_api_method_docs(ser): - doc_v1 = getdoc(getattr(ser_v1, method_name)) - assert doc_v1 - assert remove_docstring_examples(doc_v1) == remove_docstring_examples(doc) + if method_name in "hist": + # This is still very unstable in Polars so we don't have it in stable.v2 yet. + continue + doc_v2 = getdoc(getattr(ser_v2, method_name)) + assert doc_v2 + assert remove_docstring_examples(doc_v2) == remove_docstring_examples(doc) diff --git a/tests/translate/narwhalify_test.py b/tests/translate/narwhalify_test.py index 0de2d6c8c0..a703161457 100644 --- a/tests/translate/narwhalify_test.py +++ b/tests/translate/narwhalify_test.py @@ -65,23 +65,6 @@ def func( pd.testing.assert_frame_equal(result, pd.DataFrame(data)) -@pytest.mark.filterwarnings("ignore:.*distutils Version classes are deprecated") -def test_narwhalify_method_invalid() -> None: - with pytest.deprecated_call(match="please use `pass_through` instead"): - - class Foo: - @nw.narwhalify(strict=True, eager_only=True) - def func(self) -> Foo: # pragma: no cover - return self - - @nw.narwhalify(strict=True, eager_only=True) - def fun2(self, df: Any) -> Any: # pragma: no cover - return df - - with pytest.raises(TypeError): - Foo().func() - - def test_narwhalify_invalid() -> None: @nw.narwhalify(pass_through=False) def func() -> None: # pragma: no cover diff --git a/tests/v1_test.py b/tests/v1_test.py index 5ca63ed452..69b2d4ea63 100644 --- a/tests/v1_test.py +++ b/tests/v1_test.py @@ -43,7 +43,8 @@ from typing_extensions import assert_type from narwhals._namespace import EagerAllowed - from narwhals.typing import IntoDataFrameT, _2DArray + from narwhals.stable.v1.typing import IntoDataFrameT + from narwhals.typing import _2DArray from tests.utils import Constructor, ConstructorEager @@ -375,8 +376,6 @@ def test_get_level() -> None: import polars as pl df = pl.DataFrame({"a": [1, 2, 3]}) - with pytest.deprecated_call(): - assert nw.get_level(nw.from_native(df)) == "full" assert nw_v1.get_level(nw_v1.from_native(df)) == "full" assert ( nw_v1.get_level( @@ -397,9 +396,6 @@ def test_any_horizontal() -> None: result = df.select(nw_v1.any_horizontal("a", "b")) expected = {"a": [True, True, None]} assert_equal_data(result, expected) - with pytest.deprecated_call(match="ignore_nulls"): - result = df.select(nw.any_horizontal("a", "b")) - assert_equal_data(result, expected) def test_all_horizontal() -> None: @@ -413,9 +409,6 @@ def test_all_horizontal() -> None: result = df.select(nw_v1.all_horizontal("a", "b")) expected = {"a": [True, None, False]} assert_equal_data(result, expected) - with pytest.deprecated_call(match="ignore_nulls"): - result = df.select(nw.all_horizontal("a", "b")) - assert_equal_data(result, expected) def test_with_row_index(constructor: Constructor) -> None: @@ -540,10 +533,6 @@ def test_from_native_strict_false_typing() -> None: nw_v1.from_native(df, strict=False, eager_only=True) nw_v1.from_native(df, strict=False, eager_or_interchange_only=True) - with pytest.deprecated_call(match="please use `pass_through` instead"): - nw.from_native(df, strict=False) # type: ignore[call-overload] - nw.from_native(df, strict=False, eager_only=True) # type: ignore[call-overload] - def test_from_native_strict_false_invalid() -> None: with pytest.raises(ValueError, match="Cannot pass both `strict`"): @@ -845,11 +834,6 @@ def test_expr_sample(constructor_eager: ConstructorEager) -> None: expected_expr = (2, 1) assert result_expr == expected_expr - with pytest.deprecated_call( - match="is deprecated and will be removed in a future version" - ): - df.select(nw.col("a").sample(n=2)) - def test_is_frame() -> None: pytest.importorskip("pyarrow") @@ -876,13 +860,6 @@ def test_gather_every(constructor_eager: ConstructorEager, n: int, offset: int) expected = {"a": data["a"][offset::n]} assert_equal_data(result, expected) - # Test deprecation for LazyFrame in main namespace - lf = nw.from_native(constructor_eager(data)).lazy() - with pytest.deprecated_call( - match="is deprecated and will be removed in a future version" - ): - lf.gather_every(n=n, offset=offset) - @pytest.mark.parametrize("n", [1, 2]) @pytest.mark.parametrize("offset", [1, 2]) @@ -942,16 +919,6 @@ def test_deprecated_expr_methods() -> None: } assert_equal_data(result, expected) - with pytest.deprecated_call(): - df.select( - c=nw.col("a").sort().head(2), - d=nw.col("a").sort().tail(2), - e=(nw.col("a") == 0).arg_true(), - f=nw.col("a").gather_every(2), - g=nw.col("a").arg_min(), - h=nw.col("a").arg_max(), - ) - def test_dask_order_dependent_ops() -> None: # Preserve these for narwhals.stable.v1, even though they diff --git a/tests/v2_test.py b/tests/v2_test.py new file mode 100644 index 0000000000..507daf11b9 --- /dev/null +++ b/tests/v2_test.py @@ -0,0 +1,334 @@ +# Test assorted functions which we overwrite in stable.v2 + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np +import pandas as pd +import pytest + +import narwhals.stable.v2 as nw_v2 +from narwhals.utils import Version +from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data + +if TYPE_CHECKING: + from typing_extensions import assert_type + + from narwhals.stable.v2.typing import IntoDataFrameT + + +def test_toplevel() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native( + pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, None, 9]}) + ) + result = df.select( + min=nw_v2.min("a"), + max=nw_v2.max("a"), + mean=nw_v2.mean("a"), + median=nw_v2.median("a"), + sum=nw_v2.sum("a"), + sum_h=nw_v2.sum_horizontal("a"), + min_h=nw_v2.min_horizontal("a"), + max_h=nw_v2.max_horizontal("a"), + mean_h=nw_v2.mean_horizontal("a"), + len=nw_v2.len(), + concat_str=nw_v2.concat_str(nw_v2.lit("a"), nw_v2.lit("b")), + any_h=nw_v2.any_horizontal(nw_v2.lit(True), nw_v2.lit(True), ignore_nulls=True), # noqa: FBT003 + all_h=nw_v2.all_horizontal(nw_v2.lit(True), nw_v2.lit(True), ignore_nulls=True), # noqa: FBT003 + first=nw_v2.nth(0), + no_first=nw_v2.exclude("a", "c"), + coalesce=nw_v2.coalesce("c", "a"), + ) + expected = { + "min": [1, 1, 1], + "max": [3, 3, 3], + "mean": [2.0, 2.0, 2.0], + "median": [2.0, 2.0, 2.0], + "sum": [6, 6, 6], + "sum_h": [1, 2, 3], + "min_h": [1, 2, 3], + "max_h": [1, 2, 3], + "mean_h": [1, 2, 3], + "len": [3, 3, 3], + "concat_str": ["ab", "ab", "ab"], + "any_h": [True, True, True], + "all_h": [True, True, True], + "first": [1, 2, 3], + "no_first": [4, 5, 6], + "coalesce": [7, 2, 9], + } + assert_equal_data(result, expected) + assert isinstance(result, nw_v2.DataFrame) + + +def test_when_then() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native(pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [6, 7, 8]})) + result = df.select(nw_v2.when(nw_v2.col("a") > 1).then("b").otherwise("c")) + expected = {"b": [6, 5, 6]} + assert_equal_data(result, expected) + assert isinstance(result, nw_v2.DataFrame) + + +def test_constructors() -> None: + pytest.importorskip("pyarrow") + if PANDAS_VERSION < (2, 2): + pytest.skip() + assert nw_v2.new_series("a", [1, 2, 3], backend="pandas").to_list() == [1, 2, 3] + arr: np.ndarray[tuple[int, int], Any] = np.array([[1, 2], [3, 4]]) # pyright: ignore[reportAssignmentType] + result = nw_v2.from_numpy(arr, schema=["a", "b"], backend="pandas") + assert_equal_data(result, {"a": [1, 3], "b": [2, 4]}) + assert isinstance(result, nw_v2.DataFrame) + result = nw_v2.from_numpy( + arr, + schema=nw_v2.Schema({"a": nw_v2.Int64(), "b": nw_v2.Int64()}), + backend="pandas", + ) + assert_equal_data(result, {"a": [1, 3], "b": [2, 4]}) + assert isinstance(result, nw_v2.DataFrame) + result = nw_v2.from_dict({"a": [1, 2, 3]}, backend="pandas") + assert_equal_data(result, {"a": [1, 2, 3]}) + assert isinstance(result, nw_v2.DataFrame) + result = nw_v2.from_arrow(pd.DataFrame({"a": [1, 2, 3]}), backend="pandas") + assert_equal_data(result, {"a": [1, 2, 3]}) + assert isinstance(result, nw_v2.DataFrame) + + +def test_join() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native(pd.DataFrame({"a": [1, 2, 3]})).lazy() + result = df.join(df, how="inner", on="a").sort("a") + expected = {"a": [1, 2, 3]} + assert_equal_data(result, expected) + assert isinstance(result, nw_v2.LazyFrame) + result_eager = df.collect().join(df.collect(), how="inner", on="a") + assert_equal_data(result_eager, expected) + assert isinstance(result_eager, nw_v2.DataFrame) + + +def test_values_counts_v2() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native(pd.DataFrame({"a": [1, 2, 3]}), eager_only=True) + result = df["a"].value_counts().sort("a") + expected = {"a": [1, 2, 3], "count": [1, 1, 1]} + assert_equal_data(result, expected) + assert isinstance(result, nw_v2.DataFrame) + + +def test_to_frame() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native(pd.DataFrame({"a": [1, 2, 3]}), eager_only=True) + s = df["a"] + assert isinstance(s, nw_v2.Series) + df = s.to_frame() + assert isinstance(df, nw_v2.DataFrame) + + +def test_is_duplicated_unique() -> None: + pytest.importorskip("pandas") + import pandas as pd + + df = nw_v2.from_native(pd.DataFrame({"a": [1, 2, 3]}), eager_only=True) + assert df.is_duplicated().to_list() == [False, False, False] + assert df.is_unique().to_list() == [True, True, True] + assert isinstance(df.is_duplicated(), nw_v2.Series) + assert isinstance(df.is_unique(), nw_v2.Series) + + +def test_concat() -> None: + pytest.importorskip("pyarrow") + import pyarrow as pa + + df = nw_v2.from_native(pa.table({"a": [1, 2, 3]}), eager_only=True) + result = nw_v2.concat([df, df], how="vertical") + expected = {"a": [1, 2, 3, 1, 2, 3]} + assert_equal_data(result, expected) + assert isinstance(result, nw_v2.DataFrame) + if TYPE_CHECKING: + assert_type(result, nw_v2.DataFrame[Any]) + + +def test_to_dict_as_series() -> None: + pytest.importorskip("pyarrow") + import pyarrow as pa + + df = nw_v2.from_native(pa.table({"a": [1, 2, 3]}), eager_only=True) + result = df.to_dict(as_series=True) + expected = {"a": [1, 2, 3]} + assert_equal_data(result, expected) + assert isinstance(result["a"], nw_v2.Series) + + +def test_from_native_already_nw() -> None: + pytest.importorskip("polars") + import polars as pl + + df = nw_v2.from_native(pl.DataFrame({"a": [1]})) + assert isinstance(nw_v2.from_native(df), nw_v2.DataFrame) + assert nw_v2.from_native(df) is df + lf = nw_v2.from_native(pl.LazyFrame({"a": [1]})) + assert isinstance(nw_v2.from_native(lf), nw_v2.LazyFrame) + assert nw_v2.from_native(lf) is lf + s = df["a"] + assert isinstance(nw_v2.from_native(s, series_only=True), nw_v2.Series) + assert nw_v2.from_native(df) is df + + +def test_from_native_invalid_kwds() -> None: + pytest.importorskip("polars") + import polars as pl + + with pytest.raises(TypeError, match="got an unexpected keyword"): + nw_v2.from_native(pl.DataFrame({"a": [1]}), belugas=True) # type: ignore[call-overload] + + +def test_io(tmpdir: pytest.TempdirFactory) -> None: + pytest.importorskip("polars") + import polars as pl + + csv_filepath = str(tmpdir / "file.csv") # type: ignore[operator] + parquet_filepath = str(tmpdir / "file.parquet") # type: ignore[operator] + pl.DataFrame({"a": [1]}).write_csv(csv_filepath) + pl.DataFrame({"a": [1]}).write_parquet(parquet_filepath) + assert isinstance(nw_v2.read_csv(csv_filepath, backend="polars"), nw_v2.DataFrame) + assert isinstance(nw_v2.scan_csv(csv_filepath, backend="polars"), nw_v2.LazyFrame) + assert isinstance( + nw_v2.read_parquet(parquet_filepath, backend="polars"), nw_v2.DataFrame + ) + assert isinstance( + nw_v2.scan_parquet(parquet_filepath, backend="polars"), nw_v2.LazyFrame + ) + + +def test_narwhalify() -> None: + pytest.importorskip("pandas") + import pandas as pd + + data = {"a": [2, 3, 4]} + + @nw_v2.narwhalify + def func(df: nw_v2.DataFrame[IntoDataFrameT]) -> nw_v2.DataFrame[IntoDataFrameT]: + return df.with_columns(nw_v2.all() + 1) + + df = pd.DataFrame({"a": [1, 2, 3]}) + result = func(df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + result = func(df=df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + + +def test_narwhalify_method() -> None: + pytest.importorskip("pandas") + import pandas as pd + + data = {"a": [2, 3, 4]} + + class Foo: + @nw_v2.narwhalify + def func( + self, df: nw_v2.DataFrame[IntoDataFrameT], a: int = 1 + ) -> nw_v2.DataFrame[IntoDataFrameT]: + return df.with_columns(nw_v2.all() + a) + + df = pd.DataFrame({"a": [1, 2, 3]}) + result = Foo().func(df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + result = Foo().func(a=1, df=df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + + +def test_narwhalify_method_called() -> None: + pytest.importorskip("pandas") + import pandas as pd + + data = {"a": [2, 3, 4]} + + class Foo: + @nw_v2.narwhalify + def func( + self, df: nw_v2.DataFrame[IntoDataFrameT], a: int = 1 + ) -> nw_v2.DataFrame[IntoDataFrameT]: + return df.with_columns(nw_v2.all() + a) + + df = pd.DataFrame({"a": [1, 2, 3]}) + result = Foo().func(df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + result = Foo().func(df=df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + result = Foo().func(a=1, df=df) + pd.testing.assert_frame_equal(result, pd.DataFrame(data)) + + +def test_narwhalify_backends_cross() -> None: + pytest.importorskip("pandas") + pytest.importorskip("polars") + import pandas as pd + import polars as pl + + data = {"a": [2, 3, 4]} + + @nw_v2.narwhalify + def func( + arg1: Any, arg2: Any, extra: int = 1 + ) -> tuple[Any, Any, int]: # pragma: no cover + return arg1, arg2, extra + + with pytest.raises( + ValueError, + match="Found multiple backends. Make sure that all dataframe/series inputs come from the same backend.", + ): + func(pd.DataFrame(data), pl.DataFrame(data)) + + +def test_narwhalify_method_invalid() -> None: + class Foo: + @nw_v2.narwhalify(pass_through=False, eager_only=True) + def func(self) -> Foo: # pragma: no cover + return self + + @nw_v2.narwhalify(pass_through=False, eager_only=True) + def fun2(self, df: Any) -> Any: # pragma: no cover + return df + + with pytest.raises(TypeError): + Foo().func() + + +def test_with_version(constructor: Constructor) -> None: + lf = nw_v2.from_native(constructor({"a": [1, 2]})).lazy() + assert isinstance(lf, nw_v2.LazyFrame) + assert lf._compliant_frame._with_version(Version.MAIN)._version is Version.MAIN + + +def test_get_column() -> None: + pytest.importorskip("pandas") + import pandas as pd + + def minimal_function(data: nw_v2.Series[Any]) -> None: + data.is_null() + + pd_df = pd.DataFrame({"col": [1, 2, None, 4]}) + col = nw_v2.from_native(pd_df, eager_only=True).get_column("col") + # check this doesn't raise type-checking errors + minimal_function(col) + assert isinstance(col, nw_v2.Series) + + +def test_imports() -> None: + # check these don't raise + from narwhals.stable.v2.dependencies import is_pandas_dataframe # noqa: F401 + from narwhals.stable.v2.dtypes import Enum # noqa: F401 + from narwhals.stable.v2.selectors import datetime # noqa: F401 + from narwhals.stable.v2.typing import IntoDataFrame # noqa: F401 diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py index 34aa838a89..7e1b305811 100644 --- a/utils/check_api_reference.py +++ b/utils/check_api_reference.py @@ -62,16 +62,24 @@ def read_documented_members(source: str | Path) -> list[str]: NAMESPACES = {"dt", "str", "cat", "name", "list", "struct"} EXPR_ONLY_METHODS = {"over", "map_batches"} SERIES_ONLY_METHODS = { + "arg_max", + "arg_min", + "arg_true", "dtype", + "gather_every", "implementation", "is_empty", "is_sorted", + "head", "hist", "item", "name", "rename", + "sample", "scatter", "shape", + "sort", + "tail", "to_arrow", "to_dummies", "to_frame",