From 79a5d1b6dc57dce527f5785c652c8ce6a7442ee9 Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 07:39:54 -0700 Subject: [PATCH 1/7] fix(gfql): support cuDF index byte reporting --- graphistry/compute/gfql/index/api.py | 16 ++++++-- .../gfql/index/test_node_type_segments.py | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/graphistry/compute/gfql/index/api.py b/graphistry/compute/gfql/index/api.py index 8fa76166bd..fc7ce98e24 100644 --- a/graphistry/compute/gfql/index/api.py +++ b/graphistry/compute/gfql/index/api.py @@ -13,7 +13,7 @@ import pandas as pd from graphistry.Engine import EngineAbstract, Engine, EngineAbstractType, POLARS_ENGINES, is_polars_df, resolve_engine -from graphistry.compute.typing import DataFrameT +from graphistry.compute.typing import DataFrameT, SeriesT from graphistry.Plottable import Plottable from .registry import ( AdjacencyIndex, ColStatsFact, PartitionValue, ColStatsRole, GfqlIndexRegistry, EMPTY_REGISTRY, NodeIdIndex, @@ -39,6 +39,16 @@ REGISTRY_ATTR = "_gfql_index_registry" +def _series_nbytes(value: SeriesT) -> int: + if hasattr(value, "estimated_size"): + return int(value.estimated_size()) + if hasattr(value, "nbytes"): + return int(value.nbytes) + if hasattr(value, "memory_usage"): + return int(value.memory_usage()) + return 0 + + # --- lightweight, thread-local index decision trace (for gfql_explain) ------- import threading as _threading _TRACE = _threading.local() @@ -591,7 +601,7 @@ def show_indexes( ) usable, reason = _index_usability(kind, typed.engine, valid, query_engine) # other_values is shared with the ordinary adjacency index, not added memory. - nbytes = sum(int(getattr(typed, name).nbytes) for name in ( + nbytes = sum(_series_nbytes(getattr(typed, name)) for name in ( "keys_sorted", "group_offsets", "row_positions", )) rows.append({ @@ -611,7 +621,7 @@ def show_indexes( is segment ) usable, reason = _index_usability(NODE_TYPE_SEGMENT, segment.engine, valid, query_engine) - nbytes = sum(int(getattr(segment, name).nbytes) for name in ( + nbytes = sum(_series_nbytes(getattr(segment, name)) for name in ( "keys_sorted", "group_offsets", "row_positions", )) rows.append({ diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index 511bf8d044..a43fb3d11d 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -10,6 +10,7 @@ from graphistry.compute.gfql.index import ( get_registry, index_trace, maybe_index_hop, with_index_policy, ) +from graphistry.compute.gfql.index.api import _series_nbytes from graphistry.compute.gfql.index.cypher_ddl import parse_index_ddl from graphistry.compute.gfql.index.wire import apply_index_op @@ -37,6 +38,42 @@ def _graph(engine: str): return graphistry.bind(node="id", source="src", destination="dst").nodes(nodes).edges(edges) +def test_node_segment_show_indexes_reports_cudf_bytes() -> None: + pytest.importorskip("cudf") + cupy = pytest.importorskip("cupy") + try: + device_count = cupy.cuda.runtime.getDeviceCount() + except Exception: + pytest.skip("CUDA device detection failed") + if device_count == 0: + pytest.skip("cuDF requires a CUDA device") + indexed = _indexed(_graph("cudf"), "cudf") + rows = indexed.show_indexes() + segment_rows = rows[rows["kind"] == "node_type_segment"] + assert not segment_rows.empty + assert segment_rows["nbytes"].sum() > 0 + + +@pytest.mark.parametrize("attribute,value,expected", [ + ("estimated_size", 7, 7), + ("nbytes", 3, 3), + ("memory_usage", 4, 4), +]) +def test_series_nbytes_uses_engine_measurement(attribute: str, value: int, expected: int) -> None: + class _MeasuredSeries: + pass + + if attribute == "nbytes": + setattr(_MeasuredSeries, attribute, property(lambda self: value)) + else: + setattr(_MeasuredSeries, attribute, lambda self: value) + assert _series_nbytes(_MeasuredSeries()) == expected + + +def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: + assert _series_nbytes(object()) == 0 + + def _pandas(frame): return frame.to_pandas() if hasattr(frame, "to_pandas") else frame From d9717728f007a31a98a81c5ddf03e23ee9ac26bb Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 13:02:35 -0700 Subject: [PATCH 2/7] refactor(gfql): dispatch index byte sizing by engine --- graphistry/Engine.py | 14 ++++++++ graphistry/compute/gfql/index/api.py | 21 ++++++++---- .../gfql/index/test_node_type_segments.py | 34 +++++++++++-------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/graphistry/Engine.py b/graphistry/Engine.py index 3209f5349c..9c00429998 100644 --- a/graphistry/Engine.py +++ b/graphistry/Engine.py @@ -15,6 +15,7 @@ # triggers ``graphistry.compute`` package init at runtime (would be circular). from graphistry.compute.typing import DataFrameT, PolarsFrame, PolarsSeriesT, SeriesT from graphistry.Plottable import Plottable + import cudf import polars as pl # TypeGuard, NOT TypeIs (PEP 742). TypeIs additionally narrows the negative branch, and to # do that soundly it REQUIRES the narrowed type to be consistent with the declared input @@ -249,6 +250,19 @@ def is_polars_df(df: object) -> "TypeGuard[PolarsFrame]": return df is not None and "polars" in type(df).__module__ +def is_cudf_series(series: object) -> "TypeGuard[cudf.Series]": + """True if ``series`` is a cuDF ``Series``. + + The module check avoids importing the optional cuDF dependency for other engines; + the exact ``isinstance`` check prevents look-alike classes from taking the cuDF path. + """ + if series is None or "cudf" not in type(series).__module__: + return False + import cudf + + return isinstance(series, cudf.Series) + + def active_frames_are_polars(g: Any) -> bool: """True if ``g``'s active table (nodes, else edges) is a polars frame.""" if g._nodes is not None: diff --git a/graphistry/compute/gfql/index/api.py b/graphistry/compute/gfql/index/api.py index fc7ce98e24..ee44233912 100644 --- a/graphistry/compute/gfql/index/api.py +++ b/graphistry/compute/gfql/index/api.py @@ -8,11 +8,14 @@ from __future__ import annotations import copy -from typing import Dict, List, Literal, Mapping, Optional, Sequence, Set, Tuple, cast +from typing import TYPE_CHECKING, Dict, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union, cast import pandas as pd -from graphistry.Engine import EngineAbstract, Engine, EngineAbstractType, POLARS_ENGINES, is_polars_df, resolve_engine +from graphistry.Engine import ( + EngineAbstract, Engine, EngineAbstractType, POLARS_ENGINES, + is_cudf_series, is_polars_df, is_polars_series, resolve_engine, +) from graphistry.compute.typing import DataFrameT, SeriesT from graphistry.Plottable import Plottable from .registry import ( @@ -34,17 +37,23 @@ TraceEngine, SidecarFactKind, ) +if TYPE_CHECKING: + import cudf + from graphistry.compute.typing import PolarsSeriesT + + CrossEngineSeriesT = Union[SeriesT, PolarsSeriesT, "cudf.Series"] + # Private Plottable attachment keys. Keep access behind helpers. POLICY_ATTR = "_gfql_index_policy" REGISTRY_ATTR = "_gfql_index_registry" -def _series_nbytes(value: SeriesT) -> int: - if hasattr(value, "estimated_size"): +def _series_nbytes(value: CrossEngineSeriesT) -> int: + if is_polars_series(value): return int(value.estimated_size()) - if hasattr(value, "nbytes"): + if is_cudf_series(value): return int(value.nbytes) - if hasattr(value, "memory_usage"): + if isinstance(value, pd.Series): return int(value.memory_usage()) return 0 diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index a43fb3d11d..870f7ca21f 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -54,20 +54,26 @@ def test_node_segment_show_indexes_reports_cudf_bytes() -> None: assert segment_rows["nbytes"].sum() > 0 -@pytest.mark.parametrize("attribute,value,expected", [ - ("estimated_size", 7, 7), - ("nbytes", 3, 3), - ("memory_usage", 4, 4), -]) -def test_series_nbytes_uses_engine_measurement(attribute: str, value: int, expected: int) -> None: - class _MeasuredSeries: - pass - - if attribute == "nbytes": - setattr(_MeasuredSeries, attribute, property(lambda self: value)) - else: - setattr(_MeasuredSeries, attribute, lambda self: value) - assert _series_nbytes(_MeasuredSeries()) == expected +def test_series_nbytes_uses_pandas_measurement() -> None: + series = pd.Series([1], dtype="int64") + assert _series_nbytes(series) == series.memory_usage() + + +def test_series_nbytes_uses_polars_measurement() -> None: + pl = pytest.importorskip("polars") + assert _series_nbytes(pl.Series([1], dtype=pl.Int64)) == 8 + + +def test_series_nbytes_uses_cudf_measurement() -> None: + cudf = pytest.importorskip("cudf") + try: + device_count = cudf.cuda.runtime.getDeviceCount() + except Exception: + pytest.skip("CUDA device detection failed") + if device_count == 0: + pytest.skip("cuDF requires a CUDA device") + series = cudf.Series([1], dtype="int64") + assert _series_nbytes(series) == series.nbytes def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: From eaf359cf5338932e6c19d4c9f8947278427e3941 Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 13:04:21 -0700 Subject: [PATCH 3/7] test(gfql): verify index byte engine dispatch --- .../compute/gfql/index/test_node_type_segments.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index 870f7ca21f..7c6ecf2151 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -54,17 +54,19 @@ def test_node_segment_show_indexes_reports_cudf_bytes() -> None: assert segment_rows["nbytes"].sum() > 0 -def test_series_nbytes_uses_pandas_measurement() -> None: +def test_series_nbytes_uses_pandas_measurement(monkeypatch: pytest.MonkeyPatch) -> None: series = pd.Series([1], dtype="int64") - assert _series_nbytes(series) == series.memory_usage() + monkeypatch.setattr(pd.Series, "memory_usage", lambda self: 4) + assert _series_nbytes(series) == 4 -def test_series_nbytes_uses_polars_measurement() -> None: +def test_series_nbytes_uses_polars_measurement(monkeypatch: pytest.MonkeyPatch) -> None: pl = pytest.importorskip("polars") - assert _series_nbytes(pl.Series([1], dtype=pl.Int64)) == 8 + monkeypatch.setattr(pl.Series, "estimated_size", lambda self: 7) + assert _series_nbytes(pl.Series([1], dtype=pl.Int64)) == 7 -def test_series_nbytes_uses_cudf_measurement() -> None: +def test_series_nbytes_uses_cudf_measurement(monkeypatch: pytest.MonkeyPatch) -> None: cudf = pytest.importorskip("cudf") try: device_count = cudf.cuda.runtime.getDeviceCount() @@ -73,7 +75,8 @@ def test_series_nbytes_uses_cudf_measurement() -> None: if device_count == 0: pytest.skip("cuDF requires a CUDA device") series = cudf.Series([1], dtype="int64") - assert _series_nbytes(series) == series.nbytes + monkeypatch.setattr(cudf.Series, "nbytes", 3) + assert _series_nbytes(series) == 3 def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: From 0260a436cd955ec7ebc90e038f108dd433301ad3 Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 13:35:54 -0700 Subject: [PATCH 4/7] test(gfql): exercise native index byte measurements --- .../gfql/index/test_node_type_segments.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index 7c6ecf2151..a07565bc59 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -54,19 +54,18 @@ def test_node_segment_show_indexes_reports_cudf_bytes() -> None: assert segment_rows["nbytes"].sum() > 0 -def test_series_nbytes_uses_pandas_measurement(monkeypatch: pytest.MonkeyPatch) -> None: +def test_series_nbytes_uses_pandas_measurement() -> None: series = pd.Series([1], dtype="int64") - monkeypatch.setattr(pd.Series, "memory_usage", lambda self: 4) - assert _series_nbytes(series) == 4 + assert _series_nbytes(series) == series.memory_usage() -def test_series_nbytes_uses_polars_measurement(monkeypatch: pytest.MonkeyPatch) -> None: +def test_series_nbytes_uses_polars_measurement() -> None: pl = pytest.importorskip("polars") - monkeypatch.setattr(pl.Series, "estimated_size", lambda self: 7) - assert _series_nbytes(pl.Series([1], dtype=pl.Int64)) == 7 + series = pl.Series([1], dtype=pl.Int64) + assert _series_nbytes(series) == series.estimated_size() -def test_series_nbytes_uses_cudf_measurement(monkeypatch: pytest.MonkeyPatch) -> None: +def test_series_nbytes_uses_cudf_measurement() -> None: cudf = pytest.importorskip("cudf") try: device_count = cudf.cuda.runtime.getDeviceCount() @@ -75,8 +74,7 @@ def test_series_nbytes_uses_cudf_measurement(monkeypatch: pytest.MonkeyPatch) -> if device_count == 0: pytest.skip("cuDF requires a CUDA device") series = cudf.Series([1], dtype="int64") - monkeypatch.setattr(cudf.Series, "nbytes", 3) - assert _series_nbytes(series) == 3 + assert _series_nbytes(series) == series.nbytes def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: From d22e49883d005643dbcfab710336f3b3a33ab78e Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 13:43:09 -0700 Subject: [PATCH 5/7] fix(gfql): measure cuDF series memory usage --- graphistry/compute/gfql/index/api.py | 2 +- graphistry/tests/compute/gfql/index/test_node_type_segments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/gfql/index/api.py b/graphistry/compute/gfql/index/api.py index ee44233912..3e59e2df95 100644 --- a/graphistry/compute/gfql/index/api.py +++ b/graphistry/compute/gfql/index/api.py @@ -52,7 +52,7 @@ def _series_nbytes(value: CrossEngineSeriesT) -> int: if is_polars_series(value): return int(value.estimated_size()) if is_cudf_series(value): - return int(value.nbytes) + return int(value.memory_usage()) if isinstance(value, pd.Series): return int(value.memory_usage()) return 0 diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index a07565bc59..172e0f8bf1 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -74,7 +74,7 @@ def test_series_nbytes_uses_cudf_measurement() -> None: if device_count == 0: pytest.skip("cuDF requires a CUDA device") series = cudf.Series([1], dtype="int64") - assert _series_nbytes(series) == series.nbytes + assert _series_nbytes(series) == series.memory_usage() def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: From 039affd55cd8c91b47912883cfd8d244aec691a7 Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 13:56:04 -0700 Subject: [PATCH 6/7] Fix GFQL index byte accounting across array engines --- graphistry/Engine.py | 14 +++++++ graphistry/compute/gfql/index/api.py | 17 ++++++--- .../gfql/index/test_node_type_segments.py | 37 ++++++++++++++----- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/graphistry/Engine.py b/graphistry/Engine.py index 9c00429998..19bcfdcef4 100644 --- a/graphistry/Engine.py +++ b/graphistry/Engine.py @@ -16,6 +16,7 @@ from graphistry.compute.typing import DataFrameT, PolarsFrame, PolarsSeriesT, SeriesT from graphistry.Plottable import Plottable import cudf + import cupy import polars as pl # TypeGuard, NOT TypeIs (PEP 742). TypeIs additionally narrows the negative branch, and to # do that soundly it REQUIRES the narrowed type to be consistent with the declared input @@ -263,6 +264,19 @@ def is_cudf_series(series: object) -> "TypeGuard[cudf.Series]": return isinstance(series, cudf.Series) +def is_cupy_array(array: object) -> "TypeGuard[cupy.ndarray]": + """True if ``array`` is a CuPy array. + + The module check avoids importing the optional GPU dependency for other + engines; the exact ``isinstance`` check rejects look-alikes. + """ + if array is None or "cupy" not in type(array).__module__: + return False + import cupy + + return isinstance(array, cupy.ndarray) + + def active_frames_are_polars(g: Any) -> bool: """True if ``g``'s active table (nodes, else edges) is a polars frame.""" if g._nodes is not None: diff --git a/graphistry/compute/gfql/index/api.py b/graphistry/compute/gfql/index/api.py index 3e59e2df95..aa39d51e0f 100644 --- a/graphistry/compute/gfql/index/api.py +++ b/graphistry/compute/gfql/index/api.py @@ -10,11 +10,12 @@ import copy from typing import TYPE_CHECKING, Dict, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union, cast +import numpy as np import pandas as pd from graphistry.Engine import ( EngineAbstract, Engine, EngineAbstractType, POLARS_ENGINES, - is_cudf_series, is_polars_df, is_polars_series, resolve_engine, + is_cudf_series, is_cupy_array, is_polars_df, is_polars_series, resolve_engine, ) from graphistry.compute.typing import DataFrameT, SeriesT from graphistry.Plottable import Plottable @@ -39,20 +40,24 @@ if TYPE_CHECKING: import cudf - from graphistry.compute.typing import PolarsSeriesT + from graphistry.compute.typing import ArrayLike, PolarsSeriesT - CrossEngineSeriesT = Union[SeriesT, PolarsSeriesT, "cudf.Series"] + CrossEngineByteSourceT = Union[ArrayLike, SeriesT, PolarsSeriesT, "cudf.Series"] # Private Plottable attachment keys. Keep access behind helpers. POLICY_ATTR = "_gfql_index_policy" REGISTRY_ATTR = "_gfql_index_registry" -def _series_nbytes(value: CrossEngineSeriesT) -> int: +def _byte_source_nbytes(value: CrossEngineByteSourceT) -> int: if is_polars_series(value): return int(value.estimated_size()) if is_cudf_series(value): return int(value.memory_usage()) + if isinstance(value, np.ndarray): + return int(value.nbytes) + if is_cupy_array(value): + return int(value.nbytes) if isinstance(value, pd.Series): return int(value.memory_usage()) return 0 @@ -610,7 +615,7 @@ def show_indexes( ) usable, reason = _index_usability(kind, typed.engine, valid, query_engine) # other_values is shared with the ordinary adjacency index, not added memory. - nbytes = sum(_series_nbytes(getattr(typed, name)) for name in ( + nbytes = sum(_byte_source_nbytes(getattr(typed, name)) for name in ( "keys_sorted", "group_offsets", "row_positions", )) rows.append({ @@ -630,7 +635,7 @@ def show_indexes( is segment ) usable, reason = _index_usability(NODE_TYPE_SEGMENT, segment.engine, valid, query_engine) - nbytes = sum(_series_nbytes(getattr(segment, name)) for name in ( + nbytes = sum(_byte_source_nbytes(getattr(segment, name)) for name in ( "keys_sorted", "group_offsets", "row_positions", )) rows.append({ diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index 172e0f8bf1..24af003692 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -3,6 +3,7 @@ import pandas as pd import pytest +import numpy as np import graphistry from graphistry.Engine import Engine @@ -10,7 +11,8 @@ from graphistry.compute.gfql.index import ( get_registry, index_trace, maybe_index_hop, with_index_policy, ) -from graphistry.compute.gfql.index.api import _series_nbytes +from graphistry.Engine import is_cupy_array +from graphistry.compute.gfql.index.api import _byte_source_nbytes from graphistry.compute.gfql.index.cypher_ddl import parse_index_ddl from graphistry.compute.gfql.index.wire import apply_index_op @@ -54,18 +56,18 @@ def test_node_segment_show_indexes_reports_cudf_bytes() -> None: assert segment_rows["nbytes"].sum() > 0 -def test_series_nbytes_uses_pandas_measurement() -> None: +def test_byte_source_nbytes_uses_pandas_measurement() -> None: series = pd.Series([1], dtype="int64") - assert _series_nbytes(series) == series.memory_usage() + assert _byte_source_nbytes(series) == series.memory_usage() -def test_series_nbytes_uses_polars_measurement() -> None: +def test_byte_source_nbytes_uses_polars_measurement() -> None: pl = pytest.importorskip("polars") series = pl.Series([1], dtype=pl.Int64) - assert _series_nbytes(series) == series.estimated_size() + assert _byte_source_nbytes(series) == series.estimated_size() -def test_series_nbytes_uses_cudf_measurement() -> None: +def test_byte_source_nbytes_uses_cudf_measurement() -> None: cudf = pytest.importorskip("cudf") try: device_count = cudf.cuda.runtime.getDeviceCount() @@ -74,11 +76,28 @@ def test_series_nbytes_uses_cudf_measurement() -> None: if device_count == 0: pytest.skip("cuDF requires a CUDA device") series = cudf.Series([1], dtype="int64") - assert _series_nbytes(series) == series.memory_usage() + assert _byte_source_nbytes(series) == series.memory_usage() -def test_series_nbytes_defaults_to_zero_for_unknown_series() -> None: - assert _series_nbytes(object()) == 0 +def test_byte_source_nbytes_defaults_to_zero_for_unknown_byte_source() -> None: + assert _byte_source_nbytes(object()) == 0 + + +def test_byte_source_nbytes_uses_numpy_measurement() -> None: + assert _byte_source_nbytes(np.asarray([1], dtype="int64")) == 8 + + +def test_byte_source_nbytes_uses_cupy_measurement() -> None: + cupy = pytest.importorskip("cupy") + try: + device_count = cupy.cuda.runtime.getDeviceCount() + except Exception: + pytest.skip("CUDA device detection failed") + if device_count == 0: + pytest.skip("CuPy requires a CUDA device") + array = cupy.asarray([1], dtype="int64") + assert _byte_source_nbytes(array) == 8 + assert is_cupy_array(array) def _pandas(frame): From f6beb08471f4117abf908ce53460f9f096d3e020 Mon Sep 17 00:00:00 2001 From: lmeyerov Date: Sat, 17 Oct 2026 14:17:34 -0700 Subject: [PATCH 7/7] test: exercise engine guards without CUDA --- .../gfql/index/test_node_type_segments.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/graphistry/tests/compute/gfql/index/test_node_type_segments.py b/graphistry/tests/compute/gfql/index/test_node_type_segments.py index 24af003692..e1fecf746f 100644 --- a/graphistry/tests/compute/gfql/index/test_node_type_segments.py +++ b/graphistry/tests/compute/gfql/index/test_node_type_segments.py @@ -1,9 +1,11 @@ """Node type-segment index must preserve canonical GFQL results (edge typed-index dual).""" from __future__ import annotations +import numpy as np import pandas as pd import pytest -import numpy as np +import sys +import types import graphistry from graphistry.Engine import Engine @@ -17,6 +19,22 @@ from graphistry.compute.gfql.index.wire import apply_index_op +class _CpuCudfSeries: + def __init__(self, values: list[int]) -> None: + self.values = values + + def memory_usage(self) -> int: + return 16 + + +class _CpuCuPyArray: + nbytes = 8 + + +_CpuCudfSeries.__module__ = "cudf" +_CpuCuPyArray.__module__ = "cupy" + + def _graph(engine: str): # Node 1 has TWO labels (membership); node 5 has a null type; node 4 is # label-only-False; a null type never matches any scalar segment. @@ -79,6 +97,12 @@ def test_byte_source_nbytes_uses_cudf_measurement() -> None: assert _byte_source_nbytes(series) == series.memory_usage() +def test_byte_source_nbytes_uses_cudf_guard_without_cuda(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setitem(sys.modules, "cudf", types.SimpleNamespace(Series=_CpuCudfSeries)) + series = _CpuCudfSeries([1]) + assert _byte_source_nbytes(series) == 16 + + def test_byte_source_nbytes_defaults_to_zero_for_unknown_byte_source() -> None: assert _byte_source_nbytes(object()) == 0 @@ -100,6 +124,13 @@ def test_byte_source_nbytes_uses_cupy_measurement() -> None: assert is_cupy_array(array) +def test_byte_source_nbytes_uses_cupy_guard_without_cuda(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setitem(sys.modules, "cupy", types.SimpleNamespace(ndarray=_CpuCuPyArray)) + array = _CpuCuPyArray() + assert _byte_source_nbytes(array) == 8 + assert is_cupy_array(array) + + def _pandas(frame): return frame.to_pandas() if hasattr(frame, "to_pandas") else frame