From a191394bfdd24073511defae0c91af405bec6098 Mon Sep 17 00:00:00 2001 From: Hussain Sultan Date: Mon, 4 May 2026 07:14:16 -0400 Subject: [PATCH] refactor: route all xorq imports through single _xorq shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add src/boring_semantic_layer/_xorq.py as the one place that imports from xorq.vendor.ibis, xorq.api, xorq.common.utils, and xorq.expr. All 13 BSL modules that previously imported xorq directly now import via the shim, so any future xorq API drift only requires updating one file instead of fanning out across the package. This shim does not replace plain `ibis` (PyPI ibis-framework) — BSL intentionally coexists with both ibis flavors. The shim covers only the xorq-vendored side. No behavior change. 930 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/boring_semantic_layer/_xorq.py | 104 ++++++++++++++++++ .../chart/md_parser/executor.py | 2 +- src/boring_semantic_layer/compile_all.py | 2 +- src/boring_semantic_layer/config.py | 2 +- src/boring_semantic_layer/expr.py | 10 +- src/boring_semantic_layer/format.py | 2 +- src/boring_semantic_layer/graph_utils.py | 12 +- src/boring_semantic_layer/measure_scope.py | 4 +- src/boring_semantic_layer/nested_access.py | 2 +- src/boring_semantic_layer/ops.py | 37 ++++--- src/boring_semantic_layer/profile.py | 2 +- src/boring_semantic_layer/projection_utils.py | 2 +- src/boring_semantic_layer/query.py | 2 +- .../serialization/__init__.py | 3 +- .../serialization/reconstruct.py | 23 ++-- .../serialization/tag_handler.py | 2 +- src/boring_semantic_layer/utils.py | 28 +++-- 17 files changed, 171 insertions(+), 68 deletions(-) create mode 100644 src/boring_semantic_layer/_xorq.py diff --git a/src/boring_semantic_layer/_xorq.py b/src/boring_semantic_layer/_xorq.py new file mode 100644 index 00000000..167d714b --- /dev/null +++ b/src/boring_semantic_layer/_xorq.py @@ -0,0 +1,104 @@ +"""Single import point for the xorq surface used by BSL. + +All BSL modules should import xorq symbols from this shim. If xorq renames +or moves something, only this file needs to change. + +This shim does NOT replace the plain ``ibis`` package (PyPI ibis-framework). +BSL coexists with both flavors: use ``import ibis`` for the plain side, and +this module for the ``xorq.vendor.ibis`` side. +""" + +from __future__ import annotations + +import xorq.api as api +from xorq.api import selectors +from xorq.common.utils.graph_utils import to_node +from xorq.common.utils.ibis_utils import from_ibis, map_ibis +from xorq.common.utils.node_utils import replace_nodes, walk_nodes +from xorq.expr.builders import TagHandler +from xorq.expr.relations import CachedNode, Read, RemoteTable, Tag +from xorq.vendor import ibis +from xorq.vendor.ibis import _ +from xorq.vendor.ibis.backends.profiles import Profile +from xorq.vendor.ibis.common.collections import FrozenDict, FrozenOrderedDict +from xorq.vendor.ibis.common.deferred import ( + Attr, + BinaryOperator, + Call, + Deferred, + Item, + Just, + JustUnhashable, + Mapping, + Sequence, + UnaryOperator, + Variable, +) +from xorq.vendor.ibis.common.graph import Graph +from xorq.vendor.ibis.config import Config +from xorq.vendor.ibis.expr import operations, types +from xorq.vendor.ibis.expr.format import fmt, render_fields +from xorq.vendor.ibis.expr.operations import relations +from xorq.vendor.ibis.expr.operations.core import Node +from xorq.vendor.ibis.expr.operations.generic import Literal +from xorq.vendor.ibis.expr.operations.relations import ( + DatabaseTable, + Field, + JoinChain, + JoinReference, +) +from xorq.vendor.ibis.expr.operations.sortkeys import SortKey +from xorq.vendor.ibis.expr.schema import Schema +from xorq.vendor.ibis.expr.types import Expr, Table +from xorq.vendor.ibis.expr.types.generic import Column +from xorq.vendor.ibis.expr.types.groupby import GroupedTable + +__all__ = [ + "Attr", + "BinaryOperator", + "CachedNode", + "Call", + "Column", + "Config", + "DatabaseTable", + "Deferred", + "Expr", + "Field", + "FrozenDict", + "FrozenOrderedDict", + "Graph", + "GroupedTable", + "Item", + "JoinChain", + "JoinReference", + "Just", + "JustUnhashable", + "Literal", + "Mapping", + "Node", + "Profile", + "Read", + "RemoteTable", + "Schema", + "Sequence", + "SortKey", + "Table", + "Tag", + "TagHandler", + "UnaryOperator", + "Variable", + "_", + "api", + "fmt", + "from_ibis", + "ibis", + "map_ibis", + "operations", + "relations", + "render_fields", + "replace_nodes", + "selectors", + "to_node", + "types", + "walk_nodes", +] diff --git a/src/boring_semantic_layer/chart/md_parser/executor.py b/src/boring_semantic_layer/chart/md_parser/executor.py index 1bd61cfb..3f127148 100644 --- a/src/boring_semantic_layer/chart/md_parser/executor.py +++ b/src/boring_semantic_layer/chart/md_parser/executor.py @@ -5,10 +5,10 @@ from typing import Any import ibis -import xorq.api as xo from returns.result import Success from boring_semantic_layer import to_semantic_table +from boring_semantic_layer._xorq import api as xo from boring_semantic_layer.utils import safe_eval diff --git a/src/boring_semantic_layer/compile_all.py b/src/boring_semantic_layer/compile_all.py index 06998ddd..59154a1b 100644 --- a/src/boring_semantic_layer/compile_all.py +++ b/src/boring_semantic_layer/compile_all.py @@ -145,7 +145,7 @@ def _get_ibis_module(table): table_module = type(table).__module__ if table_module.startswith("xorq.vendor.ibis"): # Table is from xorq's vendored ibis - from xorq.vendor import ibis as xorq_ibis + from ._xorq import ibis as xorq_ibis return xorq_ibis else: # Table is from regular ibis diff --git a/src/boring_semantic_layer/config.py b/src/boring_semantic_layer/config.py index 18d088c2..8e578b47 100644 --- a/src/boring_semantic_layer/config.py +++ b/src/boring_semantic_layer/config.py @@ -9,7 +9,7 @@ to reduce data scanned, which is especially beneficial for wide tables. """ -from xorq.vendor.ibis.config import Config +from ._xorq import Config class Options(Config): diff --git a/src/boring_semantic_layer/expr.py b/src/boring_semantic_layer/expr.py index 91f3d9d7..db757667 100644 --- a/src/boring_semantic_layer/expr.py +++ b/src/boring_semantic_layer/expr.py @@ -12,9 +12,11 @@ from ibis.expr.types.groupby import GroupedTable as IbisGroupedTable from ibis.expr.types.relations import Table as IbisTable from returns.result import Success, safe -from xorq.vendor.ibis.expr.types import Table -from xorq.vendor.ibis.expr.types.generic import Column as XorqColumn -from xorq.vendor.ibis.expr.types.groupby import GroupedTable +from ._xorq import ( + Column as XorqColumn, + GroupedTable, + Table, +) from .chart import chart as create_chart from .measure_scope import AggregationExpr, MeasureScope @@ -1253,7 +1255,7 @@ def collect_struct(struct_dict): # each can only infer types from columns of its own module first_col = next(iter(struct_dict.values())) if isinstance(first_col, XorqColumn): - import xorq.vendor.ibis as xibis + from ._xorq import ibis as xibis return xibis.struct(struct_dict).collect() return ibis.struct(struct_dict).collect() diff --git a/src/boring_semantic_layer/format.py b/src/boring_semantic_layer/format.py index bac9536d..ac4999a6 100644 --- a/src/boring_semantic_layer/format.py +++ b/src/boring_semantic_layer/format.py @@ -5,7 +5,7 @@ try: from ibis.expr.format import fmt, render_fields except ImportError: - from xorq.vendor.ibis.expr.format import fmt, render_fields + from ._xorq import fmt, render_fields from boring_semantic_layer.ops import ( SemanticAggregateOp, diff --git a/src/boring_semantic_layer/graph_utils.py b/src/boring_semantic_layer/graph_utils.py index 3bdae9c6..424d6623 100644 --- a/src/boring_semantic_layer/graph_utils.py +++ b/src/boring_semantic_layer/graph_utils.py @@ -11,20 +11,18 @@ from returns.maybe import Maybe, Nothing, Some from returns.result import Failure, Result, Success, safe from toolz import compose -from xorq.common.utils.graph_utils import ( +from ._xorq import ( + Expr as XorqExpr, + Graph, + Node, replace_nodes as _xorq_replace_nodes, -) -from xorq.common.utils.graph_utils import ( to_node as _xorq_to_node, ) -from xorq.vendor.ibis.common.graph import Graph -from xorq.vendor.ibis.expr.operations.core import Node -from xorq.vendor.ibis.expr.types import Expr as XorqExpr def _collect_field_types() -> tuple[type, ...]: """Collect all Field types that may appear in expressions.""" - from xorq.vendor.ibis.expr.operations.relations import Field as XorqField + from ._xorq import Field as XorqField types = [XorqField] try: diff --git a/src/boring_semantic_layer/measure_scope.py b/src/boring_semantic_layer/measure_scope.py index 38ad485f..5a0874d5 100644 --- a/src/boring_semantic_layer/measure_scope.py +++ b/src/boring_semantic_layer/measure_scope.py @@ -373,7 +373,7 @@ def __getitem__(self, name: str): return _resolve_column_item(self.tbl, name) def all(self, ref): - from xorq.vendor import ibis as ibis_mod + from ._xorq import ibis as ibis_mod if isinstance(ref, str): if self.post_agg: @@ -431,7 +431,7 @@ def __getitem__(self, name: str): return self.tbl[name] def all(self, ref): - from xorq.vendor import ibis as ibis_mod + from ._xorq import ibis as ibis_mod if isinstance(ref, str): return self.tbl[ref].sum().over(ibis_mod.window()) diff --git a/src/boring_semantic_layer/nested_access.py b/src/boring_semantic_layer/nested_access.py index f81e568e..4706b2bb 100644 --- a/src/boring_semantic_layer/nested_access.py +++ b/src/boring_semantic_layer/nested_access.py @@ -21,7 +21,7 @@ from attrs import frozen from toolz import curry -from xorq.vendor.ibis.expr import types as ir +from ._xorq import types as ir @frozen diff --git a/src/boring_semantic_layer/ops.py b/src/boring_semantic_layer/ops.py index ff2f534b..a73189e9 100644 --- a/src/boring_semantic_layer/ops.py +++ b/src/boring_semantic_layer/ops.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any import ibis -from xorq.api import selectors as s from attrs import field, frozen from ibis.common.deferred import Deferred from ibis.expr import datatypes as dt @@ -17,9 +16,13 @@ from ibis.expr.operations.relations import Field, Relation from ibis.expr.schema import Schema -from xorq.vendor.ibis.common.collections import FrozenDict, FrozenOrderedDict -from xorq.vendor.ibis.expr import operations as xorq_ops -from xorq.vendor.ibis.expr.schema import Schema as XorqSchema +from ._xorq import ( + FrozenDict, + FrozenOrderedDict, + Schema as XorqSchema, + operations as xorq_ops, + selectors as s, +) _SchemaClass = XorqSchema _FrozenOrderedDict = FrozenOrderedDict @@ -204,8 +207,8 @@ def _patch_xorq_sortkey_compat(): while xorq's vendored ibis keeps ``SortKey.expr``. Handle both. """ from ibis.expr.operations.sortkeys import SortKey as IbisSortKey - from xorq.common.utils.ibis_utils import map_ibis - from xorq.vendor.ibis.expr.operations.sortkeys import SortKey as XorqSortKey + + from ._xorq import SortKey as XorqSortKey, map_ibis if IbisSortKey in map_ibis.registry: return # already patched @@ -236,7 +239,7 @@ def _ensure_xorq_table(table): _patch_xorq_sortkey_compat() if "xorq.vendor.ibis" not in type(table).__module__: try: - from xorq.common.utils.ibis_utils import from_ibis + from ._xorq import from_ibis return from_ibis(table) except Exception: @@ -252,7 +255,7 @@ def _rebind_to_backend(expr, target_backend): reason; callers must pass a xorq-vendored ``target_backend``. """ try: - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel except Exception: return expr @@ -285,8 +288,7 @@ def _rebind_to_canonical_backend(expr): No-op on plain ibis expressions (not xorq-vendored). """ try: - from xorq.common.utils.node_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel, walk_nodes except Exception: return expr @@ -471,7 +473,7 @@ def _resolve_expr(expr: Deferred | Callable | Any, scope: ir.Table) -> ir.Value: scope_is_xorq = "xorq.vendor.ibis" in scope_module if result_is_regular_ibis and scope_is_xorq: - from xorq.common.utils.ibis_utils import from_ibis + from ._xorq import from_ibis result = from_ibis(result) @@ -4062,8 +4064,7 @@ def _rebind_join_backends(left_tbl, right_tbl): returning the inputs unchanged so ibis executes the join natively. """ try: - from xorq.common.utils.node_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel, walk_nodes except Exception: return left_tbl, right_tbl @@ -4247,7 +4248,7 @@ def _get_weight_expr( all_roots: list, is_string: bool, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo if not by_measure: return xo._.count() @@ -4266,7 +4267,7 @@ def _build_string_index_fragment( type_str: str, weight_expr: Any, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo return ( base_tbl.group_by(field_expr.name("value")) @@ -4289,7 +4290,7 @@ def _build_numeric_index_fragment( type_str: str, weight_expr: Any, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo return ( base_tbl.select(field_expr.name("value")) @@ -4387,7 +4388,7 @@ def __repr__(self) -> str: @property def values(self) -> FrozenOrderedDict[str, Any]: - import xorq.api as xo + from ._xorq import api as xo return FrozenOrderedDict( { @@ -4435,7 +4436,7 @@ def to_untagged(self): ) if not fields_to_index: - import xorq.api as xo + from ._xorq import api as xo return xo.memtable( { diff --git a/src/boring_semantic_layer/profile.py b/src/boring_semantic_layer/profile.py index 83efbdcb..5d65c709 100644 --- a/src/boring_semantic_layer/profile.py +++ b/src/boring_semantic_layer/profile.py @@ -4,7 +4,7 @@ from pathlib import Path from ibis import BaseBackend -from xorq.vendor.ibis.backends.profiles import Profile as XorqProfile +from ._xorq import Profile as XorqProfile from .utils import read_yaml_file diff --git a/src/boring_semantic_layer/projection_utils.py b/src/boring_semantic_layer/projection_utils.py index 8651be42..bd72d8d6 100644 --- a/src/boring_semantic_layer/projection_utils.py +++ b/src/boring_semantic_layer/projection_utils.py @@ -8,7 +8,7 @@ from attrs import frozen from returns.result import Failure, Success -from xorq.vendor.ibis.expr import types as ir +from ._xorq import types as ir from .graph_utils import walk_nodes diff --git a/src/boring_semantic_layer/query.py b/src/boring_semantic_layer/query.py index 5e214f2e..37d1db5c 100644 --- a/src/boring_semantic_layer/query.py +++ b/src/boring_semantic_layer/query.py @@ -27,7 +27,7 @@ def _get_ibis_api(): xorq does not support, plain ibis is used as the fallback. """ try: - import xorq.api as xo + from ._xorq import api as xo return xo except Exception: diff --git a/src/boring_semantic_layer/serialization/__init__.py b/src/boring_semantic_layer/serialization/__init__.py index 8470ae14..ab5ca285 100644 --- a/src/boring_semantic_layer/serialization/__init__.py +++ b/src/boring_semantic_layer/serialization/__init__.py @@ -86,8 +86,7 @@ def do_convert(xorq_mod: XorqModule): import re - from xorq.common.utils.node_utils import replace_nodes - from xorq.vendor.ibis.expr.operations.relations import DatabaseTable + from .._xorq import DatabaseTable, replace_nodes xorq_table = ibis_expr diff --git a/src/boring_semantic_layer/serialization/reconstruct.py b/src/boring_semantic_layer/serialization/reconstruct.py index 645df8e0..ba06b5f8 100644 --- a/src/boring_semantic_layer/serialization/reconstruct.py +++ b/src/boring_semantic_layer/serialization/reconstruct.py @@ -82,11 +82,13 @@ def _unwrap_cached_nodes(expr): return _unwrap_xorq_wrappers(expr, strip_remote=False) def _reconstruct_table(): - from xorq.common.utils.graph_utils import walk_nodes - from xorq.common.utils.ibis_utils import from_ibis - from xorq.expr.relations import Read - from xorq.vendor import ibis - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from .._xorq import ( + Read, + from_ibis, + ibis, + relations as xorq_rel, + walk_nodes, + ) unwrapped_expr = _unwrap_cached_nodes(xorq_expr) @@ -247,8 +249,7 @@ def _reconstruct_limit( def _reconstruct_join( metadata: dict, xorq_expr, source, context: BSLSerializationContext ): - from xorq.common.utils.graph_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from .._xorq import relations as xorq_rel, walk_nodes from .. import expr as bsl_expr @@ -303,7 +304,7 @@ def _reconstruct_join( def _unwrap_xorq_wrappers(expr, *, strip_remote: bool = False): """Walk past Tag, CachedNode, and optionally RemoteTable wrappers.""" - from xorq.expr.relations import CachedNode, RemoteTable, Tag + from .._xorq import CachedNode, RemoteTable, Tag op = expr.op() if isinstance(op, Tag): @@ -319,7 +320,7 @@ def _unwrap_xorq_wrappers(expr, *, strip_remote: bool = False): def _unwrap_join_ref(expr): """If expr is a JoinReference, return the underlying table.""" - from xorq.vendor.ibis.expr.operations.relations import JoinReference + from .._xorq import JoinReference if isinstance(expr.op(), JoinReference): return expr.op().parent.to_expr() @@ -339,7 +340,7 @@ def _rebind_to_backend(expr, target_backend): def _split_join_expr(xorq_expr): """Extract left and right table expressions from a joined xorq expression.""" - from xorq.vendor.ibis.expr.operations.relations import JoinChain + from .._xorq import JoinChain expr = _unwrap_xorq_wrappers(xorq_expr, strip_remote=True) op = expr.op() @@ -373,7 +374,7 @@ def _split_join_expr(xorq_expr): def extract_xorq_metadata(xorq_expr) -> dict[str, Any] | None: """Walk a xorq expression tree to find BSL tag metadata.""" - from xorq.expr.relations import Tag + from .._xorq import Tag @safe def get_op(expr): diff --git a/src/boring_semantic_layer/serialization/tag_handler.py b/src/boring_semantic_layer/serialization/tag_handler.py index 91df1b9b..efdaac3f 100644 --- a/src/boring_semantic_layer/serialization/tag_handler.py +++ b/src/boring_semantic_layer/serialization/tag_handler.py @@ -16,7 +16,7 @@ from typing import Any -from xorq.expr.builders import TagHandler +from .._xorq import TagHandler from . import ( BSLSerializationContext, diff --git a/src/boring_semantic_layer/utils.py b/src/boring_semantic_layer/utils.py index ce5944ce..b83bba55 100644 --- a/src/boring_semantic_layer/utils.py +++ b/src/boring_semantic_layer/utils.py @@ -178,8 +178,7 @@ def _check_closure_vars(fn: Callable) -> Maybe[str]: def _try_ibis_introspection(fn: Callable) -> Maybe[str]: from returns.result import Success - from xorq.vendor.ibis import _ - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred, _ result = fn(_) if not isinstance(result, Deferred): @@ -254,8 +253,7 @@ def do_convert(): from ibis import _ try: - from xorq import api as xo - from xorq.vendor import ibis as xorq_ibis + from ._xorq import api as xo, ibis as xorq_ibis eval_context = { "ibis": ibis, @@ -284,7 +282,7 @@ def do_convert(): def _is_ibis_literal_node(value) -> bool: try: - from xorq.vendor.ibis.expr.operations.generic import Literal + from ._xorq import Literal return isinstance(value, Literal) except ImportError: return False @@ -292,7 +290,7 @@ def _is_ibis_literal_node(value) -> bool: def serialize_resolver(resolver) -> tuple: """Walk a Resolver tree and produce a hashable nested-tuple representation.""" - from xorq.vendor.ibis.common.deferred import ( + from ._xorq import ( Attr, BinaryOperator, Call, @@ -402,7 +400,7 @@ def _resolve_qualname(module_obj, qualname: str): def deserialize_resolver(data: tuple): """Reconstruct a Resolver tree from a nested-tuple representation.""" - from xorq.vendor.ibis.common.deferred import ( + from ._xorq import ( Attr, BinaryOperator, Call, @@ -426,7 +424,7 @@ def deserialize_resolver(data: tuple): return Just(func) case ("ibis_literal", py_value, dtype_str): - from xorq.vendor import ibis + from ._xorq import ibis lit_expr = ibis.literal(py_value, type=ibis.dtype(dtype_str)) return Just(lit_expr.op()) @@ -441,7 +439,7 @@ def deserialize_resolver(data: tuple): case ("call", func_data, args_data, kwargs_data): func_resolver = deserialize_resolver(func_data) args_resolvers = tuple(deserialize_resolver(a) for a in args_data) - from xorq.vendor.ibis.common.collections import FrozenDict + from ._xorq import FrozenDict kwargs_resolvers = FrozenDict( {k: deserialize_resolver(v) for k, v in kwargs_data} ) @@ -483,7 +481,7 @@ def deserialize_resolver(data: tuple): case ("map", type_name, items_data): typ = {"dict": dict}[type_name] - from xorq.vendor.ibis.common.collections import FrozenDict + from ._xorq import FrozenDict values = FrozenDict( {k: deserialize_resolver(v) for k, v in items_data} ) @@ -503,11 +501,11 @@ def _is_deferred(obj) -> bool: def expr_to_structured(fn: Callable) -> Result[tuple, Exception]: """Convert a callable/Deferred expression to a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred as XorqDeferred + from ._xorq import Deferred as XorqDeferred @safe def do_convert(): - from xorq.vendor.ibis import _ + from ._xorq import _ if isinstance(fn, XorqDeferred): return serialize_resolver(fn._resolver) @@ -528,7 +526,7 @@ def do_convert(): def structured_to_expr(data: tuple) -> Result: """Reconstruct a Deferred from a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred @safe def do_convert(): @@ -545,7 +543,7 @@ def join_predicate_to_structured(fn: Callable) -> Result[tuple, Exception]: calling the function with two named Deferred variables (``left``, ``right``) and serializing the resulting resolver tree. """ - from xorq.vendor.ibis.common.deferred import Deferred, Variable + from ._xorq import Deferred, Variable @safe def do_convert(): @@ -566,7 +564,7 @@ def do_convert(): def structured_to_join_predicate(data: tuple) -> Result[Callable, Exception]: """Reconstruct a binary join predicate from a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred @safe def do_convert():