From f9224365b4be177c896b245ea3fdadb9654aa3cb Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 25 May 2026 10:19:28 -0700 Subject: [PATCH 1/4] feat(gfql): add plottable schema accessor --- CHANGELOG.md | 1 + docs/source/api/plotter.rst | 3 +++ docs/source/gfql/schema.rst | 7 ++++++ graphistry/Plottable.py | 9 +++++++ graphistry/PlotterBase.py | 19 +++++++++++++- .../tests/compute/gfql/test_public_schema.py | 25 +++++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e5639f9b..76504a3bf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - **GFQL schema effects (#1485)**: Added an internal typed schema-effect model for graph-growing GFQL calls so bound experimental `GraphSchema` snapshots are updated after successful degree, PageRank-style node-property writes, and edge-property write calls. Later local validation can see properties added by those calls without exposing a public `SchemaEffect` API or changing remote GFQL transport. - **NetworkX Python compute API (#1619)**: Added `g.compute_networkx(...)` for the curated NetworkX algorithm subset already exposed through GFQL local Cypher, including node, edge, and `k_core` graph-returning outputs, plus updated NetworkX notebook/API docs. +- **GFQL schema accessor (#1632)**: Added experimental read-only `g.schema` for inspecting a `GraphSchema` bound through `bind(schema=...)` without reaching into private `_gfql_schema` storage. Use `g.schema is not None` for predicate checks. - **GFQL NetworkX CALL parity (#1058)**: Expanded the local Cypher `graphistry.nx.*` CALL surface with explicit NetworkX dispatch for `degree_centrality`, `closeness_centrality`, `eigenvector_centrality`, `katz_centrality`, `connected_components`, `strongly_connected_components`, `core_number`, and multi-output `hits`, including row and `.write()` coverage. - **NetworkX/SciPy optional dependency policy (#1618)**: Declared supported `networkx>=2.5,<4` and optional `scipy>=1.5,<2` ranges for NetworkX-backed GFQL CALL procedures, with runtime version guards and a focused lower/current-upper CI matrix. - **GFQL schema Arrow boundary APIs (#1339)**: Added experimental public schema↔Arrow import/export helpers, graph-level Arrow declaration payloads, and opt-in `schema_validate='strict'|'autofix'` enforcement for `plot()`, `upload()`, `to_arrow()`, and `validate_arrow_schema()` when a `GraphSchema` is bound. diff --git a/docs/source/api/plotter.rst b/docs/source/api/plotter.rst index 43852f3e5c..8d168bc517 100644 --- a/docs/source/api/plotter.rst +++ b/docs/source/api/plotter.rst @@ -27,6 +27,9 @@ the boundary. Use ``schema_validate='autofix'`` to cast compatible columns to declared Arrow types after normal Arrow conversion. The default ``schema_validate=False`` preserves existing behavior. +Use the experimental read-only ``g.schema`` accessor to inspect the bound +``GraphSchema`` object, or ``g.has_schema()`` when only a predicate is needed. + .. toctree:: :maxdepth: 3 diff --git a/docs/source/gfql/schema.rst b/docs/source/gfql/schema.rst index 9d99b41f41..0e9e39facd 100644 --- a/docs/source/gfql/schema.rst +++ b/docs/source/gfql/schema.rst @@ -73,6 +73,8 @@ check against. ) g.gfql_validate("MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name") + assert g.schema is schema + assert g.has_schema() Schema Objects -------------- @@ -96,6 +98,11 @@ Schema Objects makes schema-bound ``g.gfql_validate(...)`` permissive by default; callers can still override per call with ``g.gfql_validate(..., strict=True)``. +``g.schema`` and ``g.has_schema()`` + Read back the experimental ``GraphSchema`` bound with ``bind(schema=...)``. + ``g.schema`` returns the bound object or ``None``; ``g.has_schema()`` returns + a matching boolean. Use ``bind(schema=...)`` to attach schemas, not assignment. + ``NodeType.to_arrow()`` and ``EdgeType.to_arrow()`` Export declarations as ``pyarrow.Schema`` objects through GFQL's row-schema bridge. Label/type columns are included by default so exports line up with diff --git a/graphistry/Plottable.py b/graphistry/Plottable.py index 970dd228ec..e658346f36 100644 --- a/graphistry/Plottable.py +++ b/graphistry/Plottable.py @@ -22,6 +22,7 @@ from graphistry.models.surfaces.graphistry_frontend.url_params import URLParamsDict if TYPE_CHECKING: + from graphistry.schema import GraphSchema try: from umap import UMAP except: @@ -82,6 +83,7 @@ class Plottable(Protocol): _point_y : Optional[str] _point_longitude : Optional[str] _point_latitude : Optional[str] + _gfql_schema: Optional["GraphSchema"] _height : int _render : RenderModesConcrete _url_params : URLParamsDict @@ -370,6 +372,13 @@ def bind( def copy(self) -> 'Plottable': ... + @property + def schema(self) -> Optional["GraphSchema"]: + ... + + def has_schema(self) -> bool: + ... + # ### ComputeMixin def get_indegrees(self, col: str = 'degree_in') -> 'Plottable': diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index c512b734e0..711d774e74 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -58,6 +58,9 @@ from .util import setup_logger logger = setup_logger(__name__) +if TYPE_CHECKING: + from graphistry.schema import GraphSchema + _MAPPED_PROPERTY_ENCODING_METHODS: Dict[str, Tuple[str, str, str]] = { "encode_edge_size": ("edge", "size", "edgeSizeEncoding"), "encode_edge_weight": ("edge", "weight", "edgeWeightEncoding"), @@ -267,7 +270,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._point_y : Optional[str] = None self._point_longitude : Optional[str] = None self._point_latitude : Optional[str] = None - self._gfql_schema : Any = None + self._gfql_schema : Optional["GraphSchema"] = None # Settings self._height : int = 500 self._render : RenderModesConcrete = resolve_render_mode(self, True) @@ -1790,6 +1793,20 @@ def bind(self, return res + @property + def schema(self) -> Optional["GraphSchema"]: + """Return the bound experimental GFQL ``GraphSchema``, if any. + + The returned object is the same schema instance supplied through + ``bind(schema=...)``. The accessor is read-only: use ``bind(schema=...)`` + to attach a schema to a new plotter. + """ + return self._gfql_schema + + def has_schema(self) -> bool: + """Return ``True`` when this plotter has a GFQL ``GraphSchema`` bound.""" + return self._gfql_schema is not None + def copy(self) -> Plottable: return copy.copy(self) diff --git a/graphistry/tests/compute/gfql/test_public_schema.py b/graphistry/tests/compute/gfql/test_public_schema.py index 163ab90877..7c07396520 100644 --- a/graphistry/tests/compute/gfql/test_public_schema.py +++ b/graphistry/tests/compute/gfql/test_public_schema.py @@ -194,6 +194,31 @@ def test_bind_schema_is_chainable_and_used_by_preflight() -> None: assert report["ok"] is True +def test_schema_accessor_returns_bound_schema() -> None: + schema = _schema() + g = _graph(schema) + + assert g.schema is schema + assert g.has_schema() is True + + +def test_schema_accessor_is_read_only() -> None: + schema = _schema() + g = _graph(schema) + + with pytest.raises(AttributeError): + g.schema = None # type: ignore[misc] + + assert g.schema is schema + + +def test_schema_accessor_returns_none_when_unbound() -> None: + g = graphistry.bind() + + assert g.schema is None + assert g.has_schema() is False + + def test_bound_schema_arrow_boundary_strict_passes() -> None: pa = pytest.importorskip("pyarrow") g = _graph(_schema()) From bc8cc1dbcd259c8122bf8608ff1297f4d64dd3ff Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 25 May 2026 10:39:01 -0700 Subject: [PATCH 2/4] docs(review): require body files for github markdown --- agents/skills/review/SKILL.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/agents/skills/review/SKILL.md b/agents/skills/review/SKILL.md index 64b3c1f184..e754d3126c 100644 --- a/agents/skills/review/SKILL.md +++ b/agents/skills/review/SKILL.md @@ -296,6 +296,32 @@ Write `plans//final-report.md` with: `both` mode: - Complete findings artifacts first, then comment flow. +### GitHub Markdown Body Safety + +When creating or updating PR descriptions, issue comments, PR comments, or +review summaries with multi-line Markdown, backticks, code fences, `$()`, or +literal `\n` sequences, do **not** pass the body inline through shell flags such +as `--body "..."`, `--body '...'`, `-f body=...`, or `-F body=...`. + +Instead: + +1. Write the exact body to a local Markdown artifact, preferably under + `plans//github-body-.md` for durable review or `/tmp/` for a + throwaway retry. +2. Inspect the rendered source with `sed -n '1,220p' ` before + posting. +3. Use file-based GitHub CLI flags: + - `gh pr create --body-file ` + - `gh pr edit --body-file ` + - `gh issue comment --body-file ` + - `gh pr comment --body-file ` +4. After posting, verify with `gh pr view --json body` or + `gh api repos///issues/comments/` and confirm the + body contains real newlines and literal Markdown backticks. + +Reason: inline shell bodies can turn Markdown backticks into command +substitution and can post literal `\n` text instead of newlines. + ## Guardrails - `fixes=deferred`: read-only; do not edit source files. From c76fd9eb44efab974161fe32abd61792b0cdcb6a Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 25 May 2026 10:48:47 -0700 Subject: [PATCH 3/4] docs(gfql): clarify schema accessor scope --- docs/source/api/plotter.rst | 3 +++ docs/source/gfql/schema.rst | 21 ++++++++++++++++--- graphistry/PlotterBase.py | 6 +++++- .../tests/compute/gfql/test_public_schema.py | 2 ++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/source/api/plotter.rst b/docs/source/api/plotter.rst index 8d168bc517..6723b28a80 100644 --- a/docs/source/api/plotter.rst +++ b/docs/source/api/plotter.rst @@ -29,6 +29,9 @@ declared Arrow types after normal Arrow conversion. The default Use the experimental read-only ``g.schema`` accessor to inspect the bound ``GraphSchema`` object, or ``g.has_schema()`` when only a predicate is needed. +This reports only the local declaration attached through ``bind(schema=...)``: +it does not infer a schema from data, fetch a remote dataset schema, or serialize +the schema into ``gfql_remote()`` requests. .. toctree:: :maxdepth: 3 diff --git a/docs/source/gfql/schema.rst b/docs/source/gfql/schema.rst index 0e9e39facd..0a546d67be 100644 --- a/docs/source/gfql/schema.rst +++ b/docs/source/gfql/schema.rst @@ -102,6 +102,16 @@ Schema Objects Read back the experimental ``GraphSchema`` bound with ``bind(schema=...)``. ``g.schema`` returns the bound object or ``None``; ``g.has_schema()`` returns a matching boolean. Use ``bind(schema=...)`` to attach schemas, not assignment. + This is local declaration introspection only. It does not infer schemas from + data, fetch or hydrate remote dataset schemas, or serialize schemas into + ``gfql_remote()`` requests in this release. + + Per-type declarations such as Cat, Dog, and Car are represented by + ``GraphSchema.node_types``. The stable public type identity is + ``NodeType.name``; ``NodeType.labels`` are the GFQL label predicates that map + onto label columns such as ``label__Cat``. For example, Cat and Dog can both + carry an ``Animal`` label while still preserving separate Cat and Dog + property contracts. ``NodeType.to_arrow()`` and ``EdgeType.to_arrow()`` Export declarations as ``pyarrow.Schema`` objects through GFQL's row-schema @@ -217,12 +227,17 @@ The public schema is consumed by local validation APIs, including: ``gfql_remote(...)`` is different. It compiles Cypher strings locally and sends the resulting GFQL wire payload to the server, but this release does **not** serialize a bound ``GraphSchema`` into remote GFQL requests. Remote execution -therefore still depends on the server-side dataset schema and GFQL support. If -you want declared schema checks before a remote call, run +therefore still depends on the server-side dataset metadata and GFQL support. If +you want local declared-schema checks before a remote call, run ``g.gfql_validate(query)`` locally first, then call ``g.gfql_remote(query)``. Remote schema transport is planned as a follow-on after the local schema -contract and serialization boundary are stable. +contract and serialization boundary are stable. The intended direction is a +versioned graph-schema envelope derived from ``GraphSchema.to_arrow()``: exact +Arrow schemas for merged node/edge tables and per-type declarations, plus a +JSON summary for dataset metadata, UI, and REST consumers. That future transport +should live beside ``gfql_query`` / ``gfql_operations`` rather than as fake data +tables. Compatibility Notes ------------------- diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 711d774e74..869bf83f5a 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -1800,11 +1800,15 @@ def schema(self) -> Optional["GraphSchema"]: The returned object is the same schema instance supplied through ``bind(schema=...)``. The accessor is read-only: use ``bind(schema=...)`` to attach a schema to a new plotter. + + This is local declaration introspection only. It does not infer a schema + from data, fetch a remote dataset schema, or serialize the schema into + ``gfql_remote()`` requests in this release. """ return self._gfql_schema def has_schema(self) -> bool: - """Return ``True`` when this plotter has a GFQL ``GraphSchema`` bound.""" + """Return ``True`` when this plotter has a local GFQL schema bound.""" return self._gfql_schema is not None def copy(self) -> Plottable: diff --git a/graphistry/tests/compute/gfql/test_public_schema.py b/graphistry/tests/compute/gfql/test_public_schema.py index 7c07396520..7456bfc406 100644 --- a/graphistry/tests/compute/gfql/test_public_schema.py +++ b/graphistry/tests/compute/gfql/test_public_schema.py @@ -190,6 +190,8 @@ def test_bind_schema_is_chainable_and_used_by_preflight() -> None: g = _graph(schema).bind(point_color="name") assert g._gfql_schema is schema + assert g.schema is schema + assert g.has_schema() is True report = g.gfql_validate("MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name AS name") assert report["ok"] is True From 033aac2d0153c0485dc345c8647f8b2a0ba8b5c0 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 25 May 2026 14:47:23 -0700 Subject: [PATCH 4/4] refactor(gfql): keep schema accessor minimal --- docs/source/api/plotter.rst | 8 ++++---- docs/source/gfql/schema.rst | 9 +++++---- graphistry/Plottable.py | 3 --- graphistry/PlotterBase.py | 4 ---- graphistry/tests/compute/gfql/test_public_schema.py | 3 --- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/docs/source/api/plotter.rst b/docs/source/api/plotter.rst index 6723b28a80..8f9286af36 100644 --- a/docs/source/api/plotter.rst +++ b/docs/source/api/plotter.rst @@ -28,10 +28,10 @@ declared Arrow types after normal Arrow conversion. The default ``schema_validate=False`` preserves existing behavior. Use the experimental read-only ``g.schema`` accessor to inspect the bound -``GraphSchema`` object, or ``g.has_schema()`` when only a predicate is needed. -This reports only the local declaration attached through ``bind(schema=...)``: -it does not infer a schema from data, fetch a remote dataset schema, or serialize -the schema into ``gfql_remote()`` requests. +``GraphSchema`` object. Check ``g.schema is not None`` when only a predicate is +needed. This reports only the local declaration attached through +``bind(schema=...)``: it does not infer a schema from data, fetch a remote +dataset schema, or serialize the schema into ``gfql_remote()`` requests. .. toctree:: :maxdepth: 3 diff --git a/docs/source/gfql/schema.rst b/docs/source/gfql/schema.rst index 0a546d67be..c6b3b4bd0b 100644 --- a/docs/source/gfql/schema.rst +++ b/docs/source/gfql/schema.rst @@ -74,7 +74,7 @@ check against. g.gfql_validate("MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name") assert g.schema is schema - assert g.has_schema() + assert g.schema is not None Schema Objects -------------- @@ -98,10 +98,11 @@ Schema Objects makes schema-bound ``g.gfql_validate(...)`` permissive by default; callers can still override per call with ``g.gfql_validate(..., strict=True)``. -``g.schema`` and ``g.has_schema()`` +``g.schema`` Read back the experimental ``GraphSchema`` bound with ``bind(schema=...)``. - ``g.schema`` returns the bound object or ``None``; ``g.has_schema()`` returns - a matching boolean. Use ``bind(schema=...)`` to attach schemas, not assignment. + ``g.schema`` returns the bound object or ``None``. Use + ``g.schema is not None`` when only a predicate is needed. Use + ``bind(schema=...)`` to attach schemas, not assignment. This is local declaration introspection only. It does not infer schemas from data, fetch or hydrate remote dataset schemas, or serialize schemas into ``gfql_remote()`` requests in this release. diff --git a/graphistry/Plottable.py b/graphistry/Plottable.py index e658346f36..033210920b 100644 --- a/graphistry/Plottable.py +++ b/graphistry/Plottable.py @@ -376,9 +376,6 @@ def copy(self) -> 'Plottable': def schema(self) -> Optional["GraphSchema"]: ... - def has_schema(self) -> bool: - ... - # ### ComputeMixin def get_indegrees(self, col: str = 'degree_in') -> 'Plottable': diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 869bf83f5a..ec6ecfe775 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -1807,10 +1807,6 @@ def schema(self) -> Optional["GraphSchema"]: """ return self._gfql_schema - def has_schema(self) -> bool: - """Return ``True`` when this plotter has a local GFQL schema bound.""" - return self._gfql_schema is not None - def copy(self) -> Plottable: return copy.copy(self) diff --git a/graphistry/tests/compute/gfql/test_public_schema.py b/graphistry/tests/compute/gfql/test_public_schema.py index 7456bfc406..57e29c62e4 100644 --- a/graphistry/tests/compute/gfql/test_public_schema.py +++ b/graphistry/tests/compute/gfql/test_public_schema.py @@ -191,7 +191,6 @@ def test_bind_schema_is_chainable_and_used_by_preflight() -> None: assert g._gfql_schema is schema assert g.schema is schema - assert g.has_schema() is True report = g.gfql_validate("MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name AS name") assert report["ok"] is True @@ -201,7 +200,6 @@ def test_schema_accessor_returns_bound_schema() -> None: g = _graph(schema) assert g.schema is schema - assert g.has_schema() is True def test_schema_accessor_is_read_only() -> None: @@ -218,7 +216,6 @@ def test_schema_accessor_returns_none_when_unbound() -> None: g = graphistry.bind() assert g.schema is None - assert g.has_schema() is False def test_bound_schema_arrow_boundary_strict_passes() -> None: