From 4dea9639f82741335027636133d80a61abf7b56a Mon Sep 17 00:00:00 2001 From: WillKoehrsen Date: Wed, 29 Jul 2026 21:51:11 -0500 Subject: [PATCH 1/2] Adopt ruff 0.16's default ruleset ruff 0.16 replaced its narrow legacy default with a broad curated default set, so this drops the hand-maintained select in favour of extend-select. The project now inherits the default rules and only names the families it needs on top: pyflakes and pycodestyle in full, quotes, and import sorting. Three families are ignored with reasons (declarative class attributes, nested with blocks, and vanilla exception classes). The rest are fixed. Two of the flagged datetime calls are deliberately local rather than UTC and now say so. get_daily_peak_report documents its default market_date as the caller's current date, and process_date treats a literal "today" the same way; only the calendar date survives the strftime in both, so switching to UTC would shift the default market day for callers west of UTC. The pytest.raises(Exception) assertions match what the client actually raises, which is a bare Exception for API errors. Narrowing them means introducing a custom exception hierarchy, which is a public API change and is deferred with TRY002. The rest is mechanical: dict() and set() literals, merged isinstance calls, unnecessary range starts, and dropped unused noqa directives. Note that ruff 0.16's formatter also reformats Python inside Markdown fences. Neither `make lint` nor the pre-commit hook asks it to, so the README and CHANGELOG examples are deliberately left alone. Verified: ruff check and format are clean over the package and the whole repo, pyright reports no errors, the package imports, and all 153 tests collect. The suite itself calls the live API, so it needs a key to run and was not executed. --- .pre-commit-config.yaml | 5 ++- Examples/4. CAISO April Net Load.ipynb | 24 +++++----- .../5. Stacked Net Load Visualization.ipynb | 22 +++++----- Examples/6. Resampling Data.ipynb | 4 +- gridstatusio/gs_client.py | 7 ++- gridstatusio/tests/test_api.py | 32 ++++++++------ gridstatusio/tests/test_compression.py | 6 +-- gridstatusio/tests/test_retries.py | 4 +- gridstatusio/utils.py | 4 +- gridstatusio/version.py | 4 +- pyproject.toml | 25 ++++++++--- uv.lock | 44 +++++++++---------- 12 files changed, 104 insertions(+), 77 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c8ce03..33487df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,8 +21,9 @@ repos: rev: v0.10.1 hooks: - id: validate-pyproject - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.5.0 + # Keep this rev in sync with the ruff pin in pyproject.toml. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.0 hooks: - id: ruff types_or: [ python, jupyter ] diff --git a/Examples/4. CAISO April Net Load.ipynb b/Examples/4. CAISO April Net Load.ipynb index 01860da..4979ddd 100644 --- a/Examples/4. CAISO April Net Load.ipynb +++ b/Examples/4. CAISO April Net Load.ipynb @@ -5381,7 +5381,7 @@ " y=year_data[\"net_load\"] / 1000,\n", " mode=\"lines\",\n", " name=str(year),\n", - " line=dict(color=color, width=3),\n", + " line={\"color\": color, \"width\": 3},\n", " opacity=min(0.6 + (year - 2017) * 0.075, 1),\n", " )\n", " )\n", @@ -5393,7 +5393,7 @@ " y=min_net_load_day_2023[\"net_load\"] / 1000,\n", " mode=\"lines\",\n", " name=\"2023 Minimum Day\",\n", - " line=dict(color=\"#4a69bb\", width=4, dash=\"dash\"),\n", + " line={\"color\": \"#4a69bb\", \"width\": 4, \"dash\": \"dash\"},\n", " opacity=1,\n", " )\n", ")\n", @@ -5404,13 +5404,17 @@ "\n", "# Update layout with title, subtitle, and custom axis tick labels\n", "fig.update_layout(\n", - " title=\"Average Net Load in April in California
Net Load regularly below 0 for first time in April 2023 \", # noqa\n", - " xaxis=dict(\n", - " tickmode=\"array\",\n", - " tickvals=x_tick_values,\n", - " ticktext=[\"12am\", \"6am\", \"12pm\", \"6pm\", \"12am\"],\n", - " ),\n", - " yaxis=dict(tickmode=\"array\", tickvals=y_tick_values, ticktext=[0, 10, 20, \"25 GW\"]),\n", + " title=\"Average Net Load in April in California
Net Load regularly below 0 for first time in April 2023 \",\n", + " xaxis={\n", + " \"tickmode\": \"array\",\n", + " \"tickvals\": x_tick_values,\n", + " \"ticktext\": [\"12am\", \"6am\", \"12pm\", \"6pm\", \"12am\"],\n", + " },\n", + " yaxis={\n", + " \"tickmode\": \"array\",\n", + " \"tickvals\": y_tick_values,\n", + " \"ticktext\": [0, 10, 20, \"25 GW\"],\n", + " },\n", " template=\"plotly_dark\",\n", ")\n", "\n", @@ -5438,7 +5442,7 @@ " text=annotation[\"text\"],\n", " showarrow=False,\n", " yshift=annotation[\"y_shift\"],\n", - " font=dict(color=\"white\", size=18),\n", + " font={\"color\": \"white\", \"size\": 18},\n", " )\n", "\n", "# Diplay the figure as an interactive plot\n", diff --git a/Examples/5. Stacked Net Load Visualization.ipynb b/Examples/5. Stacked Net Load Visualization.ipynb index e79d2bc..344b83b 100644 --- a/Examples/5. Stacked Net Load Visualization.ipynb +++ b/Examples/5. Stacked Net Load Visualization.ipynb @@ -528,7 +528,7 @@ " name=\"Net Load\",\n", " stackgroup=\"one\",\n", " fill=\"none\",\n", - " line=dict(color=gray, width=5),\n", + " line={\"color\": gray, \"width\": 5},\n", " )\n", ")\n", "\n", @@ -562,7 +562,7 @@ " y=df[\"load.load\"],\n", " name=\"Load\",\n", " mode=\"lines\",\n", - " line=dict(color=red, width=5),\n", + " line={\"color\": red, \"width\": 5},\n", " )\n", ")\n", "\n", @@ -571,15 +571,15 @@ " template=\"plotly_dark\",\n", " title=f\"Net Load Visualization - {ISO.upper()} - {START} to {END} \",\n", " # bold title\n", - " title_font=dict(size=20),\n", - " font=dict(size=16),\n", - " legend=dict(\n", - " orientation=\"h\",\n", - " yanchor=\"bottom\",\n", - " y=1.02,\n", - " xanchor=\"right\",\n", - " x=1,\n", - " ),\n", + " title_font={\"size\": 20},\n", + " font={\"size\": 16},\n", + " legend={\n", + " \"orientation\": \"h\",\n", + " \"yanchor\": \"bottom\",\n", + " \"y\": 1.02,\n", + " \"xanchor\": \"right\",\n", + " \"x\": 1,\n", + " },\n", ")\n", "\n", "fig.show(\"png\", width=2000, height=800)" diff --git a/Examples/6. Resampling Data.ipynb b/Examples/6. Resampling Data.ipynb index a3c3213..a46f787 100644 --- a/Examples/6. Resampling Data.ipynb +++ b/Examples/6. Resampling Data.ipynb @@ -1033,7 +1033,7 @@ " y=df_hourly_max[\"load\"],\n", " name=\"Hourly Max\",\n", " mode=\"lines\",\n", - " line=dict(color=\"rgb(0,100,80)\"),\n", + " line={\"color\": \"rgb(0,100,80)\"},\n", " )\n", ")\n", "\n", @@ -1043,7 +1043,7 @@ " y=df_hourly_min[\"load\"],\n", " name=\"Hourly Min\",\n", " mode=\"lines\",\n", - " line=dict(color=\"rgb(0,100,80)\"),\n", + " line={\"color\": \"rgb(0,100,80)\"},\n", " fill=\"tonexty\",\n", " fillcolor=\"rgba(0,100,80,0.2)\",\n", " )\n", diff --git a/gridstatusio/gs_client.py b/gridstatusio/gs_client.py index ff51cd3..9468e95 100644 --- a/gridstatusio/gs_client.py +++ b/gridstatusio/gs_client.py @@ -475,7 +475,7 @@ def _retry_delay_and_log(reason: str): except RETRIABLE_EXCEPTIONS as e: if retries >= self.max_retries: raise Exception( - f"Network error: {str(e)}. Exceeded maximum number of retries", + f"Network error: {e!s}. Exceeded maximum number of retries", ) _retry_delay_and_log(f"Network error ({type(e).__name__})") retries += 1 @@ -1009,7 +1009,10 @@ def get_daily_peak_report( dict: The daily peak report as a dict. """ if market_date is None: - market_date = datetime.today() + # Local, not UTC: the documented default is the caller's current date, + # and only the calendar date survives the strftime below. Using UTC + # would shift the default market day for callers west of UTC. + market_date = datetime.today() # noqa: DTZ002 if isinstance(market_date, datetime): market_date = market_date.strftime("%Y-%m-%d") diff --git a/gridstatusio/tests/test_api.py b/gridstatusio/tests/test_api.py index 5f9e757..149a50e 100644 --- a/gridstatusio/tests/test_api.py +++ b/gridstatusio/tests/test_api.py @@ -107,9 +107,7 @@ def get_unique_values(data: Any, column: str) -> list: def get_min(data: Any, column: str) -> Any: """Get min value for a column, regardless of format.""" - if isinstance(data, pd.DataFrame): - return data[column].min() - elif isinstance(data, pl.DataFrame): + if isinstance(data, (pd.DataFrame, pl.DataFrame)): return data[column].min() elif isinstance(data, list): values = [v for v in get_column_values(data, column) if v is not None] @@ -120,9 +118,7 @@ def get_min(data: Any, column: str) -> Any: def get_max(data: Any, column: str) -> Any: """Get max value for a column, regardless of format.""" - if isinstance(data, pd.DataFrame): - return data[column].max() - elif isinstance(data, pl.DataFrame): + if isinstance(data, (pd.DataFrame, pl.DataFrame)): return data[column].max() elif isinstance(data, list): values = [v for v in get_column_values(data, column) if v is not None] @@ -312,9 +308,7 @@ def check_data( def data_equals(data1: Any, data2: Any, return_format: str) -> bool: """Check if two datasets are equal.""" - if return_format == ReturnFormat.PANDAS: - return data1.equals(data2) - elif return_format == ReturnFormat.POLARS: + if return_format == ReturnFormat.PANDAS or return_format == ReturnFormat.POLARS: return data1.equals(data2) elif return_format == ReturnFormat.PYTHON: return data1 == data2 @@ -441,7 +435,9 @@ def test_get_dataset_metadata_csv_request_format(): def test_get_dataset_metadata_invalid_dataset(pandas_client): """Test that an invalid dataset id raises an error.""" - with pytest.raises(Exception): + # The client raises bare Exception for API errors; narrowing this needs a custom + # exception hierarchy in the library (see the TRY002 note in pyproject.toml). + with pytest.raises(Exception): # noqa: B017 pandas_client.get_dataset_metadata("not_a_real_dataset") @@ -917,14 +913,18 @@ def test_pagination(client, return_format): assert get_length(data) == 25 # Test too large page size errors - with pytest.raises(Exception): + # The client raises bare Exception for API errors; narrowing this needs a custom + # exception hierarchy in the library (see the TRY002 note in pyproject.toml). + with pytest.raises(Exception): # noqa: B017 client.get_dataset(dataset=dataset, page_size=10**10) # Query yesterday rather than today so the daily peak report always has settled # data. Requesting the current date fails when the market day has not produced any # intervals yet (the API returns "max() iterable argument is empty"). -yesterday = datetime.now() - timedelta(days=1) +# Local, not UTC: this feeds market_date, which the client reduces to a calendar +# date, and the market day tracks the local date rather than a UTC instant. +yesterday = datetime.now() - timedelta(days=1) # noqa: DTZ005 @pytest.mark.parametrize( @@ -932,7 +932,9 @@ def test_pagination(client, return_format): [ ("ERCOT", yesterday, yesterday.strftime("%Y-%m-%d")), ("CAISO", "2024-07-01", "2024-07-01"), - ("spp", datetime(2024, 7, 10), "2024-07-10"), + # Naive by design: market_date is a calendar date, so a tzinfo would be + # discarded by the client's strftime anyway. + ("spp", datetime(2024, 7, 10), "2024-07-10"), # noqa: DTZ001 ], ) def test_reports_api(client, return_format, iso, market_date, expected_date): @@ -945,7 +947,9 @@ def test_reports_api(client, return_format, iso, market_date, expected_date): def test_invalid_resampling_frequency(client, return_format): """Test that invalid resampling frequency raises error.""" - with pytest.raises(Exception): + # The client raises bare Exception for API errors; narrowing this needs a custom + # exception hierarchy in the library (see the TRY002 note in pyproject.toml). + with pytest.raises(Exception): # noqa: B017 client.get_dataset( "pjm_load", resample="1 hour market", diff --git a/gridstatusio/tests/test_compression.py b/gridstatusio/tests/test_compression.py index d8f017d..84442f6 100644 --- a/gridstatusio/tests/test_compression.py +++ b/gridstatusio/tests/test_compression.py @@ -24,9 +24,9 @@ def test_brotli_decoder_is_installed() -> None: importlib.util.find_spec("brotli") is not None or importlib.util.find_spec("brotlicffi") is not None ) - assert ( - has_brotli_decoder - ), "a brotli decoder (brotli or brotlicffi) must be installed" + assert has_brotli_decoder, ( + "a brotli decoder (brotli or brotlicffi) must be installed" + ) def test_default_accept_encoding_advertises_brotli() -> None: diff --git a/gridstatusio/tests/test_retries.py b/gridstatusio/tests/test_retries.py index 80bdf7e..b577e39 100644 --- a/gridstatusio/tests/test_retries.py +++ b/gridstatusio/tests/test_retries.py @@ -33,7 +33,7 @@ def test_rate_limit_hit_backoff(mock_get_request, caplog): ) log_messages = [record.message for record in caplog.records] - for i in range(0, client.max_retries): + for i in range(client.max_retries): expected_text = ( f"Too Many Requests. Limit: 6 per 1 second. " f"Retrying in {1 * 2**i} seconds. " @@ -58,7 +58,7 @@ def test_connection_error_backoff(mock_get_request, caplog): ) log_messages = [record.message for record in caplog.records] - for i in range(0, client.max_retries): + for i in range(client.max_retries): expected_text = ( f"Network error (ConnectionError). " f"Retrying in {1 * 2**i} seconds. " diff --git a/gridstatusio/utils.py b/gridstatusio/utils.py index 52c20fe..53d678b 100644 --- a/gridstatusio/utils.py +++ b/gridstatusio/utils.py @@ -50,7 +50,9 @@ def handle_date( # For non-pandas mode, just validate and return the string/datetime if not use_pandas: if date == "today": - return datetime.now().strftime("%Y-%m-%d") + # Local, not UTC: the caller asked for "today", which means their + # calendar date, and only the date survives the strftime. + return datetime.now().strftime("%Y-%m-%d") # noqa: DTZ005 if isinstance(date, datetime): return date.isoformat() # Assume string is already in valid format diff --git a/gridstatusio/version.py b/gridstatusio/version.py index b165f8f..06518b1 100644 --- a/gridstatusio/version.py +++ b/gridstatusio/version.py @@ -9,7 +9,7 @@ def get_latest_version() -> str: """Get the latest version of gridstatusio from PyPI""" - response = requests.get("https://pypi.org/pypi/gridstatusio/json") # noqa: E501 + response = requests.get("https://pypi.org/pypi/gridstatusio/json") latest_version = response.json()["info"]["version"] return latest_version @@ -49,7 +49,7 @@ def check_for_update() -> None: ) print( colored( - "\nSee the changelog here: https://github.com/gridstatus/gridstatusio/blob/main/CHANGELOG.md", # noqa: E501 + "\nSee the changelog here: https://github.com/gridstatus/gridstatusio/blob/main/CHANGELOG.md", "red", ), ) diff --git a/pyproject.toml b/pyproject.toml index 9960d11..bd0f086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ dev-dependencies = [ "pre-commit>=4.5.1", "kaleido==0.2.1", "nbformat>=5.10.4", - "ruff>=0.5.0,<0.6", + "ruff ~=0.16", "pyright>=1.1.407,<2", # Include optional dependencies for testing "polars>=1.36.1", @@ -110,17 +110,30 @@ markers = [ [tool.ruff] line-length = 88 -lint.ignore = [] -lint.select = [ - # Pyflakes +# ruff >=0.16 ships a broad curated default rule set +# (https://docs.astral.sh/ruff/default-rules/). We inherit it and use +# extend-select for the families enforced here that are not part of it, rather +# than pinning our own select and missing future default additions. +lint.extend-select = [ + # Pyflakes in full (the default set carries only a subset) "F", - # Pycodestyle + # Pycodestyle in full (the default set carries only E722/E902) "E", "W", # flake8-quotes "Q", # isort - "I001" + "I001", +] +lint.ignore = [ + # Class-level mutable defaults here are declarative config, not the shared-state + # bug this targets. + "RUF012", + # Nested `with` blocks read better than one merged statement for these. + "SIM117", + # Introducing a custom exception hierarchy is a separate change from adopting + # the ruleset. + "TRY002", ] src = ["gridstatusio"] diff --git a/uv.lock b/uv.lock index 1616aab..bc3b9a5 100644 --- a/uv.lock +++ b/uv.lock @@ -636,7 +636,7 @@ dev = [ { name = "pytest", specifier = ">=9.1.0" }, { name = "pytest-rerunfailures", specifier = ">=16.3" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.5.0,<0.6" }, + { name = "ruff", specifier = "~=0.16" }, ] [[package]] @@ -2020,27 +2020,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/69e5e412f9d390adbdbcbf4f64d6914fa61b44b08839a6584655014fc524/ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5", size = 2449817, upload-time = "2024-08-08T15:43:07.467Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/eb/06e06aaf96af30a68e83b357b037008c54a2ddcbad4f989535007c700394/ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a", size = 9570571, upload-time = "2024-08-08T15:41:56.537Z" }, - { url = "https://files.pythonhosted.org/packages/a4/10/1be32aeaab8728f78f673e7a47dd813222364479b2d6573dbcf0085e83ea/ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be", size = 8685138, upload-time = "2024-08-08T15:42:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/c218ce83beb4394ba04d05e9aa2ae6ce9fba8405688fe878b0fdb40ce855/ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e", size = 8266785, upload-time = "2024-08-08T15:42:08.321Z" }, - { url = "https://files.pythonhosted.org/packages/26/79/7f49509bd844476235b40425756def366b227a9714191c91f02fb2178635/ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8", size = 9983964, upload-time = "2024-08-08T15:42:12.419Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b1/939836b70bf9fcd5e5cd3ea67fdb8abb9eac7631351d32f26544034a35e4/ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea", size = 9359490, upload-time = "2024-08-08T15:42:16.713Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/b3db19207de105daad0c8b704b2c6f2a011f9c07017bd58d8d6e7b8eba19/ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc", size = 10170833, upload-time = "2024-08-08T15:42:20.54Z" }, - { url = "https://files.pythonhosted.org/packages/a2/45/eae9da55f3357a1ac04220230b8b07800bf516e6dd7e1ad20a2ff3b03b1b/ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692", size = 10896360, upload-time = "2024-08-08T15:42:25.2Z" }, - { url = "https://files.pythonhosted.org/packages/99/67/4388b36d145675f4c51ebec561fcd4298a0e2550c81e629116f83ce45a39/ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf", size = 10477094, upload-time = "2024-08-08T15:42:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f5e6ed1751dc187a4ecf19a4970dd30a521c0ee66b7941c16e292a4043fb/ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb", size = 11480896, upload-time = "2024-08-08T15:42:33.772Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3b/2b683be597bbd02046678fc3fc1c199c641512b20212073b58f173822bb3/ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e", size = 10179702, upload-time = "2024-08-08T15:42:38.038Z" }, - { url = "https://files.pythonhosted.org/packages/f1/38/c2d94054dc4b3d1ea4c2ba3439b2a7095f08d1c8184bc41e6abe2a688be7/ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499", size = 9982855, upload-time = "2024-08-08T15:42:42.031Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e7/1433db2da505ffa8912dcf5b28a8743012ee780cbc20ad0bf114787385d9/ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e", size = 9433156, upload-time = "2024-08-08T15:42:45.339Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/4fa43250e67741edeea3d366f59a1dc993d4d89ad493a36cbaa9889895f2/ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5", size = 9782971, upload-time = "2024-08-08T15:42:49.354Z" }, - { url = "https://files.pythonhosted.org/packages/80/0e/8c276103d518e5cf9202f70630aaa494abf6fc71c04d87c08b6d3cd07a4b/ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e", size = 10247775, upload-time = "2024-08-08T15:42:53.294Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b9/673096d61276f39291b729dddde23c831a5833d98048349835782688a0ec/ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a", size = 7841772, upload-time = "2024-08-08T15:42:57.488Z" }, - { url = "https://files.pythonhosted.org/packages/67/1c/4520c98bfc06b9c73cd1457686d4d3935d40046b1ddea08403e5a6deff51/ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3", size = 8699779, upload-time = "2024-08-08T15:43:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/38/23/b3763a237d2523d40a31fe2d1a301191fe392dd48d3014977d079cf8c0bd/ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4", size = 8091891, upload-time = "2024-08-08T15:43:04.162Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] From bde0f9bc5db4f8b60c6b9cf92af64f3599c5746e Mon Sep 17 00:00:00 2001 From: WillKoehrsen Date: Wed, 29 Jul 2026 22:51:34 -0500 Subject: [PATCH 2/2] Pin ruff exactly so the hook and CI cannot drift CI runs `make lint`, which resolves ruff through uv.lock, while the pre-commit hook resolves its own rev. A compatible-release pin lets the lockfile move to 0.16.x while the hook rev stays put, so both are now pinned to the same exact version and the comment says why. --- .pre-commit-config.yaml | 3 ++- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33487df..0795c51 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,8 @@ repos: rev: v0.10.1 hooks: - id: validate-pyproject - # Keep this rev in sync with the ruff pin in pyproject.toml. + # This rev must match the exact ruff pin in pyproject.toml, which is what CI's + # `make lint` resolves through uv.lock. - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index bd0f086..f660baa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ dev-dependencies = [ "pre-commit>=4.5.1", "kaleido==0.2.1", "nbformat>=5.10.4", - "ruff ~=0.16", + "ruff==0.16.0", "pyright>=1.1.407,<2", # Include optional dependencies for testing "polars>=1.36.1", diff --git a/uv.lock b/uv.lock index bc3b9a5..f803151 100644 --- a/uv.lock +++ b/uv.lock @@ -636,7 +636,7 @@ dev = [ { name = "pytest", specifier = ">=9.1.0" }, { name = "pytest-rerunfailures", specifier = ">=16.3" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = "~=0.16" }, + { name = "ruff", specifier = "==0.16.0" }, ] [[package]]