Skip to content

Commit 7fb0120

Browse files
committed
fix: version detection in executables
1 parent c1ee262 commit 7fb0120

5 files changed

Lines changed: 83 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to Shello CLI will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.2] - 2026-01-24
9+
10+
### Fixed
11+
- **Update Command in Frozen Executables**: Fixed critical bug where `/update` command failed in PyInstaller-built executables with "Could not determine current version" error
12+
- Changed version detection from file-based reading to module import
13+
- Now uses `import shello_cli; version = shello_cli.__version__` instead of reading `__init__.py` as a file
14+
- Works correctly in both development mode and frozen executables
15+
- Updated all related tests to match new implementation
16+
17+
### Documentation
18+
- **Community Guidelines**: Added CODE_OF_CONDUCT.md for community standards and expectations
19+
820
## [0.5.1] - 2026-01-22
921

1022
### Added
@@ -724,7 +736,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
724736
### Note
725737
This is the initial release of Shello CLI. While fully functional, expect improvements and potential breaking changes before v1.0.0. Feedback and contributions are welcome!
726738

727-
[Unreleased]: https://github.com/om-mapari/shello-cli/compare/v0.5.1...HEAD
739+
[Unreleased]: https://github.com/om-mapari/shello-cli/compare/v0.5.2...HEAD
740+
[0.5.2]: https://github.com/om-mapari/shello-cli/releases/tag/v0.5.2
728741
[0.5.1]: https://github.com/om-mapari/shello-cli/releases/tag/v0.5.1
729742
[0.4.3]: https://github.com/om-mapari/shello-cli/releases/tag/v0.4.3
730743
[0.4.2]: https://github.com/om-mapari/shello-cli/releases/tag/v0.4.2

shello_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Shello CLI
22
AI Assistant with Command Execution"""
33

4-
__version__ = "0.5.1"
4+
__version__ = "0.5.2"

shello_cli/update/version_checker.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Version checking functionality for Shello CLI updates."""
22

3-
import re
4-
from pathlib import Path
53
from typing import Optional
64

75
import requests
@@ -25,33 +23,26 @@ def __init__(self, repo_owner: str, repo_name: str, timeout: int = 5):
2523
self.api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
2624

2725
def get_current_version(self) -> str:
28-
"""Get current version from shello_cli/__init__.py.
26+
"""Get current version from shello_cli.__init__.
2927
3028
Returns:
3129
Current version string (e.g., "0.4.3")
3230
3331
Raises:
34-
ValueError: If version cannot be parsed from __init__.py
32+
ValueError: If version cannot be determined
3533
"""
36-
# Get the path to shello_cli/__init__.py
37-
# This file is in shello_cli/update/version_checker.py
38-
# So we need to go up one level to get to shello_cli/__init__.py
39-
init_file = Path(__file__).parent.parent / "__init__.py"
40-
4134
try:
42-
content = init_file.read_text(encoding="utf-8")
43-
except Exception as e:
44-
raise ValueError(f"Failed to read version file: {e}")
45-
46-
# Parse __version__ = "x.y.z" with flexible formatting
47-
# Handles single quotes, double quotes, and various spacing
48-
pattern = r'__version__\s*=\s*["\']([^"\']+)["\']'
49-
match = re.search(pattern, content)
50-
51-
if not match:
52-
raise ValueError("Could not find __version__ in __init__.py")
53-
54-
return match.group(1)
35+
# Import version directly from the module
36+
# This works in both regular Python and PyInstaller frozen executables
37+
import shello_cli
38+
version = shello_cli.__version__
39+
40+
if not version:
41+
raise ValueError("__version__ is empty")
42+
43+
return version
44+
except (ImportError, AttributeError) as e:
45+
raise ValueError(f"Could not determine current version: {e}")
5546

5647
def get_latest_version(self) -> Optional[str]:
5748
"""Query GitHub API for latest release version.

tests/test_direct_executor.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,41 @@
77

88
import pytest
99
import os
10+
import platform
1011
import tempfile
1112
from hypothesis import given, strategies as st, settings, assume
1213
from shello_cli.commands.direct_executor import DirectExecutor, ExecutionResult
1314
from shello_cli.tools.bash_tool import BashTool
1415
from shello_cli.commands.command_detector import CommandDetector
1516

1617

18+
# Platform-specific command mappings
19+
def get_platform_commands():
20+
"""Get platform-appropriate commands for testing."""
21+
is_windows = platform.system() == 'Windows'
22+
23+
if is_windows:
24+
return {
25+
'pwd': 'cd', # Windows cmd uses 'cd' to show current directory
26+
'ls': 'dir',
27+
'echo': 'echo'
28+
}
29+
else:
30+
return {
31+
'pwd': 'pwd',
32+
'ls': 'ls',
33+
'echo': 'echo'
34+
}
35+
36+
37+
PLATFORM_COMMANDS = get_platform_commands()
38+
39+
1740
class TestDirectExecutorProperties:
1841
"""Property-based tests for DirectExecutor."""
1942

