diff --git a/README.md b/README.md index 06b877d224..d2338d7618 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,9 @@ documentation. `ggshield` works on macOS, Linux and Windows. -It requires **Python 3.8 and newer** (except for standalone packages) and git. +It requires **Python 3.8 or above** (except for standalone packages) and git. + +:warning: Python 3.8 is no longer supported by the Python Software Foundation since October, 14th 2024. GGShield will soon require Python 3.9 or above to run. Some commands require additional programs: diff --git a/changelog.d/20241125_113845_severine.bonnechere_scrt_5123_before_next_ggshield_release.md b/changelog.d/20241125_113845_severine.bonnechere_scrt_5123_before_next_ggshield_release.md new file mode 100644 index 0000000000..2ceb655dcf --- /dev/null +++ b/changelog.d/20241125_113845_severine.bonnechere_scrt_5123_before_next_ggshield_release.md @@ -0,0 +1,41 @@ + + + + + +### Changed + +Warning message: Python 3.8 is no longer supported by the Python Software Foundation since October, 14th 2024. GGShield will soon require Python 3.9 or above to run. + + + + diff --git a/ggshield/__main__.py b/ggshield/__main__.py index 6913e78dc3..fc96d4c76a 100644 --- a/ggshield/__main__.py +++ b/ggshield/__main__.py @@ -23,6 +23,7 @@ from ggshield.cmd.utils.common_options import add_common_options from ggshield.cmd.utils.context_obj import ContextObj from ggshield.cmd.utils.debug import setup_debug_mode +from ggshield.cmd.utils.output_format import OutputFormat from ggshield.core import check_updates, ui from ggshield.core.cache import Cache from ggshield.core.config import Config @@ -138,9 +139,14 @@ def _set_color(ctx: click.Context): ctx.color = True -def _display_deprecation_message(cfg: Config) -> None: - for message in cfg.user_config.deprecation_messages: +def _display_deprecation_message(ctx: click.Context) -> None: + for message in ctx.config.user_config.deprecation_messages: ui.display_warning(message) + if sys.version_info < (3, 9) and ctx.output_format is OutputFormat.TEXT: + ui.display_warning( + "Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run." + ) def _check_for_updates(check_for_updates: bool) -> None: @@ -165,7 +171,7 @@ def before_exit(ctx: click.Context, exit_code: int, *args: Any, **kwargs: Any) - The argument exit_code is the result of the previously executed click command. """ ctx_obj = ContextObj.get(ctx) - _display_deprecation_message(ctx_obj.config) + _display_deprecation_message(ctx_obj) _check_for_updates(ctx_obj.check_for_updates) sys.exit(exit_code) diff --git a/tests/unit/cmd/auth/test_login.py b/tests/unit/cmd/auth/test_login.py index b79acfba08..cdef34007f 100644 --- a/tests/unit/cmd/auth/test_login.py +++ b/tests/unit/cmd/auth/test_login.py @@ -1,4 +1,5 @@ import json +import sys import urllib.parse as urlparse from datetime import datetime, timedelta, timezone from enum import IntEnum, auto @@ -562,6 +563,12 @@ def test_valid_process( 'You do not need to run "ggshield auth login" again. Future requests will automatically use the token.\n' ) + if sys.version_info < (3, 9): + message += ( + "Warning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) + assert output.endswith(message) self._assert_config("mysupertoken") @@ -758,7 +765,12 @@ def _assert_last_print(output: str, expected_str: str): """ assert that the last log output is the same as the one passed in param """ - assert output.rsplit("\n", 2)[-2] == expected_str + if sys.version_info < (3, 9): + expected_str += ( + "\nWarning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) + assert output.endswith(expected_str) def _assert_open_url( self, @@ -887,6 +899,11 @@ def test_bad_sso_url( exit_code, output = self.run_cmd(cli_fs_runner, method=method) assert exit_code > 0, output self._webbrowser_open_mock.assert_not_called() + if sys.version_info < (3, 9): + expected_error += ( + "Warning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) self._assert_last_print(output, expected_error) @pytest.mark.parametrize( diff --git a/tests/unit/cmd/auth/test_logout.py b/tests/unit/cmd/auth/test_logout.py index 9123b28172..cca3aea505 100644 --- a/tests/unit/cmd/auth/test_logout.py +++ b/tests/unit/cmd/auth/test_logout.py @@ -1,3 +1,4 @@ +import sys from typing import Optional, Tuple from unittest.mock import Mock @@ -81,6 +82,12 @@ def test_valid_logout(self, revoke, instance_url, monkeypatch, cli_fs_runner): "from your configuration.\n" ) + if sys.version_info < (3, 9): + expected_output += ( + "Warning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) + assert output == expected_output def test_logout_revoke_timeout(self, monkeypatch, cli_fs_runner): diff --git a/tests/unit/cmd/scan/test_docker.py b/tests/unit/cmd/scan/test_docker.py index 43f7dcc9ec..0651e1c703 100644 --- a/tests/unit/cmd/scan/test_docker.py +++ b/tests/unit/cmd/scan/test_docker.py @@ -1,4 +1,5 @@ import json +import sys from pathlib import Path from unittest.mock import Mock, patch @@ -75,7 +76,14 @@ def test_docker_scan_abort( ["-v", "secret", "scan", "docker", "ggshield-non-existant"], ) assert_invoke_ok(result) - assert result.output == "" + + expected_output = "" + if sys.version_info < (3, 9): + expected_output += ( + "Warning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) + assert result.output == expected_output @patch("ggshield.cmd.secret.scan.docker.docker_save_to_tmp") @patch("ggshield.cmd.secret.scan.docker.docker_scan_archive") diff --git a/tests/unit/cmd/test_config.py b/tests/unit/cmd/test_config.py index d712bc15d5..7a43ad641c 100644 --- a/tests/unit/cmd/test_config.py +++ b/tests/unit/cmd/test_config.py @@ -1,4 +1,5 @@ import json +import sys from datetime import datetime, timezone from typing import Tuple @@ -49,6 +50,15 @@ """ +def _check_expected_output(output: str, expected_output: str): + if sys.version_info < (3, 9) and "Error:" not in expected_output: + expected_output += ( + "Warning: Python 3.8 is no longer supported by the Python Software Foundation. " + "GGShield will soon require Python 3.9 or above to run.\n" + ) + assert output == expected_output + + class TestConfigList: @pytest.fixture @@ -80,7 +90,7 @@ def test_valid_list(self, cli_fs_runner, setup_configs): exit_code, output = self.run_cmd(cli_fs_runner) assert exit_code == ExitCode.SUCCESS, output - assert output == EXPECTED_OUTPUT + _check_expected_output(output, EXPECTED_OUTPUT) def test_list_json_output( self, cli_fs_runner, config_list_json_schema, setup_configs @@ -91,7 +101,6 @@ def test_list_json_output( THEN all configs should be listed with the correct format """ exit_code_json, output_json = self.run_cmd(cli_fs_runner, json=True) - assert exit_code_json == ExitCode.SUCCESS, output_json dct = json.loads(output_json) jsonschema.validate(dct, config_list_json_schema) @@ -149,7 +158,7 @@ def test_set_lifetime_default_config_value(self, value, cli_fs_runner): ), "The instance config should remain unchanged" assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") @pytest.mark.parametrize("value", [0, 365]) def test_set_lifetime_instance_config_value(self, value, cli_fs_runner): @@ -185,7 +194,7 @@ def test_set_lifetime_instance_config_value(self, value, cli_fs_runner): ), "The default auth config should remain unchanged" assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") def test_set_invalid_field_name(self, cli_fs_runner): """ @@ -236,8 +245,8 @@ def test_set_lifetime_invalid_instance(self, cli_fs_runner): exit_code, output = self.run_cmd(cli_fs_runner, 0, instance_url=instance_url) - assert exit_code == ExitCode.AUTHENTICATION_ERROR, output - assert output == f"Error: Unknown instance: '{instance_url}'\n" + assert exit_code == ExitCode.AUTHENTICATION_ERROR + _check_expected_output(output, f"Error: Unknown instance: '{instance_url}'\n") config = Config() assert ( @@ -322,7 +331,7 @@ def test_unset_lifetime_instance_config_value(self, cli_fs_runner): ), "The default auth config should remain unchanged" assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") def test_unset_lifetime_default_config_value(self, cli_fs_runner): """ @@ -343,7 +352,7 @@ def test_unset_lifetime_default_config_value(self, cli_fs_runner): ), "Unrelated instance config should remain unchanged" assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") def test_unset_lifetime_all(self, cli_fs_runner): """ @@ -370,7 +379,7 @@ def test_unset_lifetime_all(self, cli_fs_runner): assert config.auth_config.default_token_lifetime is None, output assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") def test_unset_lifetime_invalid_instance(self, cli_fs_runner): """ @@ -386,7 +395,7 @@ def test_unset_lifetime_invalid_instance(self, cli_fs_runner): exit_code, output = self.run_cmd(cli_fs_runner, instance_url=instance_url) assert exit_code == ExitCode.AUTHENTICATION_ERROR, output - assert output == f"Error: Unknown instance: '{instance_url}'\n" + _check_expected_output(output, f"Error: Unknown instance: '{instance_url}'\n") config = Config() assert ( @@ -409,7 +418,7 @@ def test_unset_instance(self, cli_fs_runner): exit_code, output = self.run_cmd(cli_fs_runner, param="instance") assert exit_code == ExitCode.SUCCESS, output - assert output == "" + _check_expected_output(output, "") config, _ = UserConfig.load(config_path) assert config.instance is None @@ -461,7 +470,7 @@ def test_get_lifetime_default( exit_code, output = self.run_cmd(cli_fs_runner) - assert output == f"default_token_lifetime: {expected_value}\n" + _check_expected_output(output, f"default_token_lifetime: {expected_value}\n") assert exit_code == ExitCode.SUCCESS @pytest.mark.parametrize( @@ -497,7 +506,8 @@ def test_get_lifetime_instance( exit_code, output = self.run_cmd(cli_fs_runner, instance_url=instance_url) - assert output == f"default_token_lifetime: {expected_value}\n" + expected_output = f"default_token_lifetime: {expected_value}\n" + _check_expected_output(output, expected_output) assert exit_code == ExitCode.SUCCESS def test_unset_lifetime_invalid_instance(self, cli_fs_runner): @@ -510,7 +520,7 @@ def test_unset_lifetime_invalid_instance(self, cli_fs_runner): exit_code, output = self.run_cmd(cli_fs_runner, instance_url=instance_url) assert exit_code == ExitCode.AUTHENTICATION_ERROR, output - assert output == f"Error: Unknown instance: '{instance_url}'\n" + _check_expected_output(output, f"Error: Unknown instance: '{instance_url}'\n") def test_get_invalid_field_name(self, cli_fs_runner): """ @@ -546,7 +556,7 @@ def test_get_instance(self, default_value, expected_value, cli_fs_runner): exit_code, output = self.run_cmd(cli_fs_runner, param="instance") - assert output == f"instance: {expected_value}\n" + _check_expected_output(output, f"instance: {expected_value}\n") assert exit_code == ExitCode.SUCCESS @staticmethod