From 95e05b318183d1f5315a88d9d1e78f0f4bc1cd93 Mon Sep 17 00:00:00 2001 From: Chris Clark Date: Wed, 23 Sep 2026 11:17:29 -0400 Subject: [PATCH] Fix CI: pin ruff and make type inference tests pandas 3 compatible - lint.yml ran an unpinned ruff, so new upstream rules broke CI on unchanged code. Pin ruff 0.16.8 and set target-version = "py38" in ruff.toml to match python_requires (newer ruff defaults to py310 and suggests syntax that breaks 3.8/3.9). - Replace percent-formatting flagged by UP031 and noqa PLR0917 on query_viewmodel rather than changing its signature. - pandas 3 infers datetimes as datetime64[us] and text as the str dtype. The SQLite tables produced from uploads are unchanged (same column types and values), so relax the tests to check for any datetime / text dtype. Co-Authored-By: Claude --- .github/workflows/lint.yml | 2 +- explorer/__init__.py | 10 +++------- explorer/tests/factories.py | 2 +- explorer/tests/test_type_infer.py | 27 ++++++++++++++++----------- explorer/views/utils.py | 2 +- ruff.toml | 2 ++ 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c4ae91e8..a91aeb40 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,4 +12,4 @@ jobs: - uses: actions/checkout@v4 - name: Run ruff - run: pipx run ruff check --output-format=github explorer + run: pipx run ruff==0.16.8 check --output-format=github explorer diff --git a/explorer/__init__.py b/explorer/__init__.py index 60c80f57..e1c96d05 100644 --- a/explorer/__init__.py +++ b/explorer/__init__.py @@ -9,15 +9,11 @@ def get_version(short=False): assert __version_info__["releaselevel"] in ("alpha", "beta", "final") - vers = ["%(major)i.%(minor)i" % __version_info__, ] + vers = [f"{__version_info__['major']}.{__version_info__['minor']}", ] if __version_info__["patch"]: - vers.append(".%(patch)i" % __version_info__) + vers.append(f".{__version_info__['patch']}") if __version_info__["releaselevel"] != "final" and not short: - vers.append( - "%s%i" % ( - __version_info__["releaselevel"][0], - __version_info__["serial"]) - ) + vers.append(f"{__version_info__['releaselevel'][0]}{__version_info__['serial']}") return "".join(vers) diff --git a/explorer/tests/factories.py b/explorer/tests/factories.py index 089d8738..a3cf66ad 100644 --- a/explorer/tests/factories.py +++ b/explorer/tests/factories.py @@ -12,7 +12,7 @@ class UserFactory(DjangoModelFactory): class Meta: model = settings.AUTH_USER_MODEL - username = Sequence(lambda n: "User %03d" % n) + username = Sequence(lambda n: f"User {n:03d}") is_staff = True diff --git a/explorer/tests/test_type_infer.py b/explorer/tests/test_type_infer.py index be62b8af..ad8b6d67 100644 --- a/explorer/tests/test_type_infer.py +++ b/explorer/tests/test_type_infer.py @@ -18,6 +18,11 @@ def _get_csv(csv_name): return csv_bytes +def _is_text_dtype(series): + # pandas < 3 stores text as object; pandas 3 uses a dedicated string dtype + return pd.api.types.is_object_dtype(series) or pd.api.types.is_string_dtype(series) + + def _get_json(json_name): current_script_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(current_script_dir, "json", json_name) @@ -34,16 +39,16 @@ class TestCsvToTypedDf(TestCase): def test_mixed_types(self): df = csv_to_typed_df(_get_csv("mixed.csv")) - self.assertTrue(pd.api.types.is_object_dtype(df["Value1"])) - self.assertTrue(pd.api.types.is_object_dtype(df["Value2"])) - self.assertTrue(pd.api.types.is_object_dtype(df["Value3"])) + self.assertTrue(_is_text_dtype(df["Value1"])) + self.assertTrue(_is_text_dtype(df["Value2"])) + self.assertTrue(_is_text_dtype(df["Value3"])) def test_all_types(self): df = csv_to_typed_df(_get_csv("all_types.csv")) - self.assertTrue(pd.api.types.is_datetime64_ns_dtype(df["Dates"])) + self.assertTrue(pd.api.types.is_datetime64_any_dtype(df["Dates"])) self.assertTrue(pd.api.types.is_integer_dtype(df["Integers"])) self.assertTrue(pd.api.types.is_float_dtype(df["Floats"])) - self.assertTrue(pd.api.types.is_object_dtype(df["Strings"])) + self.assertTrue(_is_text_dtype(df["Strings"])) def test_integer_parsing(self): df = csv_to_typed_df(_get_csv("integers.csv")) @@ -62,7 +67,7 @@ def test_date_parsing(self): # Day of Year: 2024-024 (Year-DayOfYear) df = csv_to_typed_df(_get_csv("dates.csv")) - self.assertTrue(pd.api.types.is_datetime64_ns_dtype(df["Dates"])) + self.assertTrue(pd.api.types.is_datetime64_any_dtype(df["Dates"])) @skipIf(not EXPLORER_USER_UPLOADS_ENABLED, "User uploads not enabled") @@ -70,19 +75,19 @@ class TestJsonToTypedDf(TestCase): def test_basic_json(self): df = json_to_typed_df(_get_json("kings.json")) - self.assertTrue(pd.api.types.is_object_dtype(df["Name"])) - self.assertTrue(pd.api.types.is_object_dtype(df["Country"])) + self.assertTrue(_is_text_dtype(df["Name"])) + self.assertTrue(_is_text_dtype(df["Country"])) self.assertTrue(pd.api.types.is_integer_dtype(df["ID"])) def test_nested_json(self): df = json_to_typed_df(_get_json("github.json")) - self.assertTrue(pd.api.types.is_object_dtype(df["subscription_url"])) - self.assertTrue(pd.api.types.is_object_dtype(df["topics"])) + self.assertTrue(_is_text_dtype(df["subscription_url"])) + self.assertTrue(_is_text_dtype(df["topics"])) self.assertTrue(pd.api.types.is_integer_dtype(df["size"])) self.assertTrue(pd.api.types.is_integer_dtype(df["owner.id"])) def test_json_list(self): df = json_list_to_typed_df(_get_json("list.json")) self.assertTrue(pd.api.types.is_integer_dtype(df["Item.value.M.unique_connection_count.N"])) - self.assertTrue(pd.api.types.is_object_dtype(df["Item.instanceId.S"])) + self.assertTrue(_is_text_dtype(df["Item.instanceId.S"])) self.assertEqual(len(df), 5) diff --git a/explorer/views/utils.py b/explorer/views/utils.py index a01f2528..efd09437 100644 --- a/explorer/views/utils.py +++ b/explorer/views/utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -def query_viewmodel(request, query, title=None, form=None, message=None, +def query_viewmodel(request, query, title=None, form=None, message=None, # noqa: PLR0917 run_query=True, error=None, rows=app_settings.EXPLORER_DEFAULT_ROWS): """ diff --git a/ruff.toml b/ruff.toml index c31d99f2..b90f1033 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,6 @@ line-length = 120 +# Keep in sync with python_requires in setup.py; ruff can't read it from there. +target-version = "py38" extend-exclude = [ ".ruff_cache",