2043
@given(
21-
command=st.sampled_from(['pwd', 'echo', 'ls', 'dir'])
44+
command=st.sampled_from([PLATFORM_COMMANDS['pwd'], PLATFORM_COMMANDS['echo'], PLATFORM_COMMANDS['ls']])
2245
)
2346
@settings(max_examples=100, deadline=None)
2447
def test_property_3_command_execution_produces_output(self, command):
@@ -125,13 +148,15 @@ class TestDirectExecutorUnitTests:
125148
"""Unit tests for specific DirectExecutor scenarios."""
126149

127150
def test_simple_command_execution(self):
128-
"""Test executing a simple command like 'pwd'."""
151+
"""Test executing a simple command like 'echo'."""
129152
executor = DirectExecutor()
130153

131-
result = executor.execute('pwd')
154+
# Use echo command which works consistently across platforms
155+
result = executor.execute(PLATFORM_COMMANDS['echo'], 'test')
132156

133157
assert result.success
134158
assert result.output
159+
assert 'test' in result.output.lower()
135160
assert result.error is None
136161
assert not result.directory_changed
137162
assert result.new_directory is None
@@ -140,8 +165,8 @@ def test_command_with_args(self):
140165
"""Test executing a command with arguments."""
141166
executor = DirectExecutor()
142167

143-
# Use 'ls' which is in the default allowlist
144-
result = executor.execute('ls', '.')
168+
# Use platform-appropriate list command
169+
result = executor.execute(PLATFORM_COMMANDS['ls'], '.')
145170

146171
assert result.success
147172
assert result.output

tests/test_version_checker.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Tests for the VersionChecker component."""
22

33
import re
4-
from pathlib import Path
5-
from unittest.mock import Mock, patch, mock_open
4+
from unittest.mock import Mock, patch
65

76
import pytest
87
import requests
@@ -23,44 +22,45 @@ def test_init(self):
2322
assert checker.api_url == "https://api.github.com/repos/owner/repo/releases/latest"
2423

2524
def test_get_current_version_success(self):
26-
"""Test getting current version from __init__.py."""
25+
"""Test getting current version from shello_cli module."""
2726
checker = VersionChecker("owner", "repo")
2827
version = checker.get_current_version()
2928

30-
# Should read from actual shello_cli/__init__.py
29+
# Should import from actual shello_cli.__version__
3130
assert isinstance(version, str)
3231
assert re.match(r'\d+\.\d+\.\d+', version)
3332

34-
def test_get_current_version_with_different_formats(self):
35-
"""Test version parsing with different quote styles."""
33+
def test_get_current_version_import_error(self):
34+
"""Test error when shello_cli module cannot be imported."""
3635
checker = VersionChecker("owner", "repo")
3736

38-
test_cases = [
39-
'__version__ = "1.2.3"',
40-
"__version__ = '1.2.3'",
41-
'__version__="1.2.3"',
42-
'__version__ = "1.2.3"',
43-
]
44-
45-
for content in test_cases:
46-
with patch("pathlib.Path.read_text", return_value=content):
47-
version = checker.get_current_version()
48-
assert version == "1.2.3"
37+
# Patch the import statement inside get_current_version
38+
with patch.dict('sys.modules', {'shello_cli': None}):
39+
with pytest.raises(ValueError, match="Could not determine current version"):
40+
checker.get_current_version()
4941

50-
def test_get_current_version_missing_version(self):
51-
"""Test error when __version__ is missing."""
42+
def test_get_current_version_missing_attribute(self):
43+
"""Test error when __version__ attribute is missing."""
5244
checker = VersionChecker("owner", "repo")
5345

54-
with patch("pathlib.Path.read_text", return_value="# No version here"):
55-
with pytest.raises(ValueError, match="Could not find __version__"):
46+
# Create a mock module without __version__
47+
mock_module = Mock(spec=[])
48+
del mock_module.__version__ # Ensure __version__ doesn't exist
49+
50+
with patch.dict('sys.modules', {'shello_cli': mock_module}):
51+
with pytest.raises(ValueError, match="Could not determine current version"):
5652
checker.get_current_version()
5753

58-
def test_get_current_version_file_read_error(self):
59-
"""Test error when file cannot be read."""
54+
def test_get_current_version_empty_version(self):
55+
"""Test error when __version__ is empty."""
6056
checker = VersionChecker("owner", "repo")
6157

62-
with patch("pathlib.Path.read_text", side_effect=IOError("File not found")):
63-
with pytest.raises(ValueError, match="Failed to read version file"):
58+
# Create a mock module with empty __version__
59+
mock_module = Mock()
60+
mock_module.__version__ = ""
61+
62+
with patch.dict('sys.modules', {'shello_cli': mock_module}):
63+
with pytest.raises(ValueError, match="__version__ is empty"):
6464
checker.get_current_version()
6565

6666
@patch("requests.get")

0 commit comments

Comments
 (0)