From e4246d31ca129ca653a4e8a40ca74f79d2577a69 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Thu, 14 Nov 2024 10:52:14 +0400 Subject: [PATCH] feat: Add an `is_literal` method to expression `meta` namespace --- crates/polars-plan/src/dsl/meta.rs | 18 +++++++++-- crates/polars-python/src/expr/meta.rs | 4 +++ py-polars/polars/expr/meta.py | 31 +++++++++++++++++-- .../unit/operations/namespaces/test_meta.py | 31 ++++++++++++++++++- 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/crates/polars-plan/src/dsl/meta.rs b/crates/polars-plan/src/dsl/meta.rs index b23c5181cc13..76a881f08ed1 100644 --- a/crates/polars-plan/src/dsl/meta.rs +++ b/crates/polars-plan/src/dsl/meta.rs @@ -83,9 +83,21 @@ impl MetaNameSpace { | Expr::IndexColumn(_) | Expr::Selector(_) | Expr::Wildcard => true, - Expr::Alias(_, _) | Expr::KeepName(_) | Expr::RenameAlias { .. } if allow_aliasing => { - true - }, + Expr::Alias(_, _) | Expr::KeepName(_) | Expr::RenameAlias { .. } => allow_aliasing, + _ => false, + }) + } + + /// Indicate if this expression represents a literal value (optionally aliased). + pub fn is_literal(&self, allow_aliasing: bool) -> bool { + self.0.into_iter().all(|e| match e { + Expr::Literal(_) => true, + Expr::Alias(_, _) => allow_aliasing, + Expr::Cast { + expr, + dtype: DataType::Datetime(_, _), + options: CastOptions::Strict, + } if matches!(&**expr, Expr::Literal(LiteralValue::DateTime(_, _, _))) => true, _ => false, }) } diff --git a/crates/polars-python/src/expr/meta.rs b/crates/polars-python/src/expr/meta.rs index d0e3a8b3e1df..891d37d26afa 100644 --- a/crates/polars-python/src/expr/meta.rs +++ b/crates/polars-python/src/expr/meta.rs @@ -58,6 +58,10 @@ impl PyExpr { .is_column_selection(allow_aliasing) } + fn meta_is_literal(&self, allow_aliasing: bool) -> bool { + self.inner.clone().meta().is_literal(allow_aliasing) + } + fn _meta_selector_add(&self, other: PyExpr) -> PyResult { let out = self .inner diff --git a/py-polars/polars/expr/meta.py b/py-polars/polars/expr/meta.py index e6ebc6f40944..d949f7583d14 100644 --- a/py-polars/polars/expr/meta.py +++ b/py-polars/polars/expr/meta.py @@ -108,7 +108,7 @@ def is_column_selection(self, *, allow_aliasing: bool = False) -> bool: """ Indicate if this expression only selects columns (optionally with aliasing). - This can include bare columns, column matches by regex or dtype, selectors + This can include bare columns, columns matched by regex or dtype, selectors and exclude ops, and (optionally) column/expression aliasing. .. versionadded:: 0.20.30 @@ -116,7 +116,7 @@ def is_column_selection(self, *, allow_aliasing: bool = False) -> bool: Parameters ---------- allow_aliasing - If False (default), any aliasing is not considered pure column selection. + If False (default), any aliasing is not considered to be column selection. Set True to allow for column selection that also includes aliasing. Examples @@ -142,6 +142,33 @@ def is_column_selection(self, *, allow_aliasing: bool = False) -> bool: """ return self._pyexpr.meta_is_column_selection(allow_aliasing) + def is_literal(self, *, allow_aliasing: bool = False) -> bool: + """ + Indicate if this expression is a literal value (optionally aliased). + + .. versionadded:: 1.14 + + Parameters + ---------- + allow_aliasing + If False (default), only a bare literal will match. + Set True to also allow for aliased literals. + + Examples + -------- + >>> from datetime import datetime + >>> e = pl.lit(123) + >>> e.meta.is_literal() + True + >>> e = pl.lit(987.654321).alias("foo") + >>> e.meta.is_literal() + False + >>> e = pl.lit(datetime.now()).alias("bar") + >>> e.meta.is_literal(allow_aliasing=True) + True + """ + return self._pyexpr.meta_is_literal(allow_aliasing) + @overload def output_name(self, *, raise_if_undetermined: Literal[True] = True) -> str: ... diff --git a/py-polars/tests/unit/operations/namespaces/test_meta.py b/py-polars/tests/unit/operations/namespaces/test_meta.py index 38835244557e..5a0c253fbfed 100644 --- a/py-polars/tests/unit/operations/namespaces/test_meta.py +++ b/py-polars/tests/unit/operations/namespaces/test_meta.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from datetime import date, datetime, time, timedelta +from typing import TYPE_CHECKING, Any import pytest @@ -123,6 +124,34 @@ def test_is_column_selection( assert not expr.meta.is_column_selection() +@pytest.mark.parametrize( + "value", + [ + None, + 1234, + 567.89, + float("inf"), + date.today(), + datetime.now(), + time(10, 30, 45), + timedelta(hours=-24), + ["x", "y", "z"], + pl.Series([None, None]), + [[10, 20], [30, 40]], + "this is the way", + ], +) +def test_is_literal(value: Any) -> None: + e = pl.lit(value) + assert e.meta.is_literal() + + e = pl.lit(value).alias("foo") + assert not e.meta.is_literal() + + e = pl.lit(value).alias("foo") + assert e.meta.is_literal(allow_aliasing=True) + + def test_meta_is_regex_projection() -> None: e = pl.col("^.*$").name.suffix("_foo") assert e.meta.is_regex_projection()