From 275498ae35c24b0e044fec110c7a4f48b2f56183 Mon Sep 17 00:00:00 2001 From: Severine Bonnechere Date: Mon, 25 Nov 2024 11:42:31 +0100 Subject: [PATCH 1/3] chore: adding a warning message about Python 3.8 --- README.md | 4 +- ..._scrt_5123_before_next_ggshield_release.md | 41 +++++++++++++++++++ ggshield/__main__.py | 12 ++++-- tests/unit/cmd/auth/test_login.py | 41 +++++++++++++------ tests/unit/cmd/auth/test_logout.py | 7 ++++ tests/unit/cmd/scan/test_docker.py | 10 ++++- tests/unit/cmd/test_config.py | 40 +++++++++++------- 7 files changed, 123 insertions(+), 32 deletions(-) create mode 100644 changelog.d/20241125_113845_severine.bonnechere_scrt_5123_before_next_ggshield_release.md 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..719f38c0d3 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_obj: ContextObj) -> None: + for message in ctx_obj.config.user_config.deprecation_messages: ui.display_warning(message) + if sys.version_info < (3, 9) and ctx_obj.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..be18d7bfc8 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 @@ -321,7 +322,7 @@ def test_existing_token_no_expiry(self, instance_url, cli_fs_runner, monkeypatch self._webbrowser_open_mock.assert_not_called() self._assert_last_print( - output, "ggshield is already authenticated without an expiry date" + output, "ggshield is already authenticated without an expiry date\n" ) @pytest.mark.parametrize( @@ -353,7 +354,7 @@ def test_existing_non_expired_token( self._request_mock.assert_all_requests_happened() self._assert_last_print( - output, f"ggshield is already authenticated until {str_date}, 2100" + output, f"ggshield is already authenticated until {str_date}, 2100\n" ) def test_auth_login_recreates_token_if_deleted_server_side( @@ -394,7 +395,7 @@ def test_no_port_available_exits_error(self, cli_fs_runner, monkeypatch): assert exit_code == ExitCode.UNEXPECTED_ERROR self._webbrowser_open_mock.assert_not_called() - self._assert_last_print(output, "Error: Could not find unoccupied port.") + self._assert_last_print(output, "Error: Could not find unoccupied port.\n") self._assert_config_is_empty() @pytest.mark.parametrize( @@ -426,7 +427,7 @@ def test_invalid_oauth_params_exits_error( self._webbrowser_open_mock.assert_called_once() self._assert_last_print( output, - "Error: Invalid code or state received from the callback.", + "Error: Invalid code or state received from the callback.\n", ) self._assert_config_is_empty() @@ -447,18 +448,18 @@ def test_invalid_code_exchange_exits_error(self, cli_fs_runner, monkeypatch): self._request_mock.assert_all_requests_happened() self._webbrowser_open_mock.assert_called_once() - self._assert_last_print(output, "Error: Cannot create a token: kaboom.") + self._assert_last_print(output, "Error: Cannot create a token: kaboom.\n") @pytest.mark.parametrize( ("login_result", "message"), ( ( LoginResult.GARBAGE_HTML_RESPONSE, - "Error: Server response is not JSON (HTTP code: 418).", + "Error: Server response is not JSON (HTTP code: 418).\n", ), ( LoginResult.GARBAGE_NO_TOKEN_RESPONSE, - "Error: Server did not provide the created token.", + "Error: Server did not provide the created token.\n", ), ), ) @@ -495,7 +496,7 @@ def test_invalid_token_exits_error(self, cli_fs_runner, monkeypatch): self._assert_open_url() self._request_mock.assert_all_requests_happened() - self._assert_last_print(output, "Error: The created token is invalid.") + self._assert_last_print(output, "Error: The created token is invalid.\n") @pytest.mark.parametrize("token_name", [None, "some token name"]) @pytest.mark.parametrize("lifetime", [None, 0, 1, 365]) @@ -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) and "Error:" not in expected_str: + expected_str += ( + "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(expected_str) def _assert_open_url( self, @@ -857,19 +869,19 @@ def _wait_for_callback(self, *args, **kwargs): "web", "https://dashboard.gitguardian.com", "https://onprem.gitguardian.com/auth/sso/1e0f7890-2293-4b2d-8aa8-f6f0e8e92274", - "Error: instance and SSO URL params do not match", + "Error: instance and SSO URL params do not match\n", ], [ "web", "https://dashboard.gitguardian.com", "https://dashboard.gitguardian.com", - "Error: Invalid value for sso-url: Please provide a valid SSO URL.", + "Error: Invalid value for sso-url: Please provide a valid SSO URL.\n", ], [ "token", "https://dashboard.gitguardian.com", "https://dashboard.gitguardian.com/auth/sso/1e0f7890-2293-4b2d-8aa8-f6f0e8e92274", - "Error: Invalid value for sso-url: --sso-url is reserved for the web login method.", + "Error: Invalid value for sso-url: --sso-url is reserved for the web login method.\n", ], ], ) @@ -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) and "Error:" not in expected_error: + 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 From 73904b7fcbfcbe039ad3e4bd64fa40e378e18482 Mon Sep 17 00:00:00 2001 From: Severine Bonnechere Date: Mon, 25 Nov 2024 17:55:16 +0100 Subject: [PATCH 2/3] chore: edit release note --- ...e.bonnechere_scrt_5123_before_next_ggshield_release.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 index 2ceb655dcf..c706563671 100644 --- 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 @@ -17,16 +17,12 @@ Uncomment the section that is right (remove the HTML comment wrapper). --> -### 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. - -