From ca60bc0b9208ea7edbba81064ba978d870b2b3cb Mon Sep 17 00:00:00 2001 From: Rolando Bosch Date: Wed, 25 Feb 2026 11:14:10 -0800 Subject: [PATCH 1/3] infra: replace flake8 with ruff for linting (closes #466) - Add pyproject.toml with [tool.ruff] config translating all flake8 rules (E/W, F, C90) and ignore list from bin/flake8.sh - Add bin/ruff.sh runner with quick syntax + full lint passes - Update bin/lint.sh to resolve ruff (uvx > bare > python -m) - Deprecate bin/flake8.sh as wrapper delegating to ruff.sh - Replace flake8 with ruff>=0.8.0 in setup.py test extras - Clean stale # noqa comments for W503/W504/E126 (not applicable in ruff) - Update CHANGELOG.md, DEVELOP.md, and docs references Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 + DEVELOP.md | 2 +- bin/flake8.sh | 53 ++------------- bin/lint.sh | 18 +++-- bin/ruff.sh | 46 +++++++++++++ docs/source/install/extended.rst | 2 +- graphistry/PlotterBase.py | 4 +- graphistry/compute/hop.py | 12 ++-- graphistry/feature_utils.py | 12 ++-- graphistry/hyper_dask.py | 12 ++-- graphistry/layout/gib/partitioned_layout.py | 12 ++-- graphistry/plugins/igraph.py | 4 +- graphistry/pygraphistry.py | 24 +++---- graphistry/tests/test_compute_hops.py | 10 +-- graphistry/util.py | 2 +- pyproject.toml | 75 +++++++++++++++++++++ setup.py | 2 +- 17 files changed, 189 insertions(+), 104 deletions(-) create mode 100755 bin/ruff.sh create mode 100644 pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4268b37d97..acb51cd0a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Development] +### Infra +- **Linting**: Replace flake8 with ruff for linting (closes #466). Config in `pyproject.toml`, scripts in `bin/ruff.sh` / `bin/lint.sh`. Cleaned stale `# noqa` comments for W503/W504/E126 (codes not applicable in ruff). + ## [0.50.6 - 2026-01-27] ### Fixed diff --git a/DEVELOP.md b/DEVELOP.md index 1934923343..bef80349cc 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -68,7 +68,7 @@ To manually build, see `docs/`. You may need to add ignore rules: -* flake8: bin/lint.sh +* ruff: pyproject.toml (or bin/lint.sh) * mypi: mypi.ini * sphinx: docs/source/conf.py diff --git a/bin/flake8.sh b/bin/flake8.sh index bf75a8f421..f0aa2b7752 100755 --- a/bin/flake8.sh +++ b/bin/flake8.sh @@ -1,50 +1,5 @@ #!/bin/bash -set -e - -# Minimal resolution: env override or host flake8 -FLAKE8_CMD_ARR=(${FLAKE8_CMD:-flake8}) - -if ! "${FLAKE8_CMD_ARR[@]}" --version &> /dev/null; then - echo "flake8 not found. Set FLAKE8_CMD or install flake8 on PATH." - exit 1 -fi - -# Get the script directory and repo root -SCRIPT_DIR="$( cd "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Change to repo root -cd "$REPO_ROOT" - -# Check if specific files were passed as arguments -if [ $# -eq 0 ]; then - # No arguments, run on entire graphistry directory - TARGET="graphistry" -else - # Use provided arguments - TARGET="$@" -fi - -echo "Running flake8 on: $TARGET" - -# Quick syntax error check -echo "=== Running quick syntax check ===" -"${FLAKE8_CMD_ARR[@]}" \ - $TARGET \ - --count \ - --select=E9,F63,F7,F82 \ - --show-source \ - --statistics - -# Full lint check -echo "=== Running full lint check ===" -"${FLAKE8_CMD_ARR[@]}" \ - $TARGET \ - --exclude=graphistry/graph_vector_pb2.py,graphistry/_version.py \ - --count \ - --ignore=C901,E121,E122,E123,E124,E125,E128,E131,E144,E201,E202,E203,E231,E251,E265,E301,E302,E303,E401,E501,E722,F401,W291,W293,W503 \ - --max-complexity=10 \ - --max-line-length=127 \ - --statistics - -echo "Flake8 check completed successfully!" +# DEPRECATED: flake8 has been replaced by ruff (see issue #466). +# This wrapper delegates to ruff.sh for backwards compatibility. +echo "WARNING: flake8.sh is deprecated. Use ruff.sh instead." >&2 +exec "$(dirname "$0")/ruff.sh" "$@" diff --git a/bin/lint.sh b/bin/lint.sh index cc99fd04ae..febcd5faac 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,17 +4,23 @@ set -ex # Run from project root # Non-zero exit code on fail -# Resolve flake8 command, then delegate to runner (prefer uvx, then venv) +# Resolve ruff command (prefer uvx, then bare, then python -m) if command -v uvx >/dev/null 2>&1; then - FLAKE8_CMD="uvx flake8" + RUFF_CMD="uvx ruff" +elif command -v ruff >/dev/null 2>&1; then + RUFF_CMD="ruff" elif command -v python >/dev/null 2>&1; then - FLAKE8_CMD="python -m flake8" + RUFF_CMD="python -m ruff" +elif command -v python3 >/dev/null 2>&1; then + RUFF_CMD="python3 -m ruff" else - FLAKE8_CMD="flake8" + echo "ruff not found. Install ruff or set it on PATH." + exit 1 fi -FLAKE8_CMD="$FLAKE8_CMD" ./bin/flake8.sh "$@" -# Check for relative imports with '..' using flake8-quotes or custom regex +RUFF_CMD="$RUFF_CMD" ./bin/ruff.sh "$@" + +# Check for relative imports with '..' using custom regex # This will fail if any relative imports with .. are found echo "Checking for relative imports with '..' ..." if grep -r "from \.\." graphistry --include="*.py" --exclude-dir="__pycache__"; then diff --git a/bin/ruff.sh b/bin/ruff.sh new file mode 100755 index 0000000000..1378645e4a --- /dev/null +++ b/bin/ruff.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# Minimal resolution: env override or host ruff +RUFF_CMD_ARR=(${RUFF_CMD:-ruff}) + +if ! "${RUFF_CMD_ARR[@]}" version &> /dev/null; then + echo "ruff not found. Set RUFF_CMD or install ruff on PATH." + exit 1 +fi + +# Get the script directory and repo root +SCRIPT_DIR="$( cd "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Change to repo root +cd "$REPO_ROOT" + +# Check if specific files were passed as arguments +if [ $# -eq 0 ]; then + # No arguments, run on entire graphistry directory + TARGET="graphistry" +else + # Use provided arguments + TARGET="$@" +fi + +echo "Running ruff on: $TARGET" + +# Quick syntax error check (matches the old flake8 E9/F63/F7/F82 pass) +echo "=== Running quick syntax check ===" +"${RUFF_CMD_ARR[@]}" check \ + $TARGET \ + --select=E9,F63,F7,F82 \ + --output-format=full \ + --no-fix + +# Full lint check (uses config from pyproject.toml) +echo "=== Running full lint check ===" +"${RUFF_CMD_ARR[@]}" check \ + $TARGET \ + --output-format=full \ + --statistics \ + --no-fix + +echo "Ruff check completed successfully!" diff --git a/docs/source/install/extended.rst b/docs/source/install/extended.rst index 2e1d0f3523..bf5d86ba01 100644 --- a/docs/source/install/extended.rst +++ b/docs/source/install/extended.rst @@ -187,7 +187,7 @@ For contributors and developers who wish to work on PyGraphistry itself, we reco pip install graphistry[dev] -- **Includes**: Testing tools, documentation tools, and other development dependencies like `flake8`, `pytest`, `sphinx`, etc. +- **Includes**: Testing tools, documentation tools, and other development dependencies like `ruff`, `pytest`, `sphinx`, etc. References ---------- diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 6b4f6f2ac3..5f157d4695 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -2885,7 +2885,7 @@ def _table_to_arrow(self, table: Any, memoize: bool = True, validate_mode: Valid if memoize: hashed = ( hashlib.sha256(table.hash_values().to_numpy().tobytes()).hexdigest() - + hashlib.sha256(str(table.columns).encode('utf-8')).hexdigest() # noqa: W503 + + hashlib.sha256(str(table.columns).encode('utf-8')).hexdigest() ) try: if hashed in PlotterBase._cudf_hash_to_arrow: @@ -3013,7 +3013,7 @@ def _make_dataset(self, edges, nodes, name, description, mode, metadata=None, me if ('bg' in metadata) or ('fg' in metadata) or ('logo' in metadata) or ('page' in metadata): raise ValueError('Cannot set bg/fg/logo/page in api=1; try using api=3') if not (self._complex_encodings is None - or self._complex_encodings == { # noqa: W503 + or self._complex_encodings == { 'node_encodings': {'current': {}, 'default': {} }, 'edge_encodings': {'current': {}, 'default': {} }}): raise ValueError('Cannot set complex encodings ".encode_[point/edge]_[feature]()" in api=1; try using api=3 or .bind()') diff --git a/graphistry/compute/hop.py b/graphistry/compute/hop.py index 4d7292792d..401d84f85e 100644 --- a/graphistry/compute/hop.py +++ b/graphistry/compute/hop.py @@ -640,14 +640,14 @@ def resolve_label_col(requested: Optional[str], df, default_base: str) -> Option matches_edges = concat( [ matches_edges ] - + ([ hop_edges_forward[[ EDGE_ID ]] ] if hop_edges_forward is not None else mt) # noqa: W503 - + ([ hop_edges_reverse[[ EDGE_ID ]] ] if hop_edges_reverse is not None else mt), # noqa: W503 + + ([ hop_edges_forward[[ EDGE_ID ]] ] if hop_edges_forward is not None else mt) + + ([ hop_edges_reverse[[ EDGE_ID ]] ] if hop_edges_reverse is not None else mt), ignore_index=True, sort=False).drop_duplicates(subset=[EDGE_ID]) new_node_ids = concat( mt - + ( [ new_node_ids_forward ] if new_node_ids_forward is not None else mt ) # noqa: W503 - + ( [ new_node_ids_reverse] if new_node_ids_reverse is not None else mt ), # noqa: W503 + + ( [ new_node_ids_forward ] if new_node_ids_forward is not None else mt ) + + ( [ new_node_ids_reverse] if new_node_ids_reverse is not None else mt ), ignore_index=True, sort=False).drop_duplicates() if len(new_node_ids) > 0: @@ -728,10 +728,10 @@ def resolve_label_col(requested: Optional[str], df, default_base: str) -> Option else: matches_nodes = concat( mt - + ( [hop_edges_forward[[g2._source]].rename(columns={g2._source: g2._node}).drop_duplicates()] # noqa: W503 + + ( [hop_edges_forward[[g2._source]].rename(columns={g2._source: g2._node}).drop_duplicates()] if hop_edges_forward is not None else mt) - + ( [hop_edges_reverse[[g2._destination]].rename(columns={g2._destination: g2._node}).drop_duplicates()] # noqa: W503 + + ( [hop_edges_reverse[[g2._destination]].rename(columns={g2._destination: g2._node}).drop_duplicates()] if hop_edges_reverse is not None else mt), ignore_index=True, sort=False).drop_duplicates(subset=[g2._node]) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 94873f753b..7e3102f3d3 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1040,9 +1040,9 @@ def process_dirty_dataframes( y_enc, label_encoder = encode_multi_target(y, mlb=None) elif ( y is not None - and len(y.columns) > 0 # noqa: E126,W503 - and not is_dataframe_all_numeric(y) # noqa: E126,W503 - and has_skrub # noqa: E126,W503 + and len(y.columns) > 0 + and not is_dataframe_all_numeric(y) + and has_skrub ): t2 = time() logger.debug("-Fitting Targets --\n%s", y.columns) @@ -1086,9 +1086,9 @@ def process_dirty_dataframes( ) elif ( y is not None - and len(y.columns) > 0 # noqa: E126,W503 - and not is_dataframe_all_numeric(y) # noqa: E126,W503 - and not has_skrub # noqa: E126,W503 + and len(y.columns) > 0 + and not is_dataframe_all_numeric(y) + and not has_skrub ): logger.warning("-*-*- y is not numeric and no skrub, dropping non-numeric") y2 = y.select_dtypes(include=[np.number]) # type: ignore diff --git a/graphistry/hyper_dask.py b/graphistry/hyper_dask.py index 2adaafa04e..3e90f8ba43 100644 --- a/graphistry/hyper_dask.py +++ b/graphistry/hyper_dask.py @@ -451,8 +451,8 @@ def format_hyperedges( ([x for x in events.columns.tolist() if not x == defs.node_type] if not drop_edge_attrs else []) - + [defs.edge_type, defs.attrib_id, defs.event_id] # noqa: W503 - + ([defs.category] if is_using_categories else []) )) # noqa: W503 + + [defs.edge_type, defs.attrib_id, defs.event_id] + + ([defs.category] if is_using_categories else []) )) if debug and (engine in [Engine.DASK, Engine.DASK_CUDF]): #subframes = [df.persist() for df in subframes] for df in subframes: @@ -516,8 +516,8 @@ def format_direct_edges( ([x for x in events.columns.tolist() if not x == defs.node_type] if not drop_edge_attrs else []) - + [defs.edge_type, defs.source, defs.destination, defs.event_id] # noqa: W503 - + ([defs.category] if is_using_categories else []) )) # noqa: W503 + + [defs.edge_type, defs.source, defs.destination, defs.event_id] + + ([defs.category] if is_using_categories else []) )) if debug and (engine in [Engine.DASK, Engine.DASK_CUDF]): # subframes = [ df.persist() for df in subframes ] for df in subframes: @@ -543,8 +543,8 @@ def format_direct_edges( ([x for x in events.columns.tolist() if not x == defs.node_type] if not drop_edge_attrs else []) - + [defs.edge_type, defs.source, defs.destination, defs.event_id] # noqa: W503 - + ([defs.category] if is_using_categories else []) )) # noqa: W503 + + [defs.edge_type, defs.source, defs.destination, defs.event_id] + + ([defs.category] if is_using_categories else []) )) # Create empty pandas DataFrame with correct column structure, then convert to target engine # This pattern works across all engines (pandas, cudf, dask, dask_cudf) diff --git a/graphistry/layout/gib/partitioned_layout.py b/graphistry/layout/gib/partitioned_layout.py index f36deb26ea..160c7a7e63 100644 --- a/graphistry/layout/gib/partitioned_layout.py +++ b/graphistry/layout/gib/partitioned_layout.py @@ -201,11 +201,11 @@ def partitioned_layout( normalized_nodes = combined_nodes.copy() normalized_nodes['x'] = ( combined_nodes['x'] - - combined_nodes[partition_key].map(partition_stats['x_min']) # noqa: W503 + - combined_nodes[partition_key].map(partition_stats['x_min']) ) / combined_nodes[partition_key].map(partition_stats['dx']) normalized_nodes['y'] = ( combined_nodes['y'] - - combined_nodes[partition_key].map(partition_stats['y_min']) # noqa: W503 + - combined_nodes[partition_key].map(partition_stats['y_min']) ) / combined_nodes[partition_key].map(partition_stats['dy']) g_locally_positioned = self.nodes(normalized_nodes) @@ -213,16 +213,16 @@ def partitioned_layout( global_nodes['x'] = ( ( g_locally_positioned._nodes['x'] - * g_locally_positioned._nodes[partition_key].map(partition_offsets['dx']) # noqa: W503 + * g_locally_positioned._nodes[partition_key].map(partition_offsets['dx']) ) - + g_locally_positioned._nodes[partition_key].map(partition_offsets['x']) # noqa: W503 + + g_locally_positioned._nodes[partition_key].map(partition_offsets['x']) ) global_nodes['y'] = ( ( g_locally_positioned._nodes['y'] - * g_locally_positioned._nodes[partition_key].map(partition_offsets['dy']) # noqa: W503 + * g_locally_positioned._nodes[partition_key].map(partition_offsets['dy']) ) - + g_locally_positioned._nodes[partition_key].map(partition_offsets['y']) # noqa: W503 + + g_locally_positioned._nodes[partition_key].map(partition_offsets['y']) ) global_nodes['y'] = -global_nodes['y'] g_globally_positioned = g_locally_positioned.nodes(global_nodes) diff --git a/graphistry/plugins/igraph.py b/graphistry/plugins/igraph.py index fadea79437..f21df467ce 100644 --- a/graphistry/plugins/igraph.py +++ b/graphistry/plugins/igraph.py @@ -91,8 +91,8 @@ def from_igraph(self, if node_col not in nodes_df: #TODO if no g._nodes but 'name' in nodes_df, still use? if ( - ('name' in nodes_df) and # noqa: W504 - (g._nodes is not None and g._node is not None) and # noqa: W504 + ('name' in nodes_df) and + (g._nodes is not None and g._node is not None) and (g._nodes[g._node].dtype.name == nodes_df['name'].dtype.name) ): nodes_df = nodes_df.rename(columns={'name': node_col}) diff --git a/graphistry/pygraphistry.py b/graphistry/pygraphistry.py index 6a8ae4aaa9..6e8da6d60f 100644 --- a/graphistry/pygraphistry.py +++ b/graphistry/pygraphistry.py @@ -151,8 +151,8 @@ def relogin(): ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ) .login(username, password, org_name) @@ -182,8 +182,8 @@ def relogin(): ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ) .pkey_login(personal_key_id, personal_key_secret, org_name) @@ -220,8 +220,8 @@ def sso_login(self, org_name: Optional[str] = None, idp_name: Optional[str] = No arrow_uploader = ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ).sso_login(org_name, idp_name) try: @@ -354,8 +354,8 @@ def _sso_get_token(self) -> Tuple[Optional[str], Optional[str]]: arrow_uploader = ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ).sso_get_token(state) @@ -390,8 +390,8 @@ def refresh(self, token: Optional[str] = None, fail_silent: bool = False) -> Opt ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ) .refresh(self.api_token() if using_self_token else token) @@ -425,8 +425,8 @@ def verify_token(self, token: Optional[str] = None, fail_silent: bool = False) - ok = ArrowUploader( client_session=self.session, server_base_path=self.protocol() - + "://" # noqa: W503 - + self.server(), # noqa: W503 + + "://" + + self.server(), certificate_validation=self.certificate_validation(), ).verify(self.api_token() if using_self_token else token) if using_self_token: diff --git a/graphistry/tests/test_compute_hops.py b/graphistry/tests/test_compute_hops.py index 4eb323b62c..66ccaf489b 100644 --- a/graphistry/tests/test_compute_hops.py +++ b/graphistry/tests/test_compute_hops.py @@ -108,7 +108,7 @@ def test_hop_1_1_forwards(self): g = hops_graph() g2 = g.hop(pd.DataFrame({g._node: ['d']}), 1) assert g2._nodes.shape == (6, 2) - assert (g2._nodes[g2._node].sort_values().to_list() == # noqa: W504 + assert (g2._nodes[g2._node].sort_values().to_list() == sorted(['f', 'j', 'd','i', 'c', 'h'])) assert g2._edges.shape == (5, 3) @@ -148,7 +148,7 @@ def test_hop_1_1_forwards_edge(self): g = hops_graph() g2 = g.hop(pd.DataFrame({g._node: ['d']}), 1, edge_match={'d': 'f'}) assert g2._nodes.shape == (2, 2) - assert (g2._nodes[g2._node].sort_values().to_list() == # noqa: W504 + assert (g2._nodes[g2._node].sort_values().to_list() == sorted(['f', 'd'])) assert g2._edges.shape == (1, 3) @@ -156,7 +156,7 @@ def test_hop_post_match(self): g = hops_graph() g2 = g.hop(destination_node_match={'node': 'b'}) assert g2._nodes.shape == (4, 2) - assert (g2._nodes[g2._node].sort_values().to_list() == # noqa: W504 + assert (g2._nodes[g2._node].sort_values().to_list() == sorted(['b', 'l', 'o', 'p'])) assert g2._edges.shape == (3, 3) @@ -164,7 +164,7 @@ def test_hop_pre_match(self): g = hops_graph() g2 = g.hop(source_node_match={'node': 'e'}) assert g2._nodes.shape == (3, 2) - assert (g2._nodes[g2._node].sort_values().to_list() == # noqa: W504 + assert (g2._nodes[g2._node].sort_values().to_list() == sorted(['e', 'l', 'g'])) assert g2._edges.shape == (2, 3) @@ -172,7 +172,7 @@ def test_hop_pre_post_match_1(self): g = hops_graph() g2 = g.hop(source_node_match={'node': 'e'}, destination_node_match={'node': 'l'}) assert g2._nodes.shape == (2, 2) - assert (g2._nodes[g2._node].sort_values().to_list() == # noqa: W504 + assert (g2._nodes[g2._node].sort_values().to_list() == sorted(['e', 'l'])) assert g2._edges.shape == (1, 3) diff --git a/graphistry/util.py b/graphistry/util.py index 204faf348f..4502b29fe2 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -87,7 +87,7 @@ def hash_pdf(df: pd.DataFrame) -> str: hashlib.sha256( putil.hash_pandas_object(df, index=True).to_numpy().tobytes() ).hexdigest() - + hashlib.sha256(str(df.columns).encode("utf-8")).hexdigest() # noqa: W503 + + hashlib.sha256(str(df.columns).encode("utf-8")).hexdigest() ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..767f972b13 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +# Ruff linter configuration — replaces flake8 (see issue #466) +# NOTE: Build config remains in setup.py / setup.cfg (versioneer constraint). + +[tool.ruff] +target-version = "py38" # match setup.py python_requires +line-length = 127 # match previous flake8 --max-line-length + +exclude = [ + "graphistry/graph_vector_pb2.py", + "graphistry/_version.py", + "versioneer.py", +] + +[tool.ruff.lint] +# --- Rule selection -------------------------------------------------------- +# Enable the same rule families flake8 checked: +# E/W = pycodestyle F = pyflakes C90 = mccabe +select = ["E", "W", "F", "C90"] + +# --- Ignored rules --------------------------------------------------------- +# Carried over from the old flake8 --ignore list in bin/flake8.sh. +# +# Codes NOT ported (no ruff equivalents): +# E121, E122, E123, E124, E125, E128, E131 — indentation/continuation +# rules that ruff does not implement (ruff delegates formatting to +# ruff-format or black). +# E144 — non-standard flake8 code, no ruff equivalent. +# W503 — line break before binary operator; PEP 8 reversed its stance +# and ruff does not enforce this. +ignore = [ + # --- mccabe --------------------------- + "C901", # function too complex + + # --- whitespace (preview) ------------- + "E201", # whitespace after '(' + "E202", # whitespace before ')' + "E203", # whitespace before ':' / ',' / ';' + "E231", # missing whitespace after ',' / ';' / ':' + "E251", # unexpected spaces around keyword / parameter default + + # --- comments (preview) --------------- + "E265", # block comment should start with '# ' + + # --- blank lines (preview) ------------ + "E301", # expected 1 blank line before a nested definition + "E302", # expected 2 blank lines before a function / class definition + "E303", # too many blank lines + + # --- imports -------------------------- + "E401", # multiple imports on one line + "E402", # module import not at top of file (conditional imports) + "F401", # module imported but unused + + # --- line length ---------------------- + "E501", # line too long (handled by line-length setting as backstop) + + # --- comparison style ----------------- + # Ruff finds these but flake8 did not flag them; ignore for parity. + # Consider enabling in a follow-up (95 auto-fixable occurrences). + "E713", # test for membership should be 'not in x' + "E714", # test for object identity should be 'is not' + + # --- naming --------------------------- + "E741", # ambiguous variable name (e.g. 'l') + + # --- bare except ---------------------- + "E722", # do not use bare 'except' + + # --- trailing whitespace -------------- + "W291", # trailing whitespace + "W293", # whitespace before a comment +] + +[tool.ruff.lint.mccabe] +max-complexity = 10 # match previous flake8 --max-complexity diff --git a/setup.py b/setup.py index e8d4a74089..ee7579da69 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): 'sphinx-copybutton==0.5.2', 'sphinx-book-theme==1.1.3', ], - 'test': ['flake8>=5.0', 'hypothesis', 'mock', 'mypy', 'pytest', 'pytest-xdist'] + stubs + test_workarounds, + 'test': ['ruff>=0.8.0', 'hypothesis', 'mock', 'mypy', 'pytest', 'pytest-xdist'] + stubs + test_workarounds, 'testai': [ 'numba>=0.57.1' # https://github.com/numba/numba/issues/8615 ], From bf33d4bc2bbef8ea4b39d07a44af8853b7457079 Mon Sep 17 00:00:00 2001 From: Rolando Bosch Date: Wed, 25 Feb 2026 15:06:05 -0800 Subject: [PATCH 2/3] address review: remove remaining flake8 references from docs and scripts - ai/README.md: update WITH_LINT description from flake8 to ruff - ai/prompts/LINT_TYPES_CHECK.md: replace all flake8 references with ruff - bin/ruff.sh: remove stale flake8 comment - bin/flake8.sh: delete deprecated wrapper script Co-Authored-By: Claude Opus 4.6 --- ai/README.md | 2 +- ai/prompts/LINT_TYPES_CHECK.md | 34 +++++++++++++++++----------------- bin/flake8.sh | 5 ----- bin/ruff.sh | 2 +- 4 files changed, 19 insertions(+), 24 deletions(-) delete mode 100755 bin/flake8.sh diff --git a/ai/README.md b/ai/README.md index a4ed7403f6..8aa8fb3beb 100644 --- a/ai/README.md +++ b/ai/README.md @@ -202,7 +202,7 @@ docker run --rm --gpus all \ ### Environment Control | Variable | Default | Purpose | |----------|---------|---------| -| `WITH_LINT` | 1 | Run flake8 linting | +| `WITH_LINT` | 1 | Run ruff linting | | `WITH_TYPECHECK` | 1 | Run mypy type checking | | `WITH_BUILD` | 0 | Build documentation | | `WITH_NEO4J` | 0 | Run Neo4j integration tests | diff --git a/ai/prompts/LINT_TYPES_CHECK.md b/ai/prompts/LINT_TYPES_CHECK.md index 08df8a222d..3bacc6f3fb 100644 --- a/ai/prompts/LINT_TYPES_CHECK.md +++ b/ai/prompts/LINT_TYPES_CHECK.md @@ -10,7 +10,7 @@ Status: [IN_PROGRESS/COMPLETE/BLOCKED] ## Instructions for AI Assistant -This template guides systematic code quality checks using flake8 (linting) and mypy (type checking) for PyGraphistry. +This template guides systematic code quality checks using ruff (linting) and mypy (type checking) for PyGraphistry. ### Quick Start - Docker Commands ```bash @@ -54,7 +54,7 @@ cd docker && WITH_TYPECHECK=0 WITH_BUILD=0 WITH_TEST=0 ./test-cpu-local.sh The lint/type check process is **iterative by design**: - **Steps 1-2**: Identify issues AND FIX THEM IMMEDIATELY - **Step 3**: Verify and decide to iterate or finish -- **Repeat**: Continue until clean (0 flake8, 0 mypy) +- **Repeat**: Continue until clean (0 ruff, 0 mypy) - **Step 4**: Generate final report and cleanup - **Exit**: When clean OR blocked AFTER ATTEMPTING FIXES @@ -75,7 +75,7 @@ The lint/type check process is **iterative by design**: ## Execution Protocol -### Step 1: Check Lint Issues with flake8 +### Step 1: Check Lint Issues with ruff **Started**: [YYYY-MM-DD HH:MM:SS] **Command (containerized)**: `cd docker && WITH_BUILD=0 WITH_TEST=0 ./test-cpu-local.sh` **Command (direct)**: `./bin/lint.sh` (requires local environment) @@ -178,11 +178,11 @@ cd docker && WITH_BUILD=0 WITH_TEST=0 ./test-cpu-local.sh ``` **Results**: -- **Flake8 issues remaining**: [count] +- **Ruff issues remaining**: [count] - **MyPy issues remaining**: [count] **Decision**: -- [ ] ✅ **CLEAN** - 0 flake8 issues AND 0 mypy issues (excluding P4/P5) → Go to Step 4 +- [ ] ✅ **CLEAN** - 0 ruff issues AND 0 mypy issues (excluding P4/P5) → Go to Step 4 - [ ] 🔄 **ITERATE** - P0-P3 issues remain → REPEAT Steps 1-3 - [ ] 🛑 **BLOCKED** - Cannot fix remaining P0-P3 issues → Document blockers → Go to Step 4 @@ -198,7 +198,7 @@ Examples of valid blockers: - "Circular import that breaks when fixed" - "Third-party library missing type stubs configured in mypy.ini" - "Type system limitation requiring major refactor" -- "Flake8 rule conflicts with project style guide" +- "Ruff rule conflicts with project style guide" NOT valid reasons for BLOCKED: - "Too many errors" @@ -222,7 +222,7 @@ Issues Fixed: [count] ([percentage]%) Time Elapsed: [duration] Summary by Tool: -- Flake8 issues fixed: [count] +- Ruff issues fixed: [count] - MyPy issues fixed: [count] - Remaining issues: [count] @@ -250,20 +250,20 @@ Result: ✅ CLEAN / 🔧 IMPROVED / 🛑 BLOCKED - **Current Status**: [count remaining] **Per-Iteration Progress**: -| Iteration | Started | Flake8 Fixed | Flake8 Remaining | MyPy Fixed | MyPy Remaining | P4/P5 | Status | +| Iteration | Started | Ruff Fixed | Ruff Remaining | MyPy Fixed | MyPy Remaining | P4/P5 | Status | |-----------|---------|--------------|------------------|------------|----------------|-------|--------| | 1 | [time] | [count] | [count] | [count] | [count] | [count]| 🔄 | | 2 | [time] | [count] | [count] | [count] | [count] | [count]| ✅ | **Total Issues Fixed**: -- Flake8 issues fixed: [count] +- Ruff issues fixed: [count] - MyPy errors fixed: [count] - **P0-P3 remaining**: [count] - **P4/P5 ignored**: [count] ## PyGraphistry-Specific Patterns -### Common Flake8 Fixes +### Common Ruff Fixes ```python # E501: Line too long (max 127) # Break long lines @@ -311,7 +311,7 @@ df['new_col'] = values # Avoid ### Configuration Reference -**Flake8 Ignored Rules (from bin/lint.sh)**: +**Ruff Ignored Rules (from pyproject.toml [tool.ruff.lint])**: - C901: Function complexity - E121-E128: Indentation rules - E201-E203: Whitespace around brackets @@ -328,7 +328,7 @@ df['new_col'] = values # Avoid **P4 - Nice to Have (low impact)**: - Minor style inconsistencies that don't affect readability -- Flake8 rules explicitly ignored in bin/lint.sh (E121-E128, etc.) +- Ruff rules explicitly ignored in pyproject.toml (E121-E128, etc.) - MyPy errors in excluded files (tests/, _version.py) - Import errors for packages with `ignore_missing_imports = True` - Optional type annotations that would add minimal safety benefit @@ -387,7 +387,7 @@ If you have the dependencies installed locally: ./bin/mypy.sh # For specific files -flake8 graphistry/embed_utils.py --max-line-length=127 +ruff check graphistry/embed_utils.py mypy graphistry/embed_utils.py ``` @@ -440,17 +440,17 @@ Started: [YYYY-MM-DD HH:MM:SS] Fixes Applied: 1. File: [path] - - Issue: [flake8/mypy code] - [description] + - Issue: [ruff/mypy code] - [description] - Fix: [what was changed] - Status: ✅ Fixed 2. File: [path] - - Issue: [flake8/mypy code] - [description] + - Issue: [ruff/mypy code] - [description] - Fix: [what was changed] - Status: ❌ Failed - [reason] Verification: -- Flake8: [count] remaining +- Ruff: [count] remaining - MyPy: [count] remaining - Next: [ITERATE/COMPLETE/BLOCKED] ``` @@ -467,7 +467,7 @@ Issues Fixed: 23 (100%) Time Elapsed: 5 minutes 15 seconds Summary by Tool: -- Flake8 issues fixed: 21 +- Ruff issues fixed: 21 - MyPy issues fixed: 2 - Remaining issues: 0 diff --git a/bin/flake8.sh b/bin/flake8.sh deleted file mode 100755 index f0aa2b7752..0000000000 --- a/bin/flake8.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# DEPRECATED: flake8 has been replaced by ruff (see issue #466). -# This wrapper delegates to ruff.sh for backwards compatibility. -echo "WARNING: flake8.sh is deprecated. Use ruff.sh instead." >&2 -exec "$(dirname "$0")/ruff.sh" "$@" diff --git a/bin/ruff.sh b/bin/ruff.sh index 1378645e4a..125f4fd472 100755 --- a/bin/ruff.sh +++ b/bin/ruff.sh @@ -27,7 +27,7 @@ fi echo "Running ruff on: $TARGET" -# Quick syntax error check (matches the old flake8 E9/F63/F7/F82 pass) +# Quick syntax error check (E9/F63/F7/F82) echo "=== Running quick syntax check ===" "${RUFF_CMD_ARR[@]}" check \ $TARGET \ From f918ff98f7bfb820801c512b92f0e8e503f33252 Mon Sep 17 00:00:00 2001 From: Rolando Bosch Date: Thu, 26 Feb 2026 00:32:36 -0800 Subject: [PATCH 3/3] address review: remove stale bin/flake8.sh reference from pyproject.toml comment Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 767f972b13..0004e986fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ exclude = [ select = ["E", "W", "F", "C90"] # --- Ignored rules --------------------------------------------------------- -# Carried over from the old flake8 --ignore list in bin/flake8.sh. +# Carried over from the old flake8 --ignore list. # # Codes NOT ported (no ruff equivalents): # E121, E122, E123, E124, E125, E128, E131 — indentation/continuation