Skip to content

Commit 4bcd81c

Browse files
Saga4mihikap01aseembits93
authored
Warning Message for users using old versions (#701)
* Update main.py and add version_check.py * Update version.py * Address review feedback: remove config option, reduce timeout, fix imports * minor fixes and cleanup * minor fixes --------- Co-authored-by: mihikap01 <mihikap01@gmail.com> Co-authored-by: aseembits93 <aseem.bits@gmail.com>
1 parent 3dcf7a3 commit 4bcd81c

File tree

3 files changed

+293
-2
lines changed

3 files changed

+293
-2
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Version checking utilities for codeflash."""
2+
3+
from __future__ import annotations
4+
5+
import time
6+
7+
import requests
8+
from packaging import version
9+
10+
from codeflash.cli_cmds.console import console, logger
11+
from codeflash.version import __version__
12+
13+
# Simple cache to avoid checking too frequently
14+
_version_cache = {"version": '0.0.0', "timestamp": float(0)}
15+
_cache_duration = 3600 # 1 hour cache
16+
17+
18+
def get_latest_version_from_pypi() -> str | None:
19+
"""Get the latest version of codeflash from PyPI.
20+
21+
Returns:
22+
The latest version string from PyPI, or None if the request fails.
23+
24+
"""
25+
# Check cache first
26+
current_time = time.time()
27+
if _version_cache["version"] is not None and current_time - _version_cache["timestamp"] < _cache_duration:
28+
return _version_cache["version"]
29+
30+
try:
31+
response = requests.get("https://pypi.org/pypi/codeflash/json", timeout=2)
32+
if response.status_code == 200:
33+
data = response.json()
34+
latest_version = data["info"]["version"]
35+
36+
# Update cache
37+
_version_cache["version"] = latest_version
38+
_version_cache["timestamp"] = current_time
39+
40+
return latest_version
41+
logger.debug(f"Failed to fetch version from PyPI: {response.status_code}")
42+
return None # noqa: TRY300
43+
except requests.RequestException as e:
44+
logger.debug(f"Network error fetching version from PyPI: {e}")
45+
return None
46+
except (KeyError, ValueError) as e:
47+
logger.debug(f"Invalid response format from PyPI: {e}")
48+
return None
49+
except Exception as e:
50+
logger.debug(f"Unexpected error fetching version from PyPI: {e}")
51+
return None
52+
53+
54+
def check_for_newer_minor_version() -> None:
55+
"""Check if a newer minor version is available on PyPI and notify the user.
56+
57+
This function compares the current version with the latest version on PyPI.
58+
If a newer minor version is available, it prints an informational message
59+
suggesting the user upgrade.
60+
"""
61+
latest_version = get_latest_version_from_pypi()
62+
63+
if not latest_version:
64+
return
65+
66+
try:
67+
current_parsed = version.parse(__version__)
68+
latest_parsed = version.parse(latest_version)
69+
70+
# Check if there's a newer minor version available
71+
# We only notify for minor version updates, not patch updates
72+
if latest_parsed > current_parsed: # < > == operators can be directly applied on version objects
73+
logger.warning(
74+
f"A newer version({latest_version}) of Codeflash is available, please update soon!"
75+
)
76+
77+
except version.InvalidVersion as e:
78+
logger.debug(f"Invalid version format: {e}")
79+
return

codeflash/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from codeflash.cli_cmds.console import paneled_text
1212
from codeflash.code_utils.checkpoint import ask_should_use_checkpoint_get_functions
1313
from codeflash.code_utils.config_parser import parse_config_file
14+
from codeflash.code_utils.version_check import check_for_newer_minor_version
1415
from codeflash.telemetry import posthog_cf
1516
from codeflash.telemetry.sentry import init_sentry
1617

@@ -21,12 +22,15 @@ def main() -> None:
2122
CODEFLASH_LOGO, panel_args={"title": "https://codeflash.ai", "expand": False}, text_args={"style": "bold gold3"}
2223
)
2324
args = parse_args()
25+
26+
# Check for newer version for all commands
27+
check_for_newer_minor_version()
28+
2429
if args.command:
30+
disable_telemetry = False
2531
if args.config_file and Path.exists(args.config_file):
2632
pyproject_config, _ = parse_config_file(args.config_file)
2733
disable_telemetry = pyproject_config.get("disable_telemetry", False)
28-
else:
29-
disable_telemetry = False
3034
init_sentry(not disable_telemetry, exclude_errors=True)
3135
posthog_cf.initialize_posthog(not disable_telemetry)
3236
args.func()

tests/test_version_check.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""Tests for version checking functionality."""
2+
3+
import unittest
4+
from unittest.mock import Mock, patch, MagicMock
5+
from packaging import version
6+
7+
from codeflash.code_utils.version_check import (
8+
get_latest_version_from_pypi,
9+
check_for_newer_minor_version,
10+
_version_cache,
11+
_cache_duration
12+
)
13+
14+
15+
class TestVersionCheck(unittest.TestCase):
16+
"""Test cases for version checking functionality."""
17+
18+
def setUp(self):
19+
"""Reset version cache before each test."""
20+
_version_cache["version"] = None
21+
_version_cache["timestamp"] = 0
22+
23+
def tearDown(self):
24+
"""Clean up after each test."""
25+
_version_cache["version"] = None
26+
_version_cache["timestamp"] = 0
27+
28+
@patch('codeflash.code_utils.version_check.requests.get')
29+
def test_get_latest_version_from_pypi_success(self, mock_get):
30+
"""Test successful version fetch from PyPI."""
31+
# Mock successful response
32+
mock_response = Mock()
33+
mock_response.status_code = 200
34+
mock_response.json.return_value = {"info": {"version": "1.2.3"}}
35+
mock_get.return_value = mock_response
36+
37+
result = get_latest_version_from_pypi()
38+
39+
self.assertEqual(result, "1.2.3")
40+
mock_get.assert_called_once_with(
41+
"https://pypi.org/pypi/codeflash/json",
42+
timeout=2
43+
)
44+
45+
@patch('codeflash.code_utils.version_check.requests.get')
46+
def test_get_latest_version_from_pypi_http_error(self, mock_get):
47+
"""Test handling of HTTP error responses."""
48+
# Mock HTTP error response
49+
mock_response = Mock()
50+
mock_response.status_code = 404
51+
mock_get.return_value = mock_response
52+
53+
result = get_latest_version_from_pypi()
54+
55+
self.assertIsNone(result)
56+
57+
@patch('codeflash.code_utils.version_check.requests.get')
58+
def test_get_latest_version_from_pypi_network_error(self, mock_get):
59+
"""Test handling of network errors."""
60+
# Mock network error
61+
mock_get.side_effect = Exception("Network error")
62+
63+
result = get_latest_version_from_pypi()
64+
65+
self.assertIsNone(result)
66+
67+
@patch('codeflash.code_utils.version_check.requests.get')
68+
def test_get_latest_version_from_pypi_invalid_response(self, mock_get):
69+
"""Test handling of invalid response format."""
70+
# Mock invalid response format
71+
mock_response = Mock()
72+
mock_response.status_code = 200
73+
mock_response.json.return_value = {"invalid": "format"}
74+
mock_get.return_value = mock_response
75+
76+
result = get_latest_version_from_pypi()
77+
78+
self.assertIsNone(result)
79+
80+
@patch('codeflash.code_utils.version_check.requests.get')
81+
def test_get_latest_version_from_pypi_caching(self, mock_get):
82+
"""Test that version caching works correctly."""
83+
# Mock successful response
84+
mock_response = Mock()
85+
mock_response.status_code = 200
86+
mock_response.json.return_value = {"info": {"version": "1.2.3"}}
87+
mock_get.return_value = mock_response
88+
89+
# First call should hit the network
90+
result1 = get_latest_version_from_pypi()
91+
self.assertEqual(result1, "1.2.3")
92+
self.assertEqual(mock_get.call_count, 1)
93+
94+
# Second call should use cache
95+
result2 = get_latest_version_from_pypi()
96+
self.assertEqual(result2, "1.2.3")
97+
self.assertEqual(mock_get.call_count, 1) # Still only 1 call
98+
99+
@patch('codeflash.code_utils.version_check.requests.get')
100+
def test_get_latest_version_from_pypi_cache_expiry(self, mock_get):
101+
"""Test that cache expires after the specified duration."""
102+
import time
103+
104+
# Mock successful response
105+
mock_response = Mock()
106+
mock_response.status_code = 200
107+
mock_response.json.return_value = {"info": {"version": "1.2.3"}}
108+
mock_get.return_value = mock_response
109+
110+
# First call
111+
result1 = get_latest_version_from_pypi()
112+
self.assertEqual(result1, "1.2.3")
113+
114+
# Manually expire the cache
115+
_version_cache["timestamp"] = time.time() - _cache_duration - 1
116+
117+
# Second call should hit the network again
118+
result2 = get_latest_version_from_pypi()
119+
self.assertEqual(result2, "1.2.3")
120+
self.assertEqual(mock_get.call_count, 2)
121+
122+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
123+
@patch('codeflash.code_utils.version_check.console')
124+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
125+
def test_check_for_newer_minor_version_newer_available(self, mock_console, mock_get_version):
126+
"""Test warning message when newer minor version is available."""
127+
mock_get_version.return_value = "1.1.0"
128+
129+
check_for_newer_minor_version()
130+
131+
mock_console.print.assert_called_once()
132+
call_args = mock_console.print.call_args[0][0]
133+
self.assertIn("ℹ️ A newer version of Codeflash is available!", call_args)
134+
self.assertIn("Current version: 1.0.0", call_args)
135+
self.assertIn("Latest version: 1.1.0", call_args)
136+
137+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
138+
@patch('codeflash.code_utils.version_check.console')
139+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
140+
def test_check_for_newer_minor_version_newer_major_available(self, mock_console, mock_get_version):
141+
"""Test warning message when newer major version is available."""
142+
mock_get_version.return_value = "2.0.0"
143+
144+
check_for_newer_minor_version()
145+
146+
mock_console.print.assert_called_once()
147+
call_args = mock_console.print.call_args[0][0]
148+
self.assertIn("ℹ️ A newer version of Codeflash is available!", call_args)
149+
150+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
151+
@patch('codeflash.code_utils.version_check.console')
152+
@patch('codeflash.code_utils.version_check.__version__', '1.1.0')
153+
def test_check_for_newer_minor_version_no_newer_available(self, mock_console, mock_get_version):
154+
"""Test no warning when no newer version is available."""
155+
mock_get_version.return_value = "1.0.0"
156+
157+
check_for_newer_minor_version()
158+
159+
mock_console.print.assert_not_called()
160+
161+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
162+
@patch('codeflash.code_utils.version_check.console')
163+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
164+
def test_check_for_newer_minor_version_patch_update_ignored(self, mock_console, mock_get_version):
165+
"""Test that patch updates don't trigger warnings."""
166+
mock_get_version.return_value = "1.0.1"
167+
168+
check_for_newer_minor_version()
169+
170+
mock_console.print.assert_not_called()
171+
172+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
173+
@patch('codeflash.code_utils.version_check.console')
174+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
175+
def test_check_for_newer_minor_version_same_version(self, mock_console, mock_get_version):
176+
"""Test no warning when versions are the same."""
177+
mock_get_version.return_value = "1.0.0"
178+
179+
check_for_newer_minor_version()
180+
181+
mock_console.print.assert_not_called()
182+
183+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
184+
@patch('codeflash.code_utils.version_check.console')
185+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
186+
def test_check_for_newer_minor_version_no_latest_version(self, mock_console, mock_get_version):
187+
"""Test no warning when latest version cannot be fetched."""
188+
mock_get_version.return_value = None
189+
190+
check_for_newer_minor_version()
191+
192+
mock_console.print.assert_not_called()
193+
194+
@patch('codeflash.code_utils.version_check.get_latest_version_from_pypi')
195+
@patch('codeflash.code_utils.version_check.console')
196+
@patch('codeflash.code_utils.version_check.__version__', '1.0.0')
197+
def test_check_for_newer_minor_version_invalid_version_format(self, mock_console, mock_get_version):
198+
"""Test handling of invalid version format."""
199+
mock_get_version.return_value = "invalid-version"
200+
201+
check_for_newer_minor_version()
202+
203+
mock_console.print.assert_not_called()
204+
205+
206+
207+
if __name__ == '__main__':
208+
unittest.main()

0 commit comments

Comments
 (0)