ci: run python lint on tests/ and docs/, as its own job - #896
Merged
Merged
Conversation
The Python style check already existed, but only covered `graphframes`, `python/dev` and `dev`. `python/tests` and `python/docs` were never format-checked, so formatting churn in those directories accumulated on main and surfaced later inside unrelated feature PRs. `python/tests` is touched by most feature PRs, which is where this is felt most. Changes: - Add `tests` and `docs` to the checked paths, in both the CI workflow and .pre-commit-config.yaml, and note that the two lists must agree. - Move the style check out of the Spark test matrix into a standalone `lint` job. It ran three times (once per matrix entry) behind a full JDK/Spark/jar setup; now it runs once and reports independently. Each tool is its own step so a failure names the tool. - Split black/isort/flake8 into separate steps for clearer failures. - Fix `[tool.black] include`, which was a list where black expects a regex string. A list silently disables black's default `\.pyi?$` filter, so black tried to parse Makefile/.rst/.css when handed a directory. This blocked linting `docs/` at all. - Apply black + isort to the newly covered files, and fix the six remaining flake8 findings (3x E265, 2x E501, 1x W391). Everything outside the two newly-covered directories is untouched; `graphframes`, `dev` and `../dev` were already clean and stay byte identical.
Merged
python/graphframes/connect/proto/ is generated by `buf generate` (see buf.gen.yaml, which runs the protocolbuffers/pyi and python plugins into that directory). Formatting generated output fights the generator: black reflows it, and the next regen emits the original again, so the same churn reappears on every regeneration. This already bit graphframes#809, where graphframes_pb2.pyi picked up a reformatted __slots__ tuple that has nothing to do with that PR. black needs force-exclude rather than exclude, because exclude is not applied to paths passed explicitly on the command line, and both CI and the pre-commit hook pass paths explicitly. flake8 is left alone: tox.ini already carves out E501/F401 for this directory, and its remaining checks do not rewrite the file.
james-willis
marked this pull request as ready for review
August 31, 2026 18:22
cursor Bot
pushed a commit
to rjurney/graphframes
that referenced
this pull request
Sep 2, 2026
Merging the latest rjurney/pypi-tutorials in to test the neo4j.py fixes against a clean base surfaced an invalid pyproject.toml already sitting on that branch (from "Merge branch 'main' into rjurney/pypi-tutorials", 37df7b9): the [tool.black] section had two leftover merge-conflict remnants, a stray `'''` and `=======`, making the file invalid TOML. That breaks poetry, black and isort outright for anyone on the branch. Removed both stray lines. That still left `force-exclude` combining main's proto exclusion with this branch's tutorials-data exclusion as a Python list - `["/graphframes/connect/proto/", "/graphframes/tutorials/data/"]` - which black's own `--verbose` output confirmed matches every path ("ignored: matches the --force-exclude regular expression" on files that match neither string): `force-exclude` needs a single regex string, not a list, the exact same mistake `include` had before graphframes#896 fixed it. Combined both into one regex string with alternation instead. Verified: `python -c "import tomllib; tomllib.load(...)"` now parses cleanly; `black --check` now finds and correctly formats 40 real files while still excluding graphframes_pb2.pyi and tutorials/data/*; isort and flake8 both clean; poetry check/poetry check --lock clean; full local test suite (109 tests) passes. Co-authored-by: Russell Jurney <rjurney@users.noreply.github.com>
rjurney
added a commit
to rjurney/graphframes
that referenced
this pull request
Sep 5, 2026
…oordinates (#10) * fix(python): repair corrupted pyproject.toml on rjurney/pypi-tutorials `poetry install`/`poetry check` currently fail outright on this branch: Invalid TOML file /workspace/python/pyproject.toml: Unexpected end of file at line 137 col 0 The [tool.black] section has two leftover merge-conflict remnants - a stray `'''` and `=======` - left behind by "Merge branch 'main' into rjurney/pypi-tutorials" (37df7b9), making the file invalid TOML. Removed both stray lines. That still left `force-exclude` combining main's proto exclusion with this branch's tutorials-data exclusion as a Python list - `["/graphframes/connect/proto/", "/graphframes/tutorials/data/"]` - which is the same mistake `include` had before graphframes#896 fixed it: black's own `--verbose` output confirms a list there matches *every* file ("ignored: matches the --force-exclude regular expression" on files that match neither string), not just the two intended paths. `force-exclude` needs a single regex string. Combined both exclusions into one string with alternation instead. Verified on a clean checkout of origin/rjurney/pypi-tutorials: - `poetry check` and `poetry install --with=dev` both now succeed (they currently fail with exit code 1 on this branch as-is). - `python -c "import tomllib; tomllib.load(...)"` parses the file. - `black --check` now finds and correctly formats all 40 real Python files under graphframes/tests/docs/dev, while still excluding the generated graphframes_pb2.pyi and everything under graphframes/tutorials/data/. - `isort --check` and `flake8` both clean. - `poetry check --lock` clean (no lock-file drift from this fix). - Full local test suite: 109/109 passing. Co-authored-by: Russell Jurney <rjurney@users.noreply.github.com> * fix(neo4j): use the Neo4j connector's actual current Maven coordinates Ran the full tutorial for real to check it: downloaded the real stats.meta.stackexchange.com dump, built Nodes/Edges.parquet, stood up a real Neo4j server (no Docker available, so the plain tarball distribution instead - same Bolt/HTTP endpoints `graphframes neo4j setup` would give you), ran `graphframes neo4j load` against it, then spark-submit'd neo4j.py for real. That surfaced two real bugs in the documented Maven coordinates, both invisible to a plain dependency resolution check because Ivy silently follows Maven relocation POMs: 1. `org.neo4j:neo4j-connector-apache-spark_2.13:6.0.0_for_spark_4` is a relocation stub - Neo4j moved the connector to `org.neo4j.connectors:spark` at 6.0.0. It still resolves and runs, but points at a coordinate Neo4j's own docs no longer document, so switched every reference to the live one: `org.neo4j.connectors:spark:6.0.0-s_2.13`. 2. `6.0.0_for_spark_3` relocates to the exact same Spark-4-only jar as `6.0.0_for_spark_4` - the message on the relocation POM says so outright ("requires Spark 4 with Scala 2.13") - so the tutorial's Spark 3.5 guidance was actually pointing Spark 3.5 users at a jar that cannot load under Spark 3.5. Confirmed the failure directly (`NoClassDefFoundError: scala/$less$colon$less`, a Scala 2.13-vs-2.12 class loaded under a Scala 2.12 Spark 3.5 runtime) and confirmed the fix: `graphframes-spark3_2.12:0.12.1` (PySpark 3.5 ships Scala 2.12, not the 2.13 the tutorial suggested) with `neo4j-connector-apache-spark_2.12:5.4.3_for_spark_3`, the last connector line that actually targets Spark 3.5. Also aligned `neo4j_cli.py`'s NEO4J_PACKAGE constant (used for `load`'s own SparkSession, built outside spark-submit) to the same live coordinate, and noted there that `load` specifically needs a Spark 4.x PySpark install regardless of which Spark version `neo4j.py` targets. Verified end to end, twice, against the real Neo4j instance with the real 129,751-node/97,104-edge graph, using the exact corrected commands: - Spark 4.1.3 (poetry's pinned dev dependency), packages `graphframes-spark4_2.13:0.12.1,org.neo4j.connectors:spark:6.0.0-s_2.13`: `graphframes neo4j load` then `spark-submit .../neo4j.py` both ran clean, producing the exact counts the tutorial documents (40,115 components, the 56,442-node/6-type giant-component breakdown, the same top-10 decorated users by badge count), and the write-back actually landed - every one of the 129,751 nodes came back out of Neo4j with a `component` property afterward. - Spark 3.5.8 in an isolated venv (PySpark's actual bundled Scala, 2.12), packages `graphframes-spark3_2.12:0.12.1,org.neo4j:neo4j-connector-apache-spark_2.12:5.4.3_for_spark_3`: same script, same real Neo4j data, identical results. Confirmed the currently-documented (pre-fix) coordinates actually fail under Spark 3.5 by running them as-is first and reproducing the NoClassDefFoundError, then confirmed the fix resolves it - not just that the new coordinate exists on Maven Central. black/isort/flake8 clean; full local suite (109 tests) still passes. Co-authored-by: Russell Jurney <rjurney@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Russell Jurney <rjurney@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Formatting churn keeps showing up inside unrelated feature PRs (most recently in #809, in a 5k+ line docs PR). The usual conclusion is "CI doesn't lint Python" — but it does:
python-ci.ymlhas runblack --check,flake8andisort --checkfor a while, and it is green on main.The problem is that the check has holes.
python/graphframespython/dev,devpython/testspython/docspython/testsis the one that hurts: it is touched by most feature PRs, and it has never been format-checked. So drift lands on main quietly and then surfaces as churn in whoever's PR happens to touch those files next.Reproduced against
mainwith the repo's own pinned tools:What this changes
tests/anddocs/in bothpython-ci.ymland.pre-commit-config.yaml, with a comment on each noting the two path lists must stay in sync. The pre-commit config had the identical gap, so local hooks did not catch it either.lintjob. It was a step inside the Spark test matrix, so it ran three times (once per matrix entry) and only after a full JDK + Spark + jar-build setup. It now runs once, independently, with no Spark, and reports its own status. black/isort/flake8 are separate steps so a red check names the tool.[tool.black] include, which was["graphframes"]— a list where black expects a regex string. A list silently disables black's default\.pyi?$filter, so black tried to parseMakefile,.rst,.cssand.jswhen handed a directory. This is whydocs/could not simply be added to the path list.graphframes/connect/proto/) from black and isort. They are emitted bybuf generate(seebuf.gen.yaml), so formatting them fights the generator — black reflows the output, the next regen emits the original, and the churn returns. doc: Pregel Tutorial #809 shows this:graphframes_pb2.pyipicked up a reformatted__slots__tuple unrelated to that PR. black needsforce-excluderather thanexclude, sinceexcludeis not applied to paths passed explicitly on the command line, and both CI and the pre-commit hook pass paths explicitly. flake8 is left as-is —tox.inialready carves outE501/F401there and its remaining checks do not rewrite the file.E265, 2xE501, 1xW391).Nothing outside
python/testsandpython/docsis reformatted.graphframes,devand../devwere already clean and are byte-identical.On black vs ruff
I looked at swapping to ruff while here and deliberately did not. The defect is coverage, not tooling — ruff would not have caught any of this either, since it would have been pointed at the same paths. For the record,
ruff formatover the code black already keeps clean is only ~36 lines across 6 files, so the migration is cheap and worth discussing, just not bundled into the PR that is supposed to stop incidental reformatting.One thing this does not fix
black --checkstructurally cannot catch the churn ingraphframes_client.pyin #809. That one is black's magic trailing comma:88and this repo's configured100pyproject.tomland falls back to its default). Black split it one-arg-per-line and added a trailing commaBoth forms therefore pass
black --checkat line-length 100, and CI is green on either. I confirmed it is not version drift: black 26.5.1 at 100 also leaves the original untouched. It is a one-way ratchet — one run under the wrong config leaves a mark the right config never undoes.black --check --skip-magic-trailing-commadoes catch it, but adopting that repo-wide is a 266-line reformat across 17 files and would flatten intentionally-exploded collections. That is a separate, opinionated call — happy to open it as a follow-up if there's appetite. Making the repo's line-length discoverable from the root (or documenting--config) would also close the hole at the source.This is intended as the scoped, standalone reformat of the previously-unlinted
directories that was asked for on #809, kept away from any feature PR.