Skip to content

ci: run python lint on tests/ and docs/, as its own job - #896

Merged
james-willis merged 2 commits into
graphframes:mainfrom
james-willis:jw/python-lint-ci
Aug 31, 2026
Merged

james-willis merged 2 commits into
graphframes:mainfrom
james-willis:jw/python-lint-ci

Conversation

@james-willis

@james-willis james-willis commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

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.yml has run black --check, flake8 and isort --check for a while, and it is green on main.

The problem is that the check has holes.

Path .py files Linted before Linted after
python/graphframes 23 yes yes
python/dev, dev 5 yes yes
python/tests 5 no yes
python/docs 3 no yes

python/tests is 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 main with the repo's own pinned tools:

# old CI paths
black --check --config pyproject.toml graphframes dev ../dev
  -> All done! 31 files would be left unchanged.        # passes

# same tree, new CI paths
black --check --config pyproject.toml graphframes tests docs dev ../dev
  -> 6 files would be reformatted.                      # catches it

What this changes

  • Cover tests/ and docs/ in both python-ci.yml and .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.
  • Move the style check into its own lint job. 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.
  • Fix [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 parse Makefile, .rst, .css and .js when handed a directory. This is why docs/ could not simply be added to the path list.
  • Exclude the generated protobuf stubs (graphframes/connect/proto/) from black and isort. They are emitted by buf generate (see buf.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.pyi picked up a reformatted __slots__ tuple unrelated to that PR. black needs force-exclude rather than exclude, since 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 as-is — tox.ini already carves out E501/F401 there and its remaining checks do not rewrite the file.
  • Apply black + isort to the newly covered files and fix the 6 remaining flake8 findings (3x E265, 2x E501, 1x W391).

Nothing outside python/tests and python/docs is reformatted. graphframes, dev and ../dev were 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 format over 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 --check structurally cannot catch the churn in graphframes_client.py in #809. That one is black's magic trailing comma:

  • the original signature line is 90 chars — between black's default 88 and this repo's configured 100
  • something formatted it at 88 (an editor format-on-save typically formats a temp buffer, where black finds no pyproject.toml and falls back to its default). Black split it one-arg-per-line and added a trailing comma
  • black treats that comma as an explicit "keep this exploded" marker, so a correct run at 100 preserves it rather than collapsing it back

Both forms therefore pass black --check at 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-comma does 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.

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.
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
james-willis marked this pull request as ready for review August 31, 2026 18:22

@SemyonSinchenko SemyonSinchenko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~LGTM

@james-willis
james-willis merged commit 3a6b761 into graphframes:main Aug 31, 2026
12 checks passed
@james-willis
james-willis deleted the jw/python-lint-ci branch August 31, 2026 18:52
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants