diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec9a1c8aa8..57c321664d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout code diff --git a/.github/workflows/pypi-release-aboutcode-pipeline.yml b/.github/workflows/pypi-release-aboutcode-pipeline.yml index f8fff53b5f..115f9841a3 100644 --- a/.github/workflows/pypi-release-aboutcode-pipeline.yml +++ b/.github/workflows/pypi-release-aboutcode-pipeline.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - name: Install flot run: python -m pip install flot --user diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 3698093b62..84ea29381c 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - name: Install pypa/build run: python -m pip install build --user diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 50459797f5..f459d6d7e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,10 @@ Changelog v34.12.0 (unreleased) --------------------- +- Add support for Python 3.13. + Upgrade the base image in Dockerfile to ``python:3.13-slim``. + https://github.com/aboutcode-org/scancode.io/pull/1469/files + - Display matched snippets details in "Resource viewer", including the package, resource, and similarity values. https://github.com/aboutcode-org/scancode.io/issues/1688 diff --git a/Dockerfile b/Dockerfile index 49205124e7..e624b113f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ # ScanCode.io is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/aboutcode-org/scancode.io for support and download. -FROM python:3.12-slim +FROM python:3.13-slim LABEL org.opencontainers.image.source="https://github.com/aboutcode-org/scancode.io" LABEL org.opencontainers.image.description="ScanCode.io" diff --git a/docs/installation.rst b/docs/installation.rst index 03d2bd402a..83cc1ad197 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -245,7 +245,7 @@ Pre-installation Checklist Before you install ScanCode.io, make sure you have the following prerequisites: - * **Python: versions 3.10 to 3.12** found at https://www.python.org/downloads/ + * **Python: versions 3.10 to 3.13** found at https://www.python.org/downloads/ * **Git**: most recent release available at https://git-scm.com/ * **PostgreSQL**: release 11 or later found at https://www.postgresql.org/ or https://postgresapp.com/ on macOS diff --git a/scanpipe/apps.py b/scanpipe/apps.py index b679fe0d9d..127245b291 100644 --- a/scanpipe/apps.py +++ b/scanpipe/apps.py @@ -20,6 +20,7 @@ # ScanCode.io is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/aboutcode-org/scancode.io for support and download. +import importlib.util import inspect import logging import sys @@ -134,7 +135,14 @@ def register_pipeline_from_file(self, path): after being found. """ module_name = inspect.getmodulename(path) - module = SourceFileLoader(module_name, str(path)).load_module() + + loader = SourceFileLoader(module_name, str(path)) + spec = importlib.util.spec_from_loader(module_name, loader) + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + else: + raise ImportError(f"Could not load module from path: {path}") def is_local_module_pipeline(obj): return is_pipeline(obj) and obj.__module__ == module_name diff --git a/scanpipe/pipes/fetch.py b/scanpipe/pipes/fetch.py index 7151ad7a17..2f74d0cf10 100644 --- a/scanpipe/pipes/fetch.py +++ b/scanpipe/pipes/fetch.py @@ -20,7 +20,6 @@ # ScanCode.io is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/aboutcode-org/scancode.io for support and download. -import cgi import json import logging import os @@ -33,6 +32,7 @@ from urllib.parse import urlparse from django.conf import settings +from django.utils.http import parse_header_parameters import git import requests @@ -126,7 +126,7 @@ def fetch_http(uri, to=None): raise requests.RequestException content_disposition = response.headers.get("content-disposition", "") - _, params = cgi.parse_header(content_disposition) + _, params = parse_header_parameters(content_disposition) filename = params.get("filename") if not filename: # Using `response.url` in place of provided `Scan.uri` since the former diff --git a/scanpipe/pipes/spdx.py b/scanpipe/pipes/spdx.py index b0ccc1db41..ce94d782ea 100644 --- a/scanpipe/pipes/spdx.py +++ b/scanpipe/pipes/spdx.py @@ -26,6 +26,7 @@ from dataclasses import dataclass from dataclasses import field from datetime import datetime +from datetime import timezone from pathlib import Path from typing import List # Python 3.8 compatibility @@ -125,7 +126,7 @@ class CreationInfo: Format: YYYY-MM-DDThh:mm:ssZ """ created: str = field( - default_factory=lambda: datetime.utcnow().isoformat(timespec="seconds") + "Z", + default_factory=lambda: datetime.now(timezone.utc).isoformat(timespec="seconds") ) def as_dict(self): diff --git a/scanpipe/tests/__init__.py b/scanpipe/tests/__init__.py index 59c1fc3faa..9f57c4abfc 100644 --- a/scanpipe/tests/__init__.py +++ b/scanpipe/tests/__init__.py @@ -22,7 +22,9 @@ import os import uuid +import warnings from datetime import datetime +from functools import wraps from unittest import mock from django.apps import apps @@ -48,6 +50,24 @@ mocked_now = mock.Mock(now=lambda: datetime(2010, 10, 10, 10, 10, 10)) +def filter_warnings(action, category, module=None): + """Apply a warning filter to a function.""" + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + original_filters = warnings.filters[:] + try: + warnings.filterwarnings(action, category=category, module=module) + return func(*args, **kwargs) + finally: + warnings.filters = original_filters + + return wrapper + + return decorator + + def make_string(length): return str(uuid.uuid4())[:length] diff --git a/scanpipe/tests/test_api.py b/scanpipe/tests/test_api.py index b8030216d8..7edcc09c67 100644 --- a/scanpipe/tests/test_api.py +++ b/scanpipe/tests/test_api.py @@ -56,6 +56,7 @@ from scanpipe.pipes.input import copy_input from scanpipe.pipes.output import JSONResultsGenerator from scanpipe.tests import dependency_data1 +from scanpipe.tests import filter_warnings from scanpipe.tests import make_message from scanpipe.tests import make_package from scanpipe.tests import make_project @@ -472,6 +473,7 @@ def test_scanpipe_api_project_create_multiple_pipelines(self): } self.assertEqual(expected, response.data) + @filter_warnings("ignore", category=DeprecationWarning, module="scanpipe") def test_scanpipe_api_project_create_pipeline_old_name_compatibility(self): data = { "name": "Single string", @@ -986,6 +988,7 @@ def test_scanpipe_api_project_action_add_pipeline(self, mock_execute_pipeline_ta self.assertEqual({"status": "Pipeline added."}, response.data) mock_execute_pipeline_task.assert_called_once() + @filter_warnings("ignore", category=DeprecationWarning, module="scanpipe") def test_scanpipe_api_project_action_add_pipeline_old_name_compatibility(self): url = reverse("project-add-pipeline", args=[self.project1.uuid]) data = { diff --git a/scanpipe/tests/test_apps.py b/scanpipe/tests/test_apps.py index 638ecddc0c..53a0363937 100644 --- a/scanpipe/tests/test_apps.py +++ b/scanpipe/tests/test_apps.py @@ -32,6 +32,7 @@ from scanpipe.models import Project from scanpipe.models import Run +from scanpipe.tests import filter_warnings from scanpipe.tests import license_policies_index from scanpipe.tests.pipelines.register_from_file import RegisterFromFile @@ -124,6 +125,7 @@ def test_scanpipe_apps_get_pipeline_choices(self): self.assertIn(main_pipeline, choices) self.assertNotIn(addon_pipeline, choices) + @filter_warnings("ignore", category=DeprecationWarning, module="scanpipe") def test_scanpipe_apps_get_new_pipeline_name(self): self.assertEqual( "scan_codebase", scanpipe_app.get_new_pipeline_name("scan_codebase") diff --git a/scanpipe/tests/test_commands.py b/scanpipe/tests/test_commands.py index aba2920148..e41da2c036 100644 --- a/scanpipe/tests/test_commands.py +++ b/scanpipe/tests/test_commands.py @@ -48,6 +48,7 @@ from scanpipe.models import WebhookSubscription from scanpipe.pipes import flag from scanpipe.pipes import purldb +from scanpipe.tests import filter_warnings from scanpipe.tests import make_mock_response from scanpipe.tests import make_package from scanpipe.tests import make_project @@ -391,6 +392,7 @@ def test_scanpipe_management_command_add_input_copy_codebase(self): expected, sorted([path.name for path in project.codebase_path.iterdir()]) ) + @filter_warnings("ignore", category=DeprecationWarning, module="scanpipe") def test_scanpipe_management_command_add_pipeline(self): out = StringIO() @@ -1284,6 +1286,7 @@ def test_scanpipe_management_command_mixin_create_project_notes(self): ) self.assertEqual(notes, project.notes) + @filter_warnings("ignore", category=DeprecationWarning, module="scanpipe") def test_scanpipe_management_command_mixin_create_project_pipelines(self): expected = "non-existing is not a valid pipeline" with self.assertRaisesMessage(CommandError, expected): diff --git a/setup.cfg b/setup.cfg index 10a6899660..3cc80eb8eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Topic :: Utilities keywords = open source @@ -84,7 +85,7 @@ install_requires = go-inspector==0.5.0 rust-inspector==0.1.0 python-inspector==0.14.0 - source-inspector==0.5.1; sys_platform != "darwin" and platform_machine != "arm64" + source-inspector==0.6.1; sys_platform != "darwin" and platform_machine != "arm64" aboutcode-toolkit==11.1.1 # Utilities XlsxWriter==3.2.5