diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 804e2f5a..e0cbb5b1 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -53,6 +53,10 @@ jobs: - name: Run tests run: uv run pytest + - name: Run no-xorq-compatible examples + if: matrix.name == 'no-xorq' + run: make examples-core + # Build examples, docs, and skills (require the full env incl. Node). build: name: Build (examples + docs + skills) diff --git a/Makefile b/Makefile index 175df596..b486e7d1 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ help: @echo " make test IBIS_VERSION=all - Run tests with all ibis versions (9.5.0, 10.8.0, 11.0.0)" @echo " make test IBIS_VERSION=10.8.0 - Run tests with specific ibis version" @echo " make examples - Run all example scripts" + @echo " make examples-core - Run no-xorq-compatible examples" @echo " make examples IBIS_VERSION=all - Run examples with all ibis versions" @echo " make examples IBIS_VERSION=10.8.0 - Run examples with specific ibis version" @echo " make docs-build - Build documentation" @@ -105,6 +106,19 @@ else @echo "✓ All examples passed!" endif +examples-core: + @echo "Running no-xorq-compatible examples..." + @for file in \ + scripts/demo_bsl_v2.py \ + examples/quickstart.py \ + examples/window_functions.py \ + examples/bucketing_with_other.py \ + examples/sessionized_data.py; do \ + echo "Running $$file..."; \ + uv run --with . "$$file" || exit 1; \ + done + @echo "✓ No-xorq-compatible examples passed!" + # Build docs docs-build: @echo "Building documentation..." diff --git a/examples/bucketing_with_other.py b/examples/bucketing_with_other.py index 14640307..86112807 100644 --- a/examples/bucketing_with_other.py +++ b/examples/bucketing_with_other.py @@ -9,6 +9,10 @@ import ibis from ibis import _ +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import from_yaml @@ -28,13 +32,13 @@ def main(): nest={"data": lambda t: t.group_by(["code", "elevation"])}, ) .mutate( - rank=lambda t: ibis.row_number().over( - ibis.window(order_by=ibis.desc(t.avg_elevation)), + rank=lambda t: xibis.row_number().over( + xibis.window(order_by=xibis.desc(t.avg_elevation)), ), ) .mutate( is_other=lambda t: t.rank > 4, - state_grouped=lambda t: ibis.ifelse(t.rank > 4, "OTHER", t.state), + state_grouped=lambda t: xibis.ifelse(t.rank > 4, "OTHER", t.state), ) .group_by("state_grouped") .aggregate( diff --git a/examples/quickstart.py b/examples/quickstart.py index 02f1bf3d..a5dfbb12 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -13,6 +13,10 @@ import ibis import pandas as pd +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import to_semantic_table @@ -111,7 +115,7 @@ def main(): .with_measures(sum_val=lambda t: t.value.sum()) ) - rolling_window = ibis.window(order_by="date", rows=(1, 1)) + rolling_window = xibis.window(order_by="date", rows=(1, 1)) expr4 = ( ts_st.group_by("date") .aggregate("sum_val") diff --git a/examples/sessionized_data.py b/examples/sessionized_data.py index 24630e65..a38a52a9 100644 --- a/examples/sessionized_data.py +++ b/examples/sessionized_data.py @@ -13,6 +13,10 @@ import ibis import pandas as pd +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import from_yaml, to_untagged # Show all columns in output @@ -31,7 +35,7 @@ def main(): # Filter for carrier WN on 2002-03-03 and add flight_date column filtered_flights = flights.filter( - lambda t: (t.carrier == "WN") & (t.dep_time.date() == ibis.date(2002, 3, 3)), + lambda t: (t.carrier == "WN") & (t.dep_time.date() == xibis.date(2002, 3, 3)), ).mutate(flight_date=lambda t: t.dep_time.date()) # Create sessions with nested flight legs @@ -52,7 +56,7 @@ def main(): ]), }, ) - .mutate(session_id=ibis.row_number().over(ibis.window())) + .mutate(session_id=xibis.row_number().over(xibis.window())) .order_by("session_id") ) diff --git a/examples/window_functions.py b/examples/window_functions.py index d56805bd..fa996f44 100644 --- a/examples/window_functions.py +++ b/examples/window_functions.py @@ -4,6 +4,10 @@ import ibis from ibis import _ +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import to_semantic_table BASE_URL = "https://pub-a45a6a332b4646f2a6f44775695c64df.r2.dev" @@ -31,13 +35,13 @@ def main(): result = ( daily_stats.mutate( rolling_avg=lambda t: t.flight_count.mean().over( - ibis.window(order_by=t.flight_date, preceding=6, following=0), + xibis.window(order_by=t.flight_date, preceding=6, following=0), ), - rank=lambda t: ibis.dense_rank().over( - ibis.window(order_by=ibis.desc(t.flight_count)), + rank=lambda t: xibis.dense_rank().over( + xibis.window(order_by=xibis.desc(t.flight_count)), ), running_total=lambda t: t.flight_count.sum().over( - ibis.window(order_by=t.flight_date), + xibis.window(order_by=t.flight_date), ), ) .order_by("flight_date") @@ -64,8 +68,8 @@ def main(): result = ( carrier_stats.mutate( - total_flights=lambda t: t.flight_count.sum().over(ibis.window()), - percent_manual=lambda t: (t.flight_count / t.flight_count.sum().over(ibis.window())) + total_flights=lambda t: t.flight_count.sum().over(xibis.window()), + percent_manual=lambda t: (t.flight_count / t.flight_count.sum().over(xibis.window())) * 100, ) .order_by(_.percent_manual.desc()) diff --git a/scripts/demo_bsl_v2.py b/scripts/demo_bsl_v2.py index a786064e..b65815ef 100644 --- a/scripts/demo_bsl_v2.py +++ b/scripts/demo_bsl_v2.py @@ -20,6 +20,10 @@ from boring_semantic_layer import to_semantic_table +# CI runs this demo with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + def main(): # Setup in-memory DuckDB @@ -116,7 +120,7 @@ def main(): .with_measures(sum_val=lambda t: t.value.sum()) ) - rolling_window = ibis.window(order_by="date", preceding=1, following=1) + rolling_window = xibis.window(order_by="date", preceding=1, following=1) expr4 = ( ts_st.group_by("date") .aggregate("sum_val") diff --git a/src/boring_semantic_layer/chart/tests/test_chart.py b/src/boring_semantic_layer/chart/tests/test_chart.py index c3a40a4a..ea01aeef 100644 --- a/src/boring_semantic_layer/chart/tests/test_chart.py +++ b/src/boring_semantic_layer/chart/tests/test_chart.py @@ -6,9 +6,9 @@ from boring_semantic_layer import to_semantic_table -# BSL's to_untagged() returns xorq-vendored ibis when xorq is installed, and -# those expressions reject a plain ibis.window (LegacyWindowBuilder). Build the -# window from the matching flavor via the shim (plain ibis when xorq is absent). +# Match BSL's active ibis flavor: plain ibis without xorq, xorq-vendored ibis +# with xorq installed. Plain-ibis users can write `import ibis` directly. +from boring_semantic_layer._xorq import ibis as xibis @pytest.fixture(scope="module") @@ -390,7 +390,7 @@ def test_chart_with_dynamic_dimension_and_rolling_window(self, flights_model): .order_by("flight_week") .mutate( rolling_avg=lambda t: t.flight_count.mean().over( - ibis.window(rows=(-2, 0), order_by="flight_week") + xibis.window(rows=(-2, 0), order_by="flight_week") ) ) ) diff --git a/src/boring_semantic_layer/ops.py b/src/boring_semantic_layer/ops.py index 574554f6..bb9fda16 100644 --- a/src/boring_semantic_layer/ops.py +++ b/src/boring_semantic_layer/ops.py @@ -740,7 +740,12 @@ def _classify_measure( if prefer_known is None: prefer_known = getattr(scope, "_prefer_known", ()) prefer_known_set = frozenset(prefer_known) - expr_prefer_known = getattr(expr, "__bsl_prefer_known__", ()) + try: + # Use object.__getattribute__ so ibis Deferred.__getattr__ does not + # synthesize a resolver for this private marker. + expr_prefer_known = object.__getattribute__(expr, "__bsl_prefer_known__") + except AttributeError: + expr_prefer_known = () if expr_prefer_known is True: prefer_known_set = prefer_known_set | known_set else: